Adopt live production state as git baseline

This commit is contained in:
root
2026-07-07 07:55:47 -05:00
commit 588cfe3f10
85 changed files with 30896 additions and 0 deletions
+608
View File
@@ -0,0 +1,608 @@
// ── 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 = '<div class="intel-empty">◈ NO INTEL JOBS<br><span style="opacity:0.5">Say "research [topic]" to activate</span></div>';
stopIntelPolling();
return;
}
// Check for active jobs
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
if (hasActive) startIntelPolling(); else stopIntelPolling();
let html = '<button class="intel-new-btn" onclick="intelPrompt()">⚡ NEW RESEARCH</button>';
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 = `<div class="intel-card-body">`;
if (provider) bodyHtml += `<div style="font-size:0.55rem;color:var(--text-dim);margin:6px 0 2px;font-family:var(--font-mono)">PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}</div>`;
if (synthesis) bodyHtml += `<div class="synthesis">${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'\n\n[...truncated — view in admin]':''}</div>`;
if (sources.length) {
bodyHtml += '<div class="intel-sources"><div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:4px;font-family:var(--font-display)">SOURCES</div>';
sources.slice(0,5).forEach((s,i) => {
const title = escHtml((s.title||s.url||'').substring(0,60));
const url = escHtml(s.url||'');
bodyHtml += `<div class="intel-source">${i+1}. <a href="${url}" target="_blank" rel="noopener">${title||url}</a></div>`;
});
bodyHtml += '</div>';
}
bodyHtml += '</div>';
}
} else if (job.status === 'running' || job.status === 'queued') {
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--text-dim);padding:8px 0;font-family:var(--font-mono)">${typeMsg}</div></div>`;
} else if (job.status === 'failed' && job.error) {
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--red);padding:8px 0;font-family:var(--font-mono)">${escHtml(job.error.substring(0,200))}</div></div>`;
}
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
html += `<div class="intel-card${(isOpen && bodyHtml) ? ' open':''}" id="intel-card-${job.id}">
<div class="intel-card-head" onclick="toggleIntelCard(${job.id})">
<span style="font-size:0.55rem;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0">${typeLabel}</span>
<span class="intel-card-query">#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}</span>
<span style="font-size:0.55rem;color:var(--text-dim);flex-shrink:0;font-family:var(--font-mono)">${ts}</span>
<span class="intel-card-status ${statusClass}">${statusLabel}</span>
</div>
${bodyHtml}
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="intel-empty">INTEL OFFLINE</div>';
}
}
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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 = '<button class="comms-triage-btn" onclick="commsTriageNow()">◈ TRIAGE INBOX NOW</button>'
+ '<div class="comms-empty">◈ NO TRIAGE DATA<br><span style="opacity:0.5">Say "check my email" to activate</span></div>';
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 = '<div style="display:flex;gap:5px;margin-bottom:5px">';
html += '<button class="comms-triage-btn" style="flex:3;margin-bottom:0" onclick="commsTriageNow()">◈ TRIAGE INBOX</button>';
html += '<button class="comms-compose-btn" style="flex:2;margin-bottom:0" onclick="commsShowCompose()">+ COMPOSE</button>';
html += '</div>';
html += '<div class="comms-header-bar">';
for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) {
html += `<div class="comms-filter-btn${_commsFilter===f?' active':''}" onclick="commsSetFilter('${f}')">${label}</div>`;
}
html += '</div>';
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 += `<div class="comms-card${isOpen?' open':''}" id="comms-card-${item.id}">
<div class="comms-card-head" onclick="toggleCommsCard(${item.id})">
<span class="comms-card-cat ${cat}">${icon} ${cat.toUpperCase()}</span>
<span class="comms-card-subject">${escHtml((item.subject||'(no subject)').substring(0,60))}</span>
<span class="comms-prio">${prio}/10</span>
</div>
<div class="comms-card-body">
<div class="comms-card-from">FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}</div>
<div class="comms-card-summary">${escHtml(item.summary||'')}</div>
${hasReply ? `<div class="comms-draft-label">DRAFT REPLY</div><div class="comms-draft" id="comms-draft-${item.id}">${escHtml(item.draft_reply)}</div>` : ''}
<div style="display:flex;gap:5px;margin-top:8px">
${hasReply ? `<button class="comms-send-btn" id="comms-send-${item.id}" onclick="commsSendReply(${item.id})">◈ SEND REPLY</button>` : ''}
${hasReply ? `<button onclick="commsCopyReply(${item.id})" style="flex:1;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">COPY</button>` : ''}
<button onclick="commsDismiss(${item.id})" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">DISMISS</button>
</div>
</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">COMMS OFFLINE</div>';
}
}
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 = `
<div class="comms-compose-inner">
<div class="comms-compose-title">◈ COMPOSE MESSAGE</div>
<select id="cc-account" class="comms-compose-field" style="cursor:pointer">
<option value="gmail">Gmail</option>
<option value="icloud">iCloud</option>
</select>
<input id="cc-to" class="comms-compose-field" placeholder="To: email address" type="email">
<input id="cc-subject" class="comms-compose-field" placeholder="Subject">
<textarea id="cc-instructions" class="comms-compose-field" rows="4" placeholder="Describe what to say (AI will draft it)"></textarea>
<div id="cc-preview" style="display:none">
<div class="comms-draft-label">DRAFTED MESSAGE</div>
<div class="comms-draft" id="cc-preview-body" style="max-height:200px"></div>
</div>
<div class="comms-compose-actions">
<button class="comms-send-btn" style="flex:1" onclick="commsComposeDraft()">◈ DRAFT</button>
<button class="comms-send-btn" style="flex:1;display:none" id="cc-send-btn" onclick="commsComposeAndSend()">◈ SEND NOW</button>
<button onclick="document.getElementById('comms-compose-modal').remove()" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">CANCEL</button>
</div>
<div id="cc-status" style="font-family:var(--font-mono);font-size:0.55rem;color:var(--cyan);margin-top:6px;min-height:14px"></div>
</div>`;
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 = '<div class="comms-empty" style="padding:10px">No sent messages yet</div>';
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 += `<div class="comms-outbox-card">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="comms-outbox-to">TO: ${escHtml((m.to_email||'').substring(0,40))}</div>
<span class="comms-outbox-status ${sc}">${sc.toUpperCase()}</span>
</div>
<div class="comms-outbox-subj">${escHtml((m.subject||'(no subject)').substring(0,60))}</div>
<div style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">${ts} · ${m.account||'gmail'}</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="comms-empty" style="padding:10px">OUTBOX OFFLINE</div>';
}
}
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 = `<div style="padding:6px 10px 4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span style="font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--cyan)">◈ GUARDIAN MODE</span>
<span style="font-family:var(--font-mono);font-size:0.5rem;color:${status.enabled?'var(--green)':'var(--red)'}">
${status.enabled ? '● ACTIVE' : '○ INACTIVE'}
</span>
<span style="font-family:var(--font-mono);font-size:0.5rem;color:var(--text-dim)">SCAN: ${lastScan}</span>
${unread ? `<button onclick="guardianAckAll()" class="guardian-ack-btn" style="margin-left:auto">ACK ALL (${unread})</button>` : '<span style="margin-left:auto"></span>'}
<button onclick="guardianSitrep()" style="background:rgba(0,212,255,0.08);border:1px solid var(--panel-border);color:var(--cyan);padding:3px 7px;border-radius:3px;font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SITREP</button>
</div>`;
if (!events.length) {
html += '<div style="text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px">◈ ALL CLEAR<br><span style="opacity:0.5">Guardian is watching...</span></div>';
} 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 += `<div class="guardian-event ${sev}${acked?' acked':''}" id="gev-${ev.id}">
<span class="guardian-sev ${sev}">${sev.toUpperCase()}</span>
<div style="flex:1">
<div class="guardian-msg">${typeIco} ${escHtml(ev.message||'')}</div>
${ev.ai_analysis ? `<div class="guardian-ai">${escHtml(ev.ai_analysis.substring(0,200))}</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">
<span class="guardian-time">${ts}</span>
${!acked ? `<button class="guardian-ack-btn" onclick="guardianAck(${ev.id})">ACK</button>` : ''}
</div>
</div>`;
}
}
el.innerHTML = html;
startGuardianPolling();
} catch(e) {
if (el) el.innerHTML = '<div style="text-align:center;padding:20px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim)">GUARDIAN OFFLINE</div>';
}
}
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) {}
}