// ── 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 = `
${typeMsg}
`; } 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 [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) { html += `
${label}
`; } 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 = `
◈ COMPOSE MESSAGE
`; 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) {} }