// ── ARC REACTOR STATUS ────────────────────────────────────────────────
let _arcOnline = false;
let _arcJobs = { queued: 0, running: 0, done: 0, failed: 0 };
async function checkArcStatus() {
const dot = document.getElementById('bb-arc-dot');
const sta = document.getElementById('bb-arc-status');
if (!dot || !sta) return;
try {
const d = await api('arc?action=status');
if (d && d.online) {
_arcOnline = true;
dot.className = 'bb-dot online';
const active = (d.active_jobs || 0) + (d.queued_jobs || 0);
sta.textContent = active > 0 ? active + ' JOB' + (active !== 1 ? 'S' : '') : 'ONLINE';
_arcJobs = { queued: d.queued_jobs||0, running: d.running_jobs||0,
done: d.jobs_done||0, failed: d.jobs_failed||0 };
} else {
_arcOnline = false;
dot.className = 'bb-dot offline';
sta.textContent = 'OFFLINE';
}
} catch(e) {
_arcOnline = false;
dot.className = 'bb-dot offline';
sta.textContent = 'OFFLINE';
}
}
// Submit a job to the Arc Reactor and return job_id
async function arcSubmitJob(type, payload, priority) {
payload = payload || {};
priority = priority || 5;
const d = await api('arc', { action: 'job_create', type: type, payload: payload, priority: priority });
return d.job_id || null;
}
// Poll a job until done or failed (max 120s), calling onProgress each tick
async function arcWaitJob(jobId, onProgress) {
var start = Date.now();
while (Date.now() - start < 120000) {
const d = await api('arc?action=job_get&id=' + jobId);
if (onProgress) onProgress(d);
if (d.status === 'done') return d;
if (d.status === 'failed') throw new Error(d.error || 'Job failed');
await new Promise(function(r){ setTimeout(r, 1500); });
}
throw new Error('Arc Reactor job timed out');
}
// ── INTEL PROTOCOL — HUD panel ────────────────────────────────────────
let _intelPollTimer = null;
let _intelActiveJobs = new Set();
let _intelLastLoad = 0;
async function loadIntel() {
const el = document.getElementById('intel-list');
if (!el) return;
_intelLastLoad = Date.now();
try {
// Fetch recent research + tool_loop jobs
const [resJobs, toolJobs] = await Promise.all([
api('arc?action=jobs&status=&limit=20').catch(() => []),
Promise.resolve([]),
]);
const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : [];
if (!jobs.length) {
el.innerHTML = '
◈ NO INTEL JOBS
Say "research [topic]" to activate
';
stopIntelPolling();
return;
}
// Check for active jobs
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
if (hasActive) startIntelPolling(); else stopIntelPolling();
let html = '';
for (const job of jobs) {
const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running';
const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running';
const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase();
const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM';
// Get result details if done
let bodyHtml = '';
if (job.status === 'done' && job.result) {
let r = job.result;
if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} }
if (typeof r === 'object') {
const synthesis = (r.synthesis || r.result || r.response || '').trim();
const sources = r.sources || [];
const query = r.query || r.task || '';
const provider = r.provider || '';
bodyHtml = ``;
if (provider) bodyHtml += `
PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}
`;
if (synthesis) bodyHtml += `
${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'\n\n[...truncated — view in admin]':''}
`;
if (sources.length) {
bodyHtml += '
SOURCES
';
sources.slice(0,5).forEach((s,i) => {
const title = escHtml((s.title||s.url||'').substring(0,60));
const url = escHtml(s.url||'');
bodyHtml += `
`;
});
bodyHtml += '
';
}
bodyHtml += '
';
}
} else if (job.status === 'running' || job.status === 'queued') {
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
bodyHtml = ``;
} else if (job.status === 'failed' && job.error) {
bodyHtml = `${escHtml(job.error.substring(0,200))}
`;
}
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
html += `
${typeLabel}
#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}
${ts}
${statusLabel}
${bodyHtml}
`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = 'INTEL OFFLINE
';
}
}
function toggleIntelCard(id) {
const card = document.getElementById('intel-card-' + id);
if (!card) return;
if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id);
else _intelActiveJobs.add(id);
card.classList.toggle('open');
}
function startIntelPolling() {
if (_intelPollTimer) return;
_intelPollTimer = setInterval(() => {
if (document.getElementById('tab-intel')?.classList.contains('active')) {
loadIntel();
}
}, 4000);
}
function stopIntelPolling() {
if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; }
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function intelPrompt() {
const input = document.getElementById('textInput');
if (input) { input.value = 'research '; input.focus(); }
}
// Called when arc_job is returned from chat response
function onArcJobStarted(jobId, jobType) {
const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep'];
if (commsTypes.includes(jobType)) {
const commsBtn = document.getElementById('tab-btn-comms');
if (commsBtn) commsBtn.click();
startCommsPolling();
} else {
_intelActiveJobs.add(jobId);
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
if (intelTab) intelTab.click();
startIntelPolling();
}
}
// ── COMMS PROTOCOL — email triage HUD ────────────────────────────────────
let _commsPollTimer = null;
let _commsFilter = 'priority';
let _commsOpenCards = new Set();
async function loadComms() {
const el = document.getElementById('comms-list');
if (!el) return;
try {
const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter);
const items = Array.isArray(res) ? res : (res.items || []);
if (!items.length) {
el.innerHTML = ''
+ '◈ NO TRIAGE DATA
Say "check my email" to activate
';
stopCommsPolling();
return;
}
const catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6};
const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'};
let html = '';
html += '';
html += '';
html += '
';
html += '';
for (const item of items) {
const cat = item.category || 'info';
const icon = catIcons[cat] || '◈';
const prio = item.priority || 0;
const isOpen = _commsOpenCards.has(item.id);
const hasReply = item.draft_reply && item.draft_reply.trim().length > 5;
html += `
${icon} ${cat.toUpperCase()}
${escHtml((item.subject||'(no subject)').substring(0,60))}
${prio}/10
FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}
${escHtml(item.summary||'')}
${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''}
${hasReply ? `` : ''}
${hasReply ? `` : ''}
`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = 'COMMS OFFLINE
';
}
}
function toggleCommsCard(id) {
const card = document.getElementById('comms-card-' + id);
if (!card) return;
if (_commsOpenCards.has(id)) _commsOpenCards.delete(id);
else _commsOpenCards.add(id);
card.classList.toggle('open');
}
function commsSetFilter(f) {
_commsFilter = f;
loadComms();
}
async function commsDismiss(id) {
await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {});
loadComms();
}
async function commsCopyReply(id) {
const draft = document.querySelector(`#comms-draft-${id}`);
if (draft) {
navigator.clipboard.writeText(draft.innerText).catch(() => {});
const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`);
if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY', 1500); }
}
}
async function commsSendReply(id) {
const btn = document.getElementById('comms-send-' + id);
const draft = document.getElementById('comms-draft-' + id);
if (!btn || !draft) return;
btn.disabled = true;
btn.textContent = '◈ SENDING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create',
type: 'send_email',
payload: { triage_id: id, content: draft.innerText },
priority: 8,
});
if (res.job_id) {
btn.textContent = '◈ SENT ✓';
btn.style.color = '#00ff88';
setTimeout(() => loadComms(), 3000);
loadCommsOutbox();
} else {
btn.disabled = false;
btn.textContent = '◈ SEND REPLY';
alert('Send failed: ' + (res.error || 'unknown error'));
}
} catch(e) {
btn.disabled = false;
btn.textContent = '◈ SEND REPLY';
}
}
function commsShowCompose() {
const existing = document.getElementById('comms-compose-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.className = 'comms-compose-modal';
modal.id = 'comms-compose-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
}
let _ccDraftedBody = '';
async function commsComposeDraft() {
const to = document.getElementById('cc-to')?.value.trim();
const subject = document.getElementById('cc-subject')?.value.trim();
const instructions = document.getElementById('cc-instructions')?.value.trim();
const account = document.getElementById('cc-account')?.value;
const status = document.getElementById('cc-status');
if (!to || !instructions) { if (status) status.textContent = 'Please fill in To and message description.'; return; }
if (status) status.textContent = '◈ DRAFTING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create', type: 'compose_email',
payload: { recipient: to, subject, instructions, account, auto_send: false },
priority: 7,
});
if (!res.job_id) throw new Error(res.error || 'No job');
// poll for result
let attempts = 0;
const poll = async () => {
const job = await api('arc?action=job_get&id=' + res.job_id);
if (job.status === 'done' && job.result?.drafted_body) {
_ccDraftedBody = job.result.drafted_body;
document.getElementById('cc-preview-body').textContent = _ccDraftedBody;
document.getElementById('cc-preview').style.display = 'block';
document.getElementById('cc-send-btn').style.display = '';
if (status) status.textContent = '◈ DRAFT READY — Review and send';
} else if (job.status === 'failed') {
if (status) status.textContent = '✗ Draft failed: ' + (job.error || 'unknown');
} else if (attempts++ < 20) {
setTimeout(poll, 1500);
} else {
if (status) status.textContent = '◈ Job still running — check INTEL tab';
}
};
setTimeout(poll, 1500);
} catch(e) {
if (status) status.textContent = '✗ Error: ' + e.message;
}
}
async function commsComposeAndSend() {
const to = document.getElementById('cc-to')?.value.trim();
const subject = document.getElementById('cc-subject')?.value.trim();
const account = document.getElementById('cc-account')?.value;
const status = document.getElementById('cc-status');
const btn = document.getElementById('cc-send-btn');
if (!to || !_ccDraftedBody) return;
if (btn) { btn.disabled = true; btn.textContent = '◈ SENDING…'; }
if (status) status.textContent = '◈ TRANSMITTING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create', type: 'send_email',
payload: { to_email: to, subject, body: _ccDraftedBody, account },
priority: 9,
});
if (res.job_id) {
if (status) status.textContent = '◈ SENT ✓ (Job #' + res.job_id + ')';
setTimeout(() => {
document.getElementById('comms-compose-modal')?.remove();
loadCommsOutbox();
}, 1500);
} else {
if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; }
if (status) status.textContent = '✗ Send failed: ' + (res.error || 'unknown');
}
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; }
if (status) status.textContent = '✗ Error: ' + e.message;
}
}
async function loadCommsOutbox() {
const el = document.getElementById('comms-outbox');
if (!el) return;
try {
const data = await api('arc?action=comms_sent&limit=20');
const sent = Array.isArray(data) ? data : (data.sent || []);
if (!sent.length) {
el.innerHTML = 'No sent messages yet
';
return;
}
const statusColor = {sent:'#00ff88', failed:'#ff2244', queued:'#ffd700'};
let html = '';
for (const m of sent) {
const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—';
const sc = m.status || 'sent';
html += `
TO: ${escHtml((m.to_email||'').substring(0,40))}
${sc.toUpperCase()}
${escHtml((m.subject||'(no subject)').substring(0,60))}
${ts} · ${m.account||'gmail'}
`;
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = 'OUTBOX OFFLINE
';
}
}
function commsTriageNow() {
const input = document.getElementById('textInput');
if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); }
}
function startCommsPolling() {
if (_commsPollTimer) return;
_commsPollTimer = setInterval(() => {
if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); }
}, 8000);
}
function stopCommsPolling() {
if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; }
}
// ── GUARDIAN MODE ─────────────────────────────────────────────────────────────
let _guardianPollTimer = null;
let _guardianChatTimer = null;
let _guardianLastChat = '';
let _guardianUnread = 0;
async function loadGuardian() {
const el = document.getElementById('guardian-list');
if (!el) return;
try {
const [statusData, eventsData] = await Promise.all([
api('arc?action=guardian_status').catch(() => ({})),
api('arc?action=guardian_events&limit=40').catch(() => []),
]);
const events = Array.isArray(eventsData) ? eventsData : [];
const status = statusData || {};
const counts = status.counts || {};
const unread = parseInt(counts.unread || 0);
const critU = parseInt(counts.critical_unread || 0);
_guardianUnread = unread;
_updateGuardianBadge(unread, critU);
if (critU > 0 && document.hidden && 'Notification' in window && Notification.permission === 'granted') {
new Notification('JARVIS ALERT', {
body: critU + ' critical alert' + (critU > 1 ? 's' : '') + ' require your attention.',
icon: '/favicon.ico',
});
}
const lastScan = status.last_scan
? new Date(status.last_scan + 'Z').toLocaleTimeString()
: '—';
let html = `
◈ GUARDIAN MODE
${status.enabled ? '● ACTIVE' : '○ INACTIVE'}
SCAN: ${lastScan}
${unread ? `` : ''}
`;
if (!events.length) {
html += '◈ ALL CLEAR
Guardian is watching...
';
} else {
for (const ev of events) {
const sev = ev.severity || 'info';
const acked = ev.acknowledged;
const ts = ev.created_at ? new Date(ev.created_at).toLocaleTimeString() : '';
const typeIco = {agent_offline:'⚠',agent_online:'✓',cpu_high:'⚡',
mem_high:'⚡',disk_high:'💾',service_down:'✗',
service_recovered:'✓',sitrep:'◈',anomaly:'◈'}[ev.event_type] || '◈';
html += `
${sev.toUpperCase()}
${typeIco} ${escHtml(ev.message||'')}
${ev.ai_analysis ? `
${escHtml(ev.ai_analysis.substring(0,200))}
` : ''}
${ts}
${!acked ? `` : ''}
`;
}
}
el.innerHTML = html;
startGuardianPolling();
} catch(e) {
if (el) el.innerHTML = 'GUARDIAN OFFLINE
';
}
}
function _updateGuardianBadge(unread, critical) {
const dot = document.getElementById('bb-guardian-dot');
const badge = document.getElementById('bb-guardian-badge');
const status = document.getElementById('bb-guardian-status');
if (!dot) return;
dot.className = 'bb-dot';
if (critical > 0) {
dot.classList.add('critical'); status.textContent = 'ALERT'; status.style.color = 'var(--red)';
} else if (unread > 0) {
dot.classList.add('warning'); status.textContent = 'WARNING'; status.style.color = '#f5a623';
} else {
dot.classList.add('all-clear'); status.textContent = 'CLEAR'; status.style.color = 'var(--green)';
}
if (unread > 0) {
badge.textContent = unread; badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
async function guardianAck(id) {
await api('arc?action=guardian_ack&id=' + id).catch(() => {});
const ev = document.getElementById('gev-' + id);
if (ev) ev.classList.add('acked');
_guardianUnread = Math.max(0, _guardianUnread - 1);
_updateGuardianBadge(_guardianUnread, 0);
}
async function guardianAckAll() {
await api('arc?action=guardian_ack').catch(() => {});
loadGuardian();
}
function guardianSitrep() {
const input = document.getElementById('textInput');
if (input) { input.value = 'sitrep'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); }
}
function switchGuardianTab() {
const btn = document.getElementById('tab-btn-guardian');
if (btn) btn.click();
}
function startGuardianPolling() {
if (_guardianPollTimer) return;
_guardianPollTimer = setInterval(() => {
if (document.getElementById('tab-guardian')?.classList.contains('active')) loadGuardian();
else _refreshGuardianBadge();
}, 30000);
}
async function _refreshGuardianBadge() {
const s = await api('arc?action=guardian_status').catch(() => null);
if (!s) return;
const counts = s.counts || {};
_updateGuardianBadge(parseInt(counts.unread||0), parseInt(counts.critical_unread||0));
}
// Proactive chat polling — checks for guardian-injected messages every 30s
let _proactiveChatLastId = 0;
async function _pollProactiveChat() {
try {
const rows = await api('arc?action=guardian_chat').catch(() => []);
if (!Array.isArray(rows)) return;
for (const row of rows) {
if (row.id > _proactiveChatLastId) {
_proactiveChatLastId = row.id;
// Don't spam on first load — only show messages from last 5 min
const age = Date.now() - new Date(row.created_at + 'Z').getTime();
if (age < 300000) {
addMessage('jarvis', row.message);
speak(row.message);
}
}
}
} catch(e) {}
}