// ── 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 JOBSSay "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 = '⚡ NEW RESEARCH ';
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 = '◈ TRIAGE INBOX NOW '
+ '◈ NO TRIAGE DATASay "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 += '◈ TRIAGE INBOX ';
html += '+ COMPOSE ';
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 ? `◈ SEND REPLY ` : ''}
${hasReply ? `COPY ` : ''}
DISMISS
`;
}
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);
const lastScan = status.last_scan
? new Date(status.last_scan + 'Z').toLocaleTimeString()
: '—';
let html = `
◈ GUARDIAN MODE
${status.enabled ? '● ACTIVE' : '○ INACTIVE'}
SCAN: ${lastScan}
${unread ? `ACK ALL (${unread}) ` : ' '}
◈ SITREP
`;
if (!events.length) {
html += '◈ ALL CLEARGuardian 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 ? `ACK ` : ''}
`;
}
}
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) {}
}
// ── MISSION OPS HUD ───────────────────────────────────────────────────────────
let _missionsOpenCards = new Set();
async function loadMissionsHud() {
const el = document.getElementById('missions-hud');
if (!el) return;
try {
const missions = await api('arc?action=missions');
const list = Array.isArray(missions) ? missions : [];
let html = '◈ MANAGE MISSIONS IN ADMIN ';
if (!list.length) {
html += '◈ NO MISSIONSCreate workflows in Admin → Mission Ops
';
el.innerHTML = html;
return;
}
const trigIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'};
for (const m of list) {
const isOpen = _missionsOpenCards.has(m.id);
const icon = trigIcons[m.trigger_type] || '◈';
const enabled = m.enabled;
const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleTimeString() : 'never';
html += `
${icon}
${escHtml(m.name)}
${m.trigger_type.replace('_',' ').toUpperCase()}
${m.run_count||0} runs
${m.description ? `
${escHtml(m.description)}
` : ''}
Last run: ${lastRun} · ${m.run_count||0} total runs
▶ RUN NOW
`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = 'MISSIONS OFFLINE
';
}
}
function toggleMissionCard(id) {
const card = document.getElementById('mission-card-' + id);
if (!card) return;
if (_missionsOpenCards.has(id)) _missionsOpenCards.delete(id);
else _missionsOpenCards.add(id);
card.classList.toggle('open');
}
async function hudRunMission(id) {
const btn = document.getElementById('mission-run-btn-' + id);
const res = document.getElementById('mission-run-result-' + id);
if (btn) { btn.disabled = true; btn.textContent = '◈ RUNNING…'; }
if (res) res.textContent = '';
try {
const data = await api('arc?action=mission_run&id=' + id, 'POST', {trigger_source: 'hud'});
const s = data.status || 'done';
const color = s === 'done' ? '#00ff88' : s === 'failed' ? '#ff2244' : '#ffd700';
if (res) res.style.color = color;
if (res) res.textContent = `◈ ${s.toUpperCase()} — Run #${data.run_id||'?'} · ${data.steps||0} steps completed`;
if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; }
setTimeout(loadMissionsHud, 2000);
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; }
if (res) res.textContent = '✗ Run failed';
}
}
// ── DIRECTIVES HUD ────────────────────────────────────────────────────────────
let _dirOpenCards = new Set();
async function loadDirectivesHud() {
const el = document.getElementById('directives-hud');
if (!el) return;
try {
const d = await api('directives/list?status=active');
const list = (d.directives || []);
let html = '◈ MANAGE IN ADMIN ';
if (!list.length) {
html += '◈ NO ACTIVE DIRECTIVESCreate objectives in Admin → Directives
';
el.innerHTML = html;
return;
}
const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'};
for (const dir of list) {
const pct = Math.min(100, Math.round(dir.progress || 0));
const isOpen = _dirOpenCards.has(dir.id);
const color = catColors[dir.category] || 'var(--cyan)';
const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644';
const daysLeft = dir.target_date
? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null;
const dueTxt = daysLeft !== null
? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`)
: '';
const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)';
html += `
${dir.category.toUpperCase()}
${escHtml(dir.title)}
${pct}%
${dueTxt ? `${dueTxt} ` : ''}
${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS
◈ AI REVIEW
`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = 'DIRECTIVES OFFLINE
';
}
}
function toggleDirCard(id) {
const card = document.getElementById('dir-card-' + id);
if (!card) return;
if (_dirOpenCards.has(id)) _dirOpenCards.delete(id);
else _dirOpenCards.add(id);
card.classList.toggle('open');
}
async function hudDirectiveReview(id) {
const res = await api('arc?action=job_create', 'POST', {
type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6,
});
if (res.job_id) {
addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`);
speak(`Directive review underway. I'll brief you on your progress in a moment.`);
}
}
// ── MEMORY CORE — bottom bar count ────────────────────────────────────────────
async function updateMemoryCount() {
try {
const stats = await api('memory?action=stats');
const el = document.getElementById('bb-memory-count');
const dot = document.getElementById('bb-memory-dot');
if (el && stats) {
const total = stats.total || 0;
el.textContent = total + ' FACTS';
if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)';
}
} catch(e) {}
}
// ── CLEARANCE PROTOCOL HUD ─────────────────────────────────────────────────────
const _clrOpenCards = new Set();
async function updateClearanceBanner() {
try {
const pending = await api('arc?action=clearance_pending');
const list = Array.isArray(pending) ? pending : [];
const count = list.length;
const banner = document.getElementById('clearance-banner');
const badge = document.getElementById('clr-tab-badge');
const bcount = document.getElementById('clr-banner-count');
if (banner) {
if (count > 0) {
banner.classList.add('active');
if (bcount) bcount.textContent = count;
} else {
banner.classList.remove('active');
}
}
if (badge) {
if (count > 0) { badge.style.display = 'inline'; badge.textContent = count; }
else badge.style.display = 'none';
}
} catch(e) {}
}
async function loadClearanceHud() {
const el = document.getElementById('clearance-hud');
if (!el) return;
try {
const [pendingRes, rulesRes, historyRes] = await Promise.all([
api('arc?action=clearance_pending'),
api('arc?action=clearance_rules'),
api('arc?action=clearance_history&limit=20')
]);
const pending = Array.isArray(pendingRes) ? pendingRes : [];
const rules = Array.isArray(rulesRes) ? rulesRes : [];
const history = Array.isArray(historyRes) ? historyRes : [];
let html = '◈ MANAGE CLEARANCE RULES IN ADMIN ';
// Pending requests
html += `PENDING AUTHORIZATION (${pending.length})
`;
if (!pending.length) {
html += '◈ NO PENDING CLEARANCE REQUESTS
';
} else {
for (const cr of pending) {
const isOpen = _clrOpenCards.has(cr.id);
const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload || '{}') : (cr.job_payload || {});
const created = cr.created_at ? new Date(cr.created_at).toLocaleString() : '';
const expires = cr.expires_at ? new Date(cr.expires_at).toLocaleString() : '';
html += `
${escHtml(cr.job_type.toUpperCase().replace(/_/g,' '))}
${cr.risk_level.toUpperCase()}
#${cr.id}
${escHtml(cr.description || 'No description')}
Requested: ${created}${expires ? ' · Expires: ' + expires : ''}
Payload: ${escHtml(JSON.stringify(pl))}
◈ AUTHORIZE
✕ DENY
`;
}
}
// Rules
html += `CLEARANCE RULES
`;
if (!rules.length) {
html += 'No rules configured
';
} else {
html += '';
for (const r of rules) {
const enClass = r.enabled ? 'clr-rule-enabled' : 'clr-rule-disabled';
const enLabel = r.enabled ? 'ON' : 'OFF';
const reqLabel = r.require_approval ? 'REQUIRES APPROVAL' : 'AUTO-ALLOW';
const autoTxt = r.auto_approve_after_min ? ` · AUTO ${r.auto_approve_after_min}m` : '';
html += `
${r.job_type.replace(/_/g,' ').toUpperCase()}
${r.risk_level.toUpperCase()}
${reqLabel}${autoTxt}
${enLabel}
`;
}
html += '
';
}
// Recent history
html += `RECENT HISTORY
`;
const recentDecided = history.filter(h => h.status !== 'pending').slice(0, 10);
if (!recentDecided.length) {
html += 'No history yet
';
} else {
html += '';
for (const h of recentDecided) {
const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : '';
html += `
◈
${h.job_type.replace(/_/g,' ').toUpperCase()}
${h.status.toUpperCase()}
${ts}
`;
}
html += '
';
}
el.innerHTML = html;
await updateClearanceBanner();
} catch(e) {
if (el) el.innerHTML = 'CLEARANCE SYSTEM OFFLINE
';
}
}
function toggleClrCard(id) {
const card = document.getElementById('clr-card-' + id);
if (!card) return;
if (_clrOpenCards.has(id)) _clrOpenCards.delete(id);
else _clrOpenCards.add(id);
card.classList.toggle('open');
}
async function hudClearanceDecide(id, action) {
const label = action === 'approve' ? 'AUTHORIZE' : 'DENY';
if (!confirm(`${label} clearance request #${id}?`)) return;
const note = action === 'deny' ? (prompt('Reason for denial (optional):') || '') : '';
try {
const res = await api(`arc?action=clearance_${action}&id=${id}`, 'POST', { decided_by: 'admin', note });
const msg = action === 'approve'
? `◈ Clearance #${id} authorized. Job dispatched.`
: `◈ Clearance #${id} denied${note ? ': ' + note : ''}.`;
addMessage('jarvis', msg);
speak(action === 'approve' ? 'Clearance granted. Job dispatched.' : 'Request denied.');
await loadClearanceHud();
} catch(e) {
addMessage('system', 'Clearance action failed.');
}
}
async function hudClearanceRuleToggle(id, newEnabled) {
try {
await api(`arc?action=clearance_rule_update&id=${id}`, 'POST', { enabled: newEnabled });
await loadClearanceHud();
} catch(e) {}
}
async function loadAgents() {
const [listData, metricsData] = await Promise.all([
api('agent/list'),
api('agent/status')
]);
const agents = listData.agents || [];
const metrics = metricsData.metrics || {};
// Fetch sparkline data (non-blocking)
api('metrics').then(d => { _sparkData = d || {}; renderAgentsTab(agents, metrics); }).catch(() => {});
renderAgentsTab(agents, metrics);
}
async function addNetworkDevice() {
const ip = prompt('IP address (e.g. 10.48.200.43):');
if (!ip) return;
const name = prompt('Device name (e.g. Yealink Phone):');
if (!name) return;
const type = prompt('Type (server, voip, nas, printer, device):', 'device') || 'device';
const r = await api('network/add', 'POST', {ip, alias: name, type});
if (r.error) { alert('Error: ' + r.error); return; }
loadNetwork();
}
async function deleteNetworkDevice(ip, evt) {
evt.stopPropagation();
if (!confirm('Remove ' + ip + ' from the network list?')) return;
const r = await api('network/delete', 'POST', {ip});
if (r.error) { alert('Error: ' + r.error); return; }
loadNetwork();
}
let _agentSparkData = {};
function sparkline(points, width=80, height=20, color='var(--cyan)') {
if (!points || points.length < 2) return '';
const max = Math.max(...points, 1);
const min = Math.min(...points);
const range = max - min || 1;
const step = width / (points.length - 1);
const pts = points.map((v, i) => {
const x = i * step;
const y = height - ((v - min) / range) * (height - 2) - 1;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
return `
`;
}
function renderAgentsTab(agents, metrics) {
const el = document.getElementById('agents-list');
if (!el) return;
if (!agents.length) {
el.innerHTML = 'NO AGENTS REGISTERED
';
return;
}
el.innerHTML = agents.map(ag => {
const m = metrics[ag.agent_id] || {};
const sys = m.system || {};
const alive = ag.status === 'online';
const cpu = sys.cpu_percent != null ? Math.round(sys.cpu_percent) : '--';
const mem = sys.memory ? Math.round(sys.memory.percent) : '--';
const memUsed = sys.memory ? Math.round(sys.memory.used_mb / 1024 * 10) / 10 + 'GB' : '--';
const memTot = sys.memory ? Math.round(sys.memory.total_mb / 1024 * 10) / 10 + 'GB' : '--';
const disks = sys.disk || [];
const maxDisk = disks.length ? Math.max(...disks.map(d => parseInt(d.percent)||0)) : null;
const uptime = sys.uptime ? sys.uptime.human : (alive ? 'ONLINE' : 'OFFLINE');
const since = ag.last_seen ? ag.last_seen.replace('T',' ').replace(/\.\d+Z$/,'') : '--';
const gauge = (val, unit='%', warn=80, crit=90) => {
const v = typeof val === 'number' ? val : parseInt(val);
if (isNaN(v)) return `-- `;
const col = v >= crit ? 'var(--red)' : v >= warn ? '#f5a623' : 'var(--green)';
return ``;
};
const svcs = (sys.services || []).filter(s => s.status !== 'inactive' || true)
.map(s => `${s.service}: ${s.status} `)
.join('');
const ctxKey = 'agent_' + ag.agent_id;
_panelCtx[ctxKey] = {type:'agent', label: ag.hostname, agent_id: ag.agent_id,
hostname: ag.hostname, status: ag.status, cpu, mem};
return `
${ag.hostname}
${ag.agent_type.toUpperCase()} · ${ag.ip_address}
${alive ? 'ONLINE' : 'OFFLINE'}
${alive ? `
MEM ${memUsed}/${memTot}
${gauge(mem)}
DISK
${maxDisk != null ? gauge(maxDisk) : '
-- '}
CPU 2H
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.cpu), 100, 18, 'rgba(0,212,255,0.7)')}
MEM 2H
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.mem), 100, 18, 'rgba(0,255,136,0.7)')}
` : ''}
UP: ${uptime} · SEEN: ${since}
${svcs ? `
${svcs}
` : ''}
${alive ? `
◈ SCREENSHOT
⚡ SYSINFO
` : ''}
`;
}).join('');
}
function openAgentModal() {
const os = detectOS();
const title = document.getElementById('agentModalTitle');
const content = document.getElementById('agentModalContent');
const modal = document.getElementById('agentModal');
const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518';
const baseUrl = 'https://jarvis.orbishosting.com/agent';
const jUrl = 'https://jarvis.orbishosting.com';
if (os === 'tablet') {
title.textContent = '● JARVIS — TABLET / MOBILE';
content.innerHTML =
'✓ You\'re viewing JARVIS on a tablet or mobile device.
' +
'The JARVIS Agent runs on desktop and server platforms (Windows, macOS, Linux). ' +
'Tablets and phones can browse the full JARVIS dashboard but do not need an agent installed — all data comes from your other monitored machines.
';
} else if (_agentOnline) {
title.textContent = '● AGENT CONNECTED';
content.innerHTML =
'✓ JARVIS Agent is active on this machine.
' +
'' +
'Host: ' + (_myAgent?.hostname||'—') + ' ' +
'IP: ' + (_myAgent?.ip_address||'—') + ' ' +
'Type: ' + (_myAgent?.agent_type||'—').toUpperCase() + ' ' +
'Reporting: CPU · Memory · Disk · Services · Uptime
';
} else {
const inst = {
windows: {
label:'Windows',
cmd:'# Run PowerShell as Administrator:\nSet-ExecutionPolicy Bypass -Scope Process -Force\nInvoke-WebRequest -Uri "'+baseUrl+'/install-windows.ps1" -OutFile "$env:TEMP\\install.ps1"\n& "$env:TEMP\\install.ps1" -JarvisUrl '+jUrl+' -Key '+regKey,
dl: baseUrl+'/install-windows.ps1',
note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.'
},
mac: {
label:'macOS',
cmd:'bash <(curl -sSL '+baseUrl+'/install-mac.sh) \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey,
dl: baseUrl+'/install-mac.sh',
note:'Run in Terminal. Installs as a launchd background service.'
},
linux: {
label:'Linux',
cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey,
dl: baseUrl+'/install.sh',
note:'Run in terminal. Installs as a systemd service.'
},
unknown: {
label:'Your System',
cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/',
dl: 'https://jarvis.orbishosting.com/agent/',
note:'Choose your platform installer from the JARVIS agent directory.'
}
};
const i = inst[os] || inst.unknown;
const osBadge = {windows:'🪟 WINDOWS', mac:'🍎 MACOS', linux:'🐧 LINUX', unknown:'❓ UNKNOWN'}[os] || os.toUpperCase();
title.textContent = '● INSTALL AGENT · ' + (inst[os] ? inst[os].label.toUpperCase() : 'YOUR SYSTEM');
content.innerHTML =
'DETECTED: ' + osBadge + '
' +
''+i.note+'
' +
''+i.cmd+' ' +
'↓ DOWNLOAD INSTALLER ' +
'After install, the AGENT indicator turns green within 30 seconds.
';
}
modal.classList.add('open');
}
document.addEventListener('click', function(e) {
if (e.target === document.getElementById('agentModal'))
document.getElementById('agentModal').classList.remove('open');
});
// ── SITES MANAGER ────────────────────────────────────────────────────
let sitesData = {};
function openSitesModal() {
document.getElementById('sitesModal').style.display = 'flex';
loadSites();
}
function closeSitesModal() {
document.getElementById('sitesModal').style.display = 'none';
}
// Close on backdrop click
document.getElementById('sitesModal').addEventListener('click', function(e) {
if (e.target === this) closeSitesModal();
});
async function loadSites() {
document.getElementById('sites-grid').innerHTML = 'LOADING SITE SETTINGS...
';
const res = await api('sites');
if (!res.success) {
document.getElementById('sites-grid').innerHTML = 'FAILED TO LOAD SETTINGS
';
return;
}
sitesData = res.sites;
// Pre-fill global key from first site
const firstKey = Object.values(res.sites)[0]?.api_key || '';
document.getElementById('global-api-key').value = firstKey;
renderSiteCards();
}
function renderSiteCards() {
const grid = document.getElementById('sites-grid');
let html = '';
for (const [id, s] of Object.entries(sitesData)) {
html += `
${s.name.toUpperCase()}
${s.url}
SAVE
`;
}
grid.innerHTML = html;
}
async function pushApiKey() {
const key = document.getElementById('global-api-key').value.trim();
const status = document.getElementById('push-status');
if (!key) { status.style.color='#f44'; status.textContent='✗ API KEY REQUIRED'; return; }
status.style.color='var(--text-dim)'; status.textContent='PUSHING TO ALL SITES...';
const res = await api('sites', 'POST', {action:'push_key', api_key:key});
if (res.success) {
const ok = Object.values(res.results).filter(Boolean).length;
const total = Object.keys(res.results).length;
status.style.color = ok === total ? 'var(--cyan)' : '#fa0';
status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`;
for (const id of Object.keys(sitesData)) sitesData[id].api_key = key;
} else {
status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
}
}
async function saveSite(id) {
const status = document.getElementById(id + '-status');
status.style.color='var(--text-dim)'; status.textContent='SAVING...';
const res = await api('sites', 'POST', {
action: 'save',
site: id,
from_email: document.getElementById(id+'-from_email').value.trim(),
from_name: document.getElementById(id+'-from_name').value.trim(),
admin_email: document.getElementById(id+'-admin_email').value.trim(),
});
if (res.success) {
status.style.color='var(--cyan)'; status.textContent='✓ SAVED';
setTimeout(() => { status.textContent=''; }, 3000);
} else {
status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
}
}
// ── VISION PROTOCOL — screenshot lightbox ────────────────────────────────────
function openVisionLightbox(title) {
const lb = document.getElementById('vision-lightbox');
document.getElementById('vision-lb-title').textContent = title || '◈ VISION PROTOCOL';
document.getElementById('vision-lb-img').style.display = 'none';
document.getElementById('vision-lb-img').src = '';
document.getElementById('vision-lb-analysis').textContent = '';
document.getElementById('vision-lb-spinner').style.display = 'block';
lb.classList.add('open');
}
function closeVisionLightbox() {
document.getElementById('vision-lightbox').classList.remove('open');
}
async function agentScreenshot(hostname) {
openVisionLightbox('◈ VISION PROTOCOL — ' + hostname.toUpperCase());
const arcRes = await api('arc?action=job_create', 'POST', {
type: 'screenshot',
payload: {agent: hostname, analyze: true},
priority: 8,
}).catch(() => null);
if (!arcRes || !arcRes.job_id) {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Failed to submit screenshot job — Arc Reactor may be offline.';
return;
}
// Poll for result
const jobId = arcRes.job_id;
let tries = 0;
const poll = async () => {
tries++;
const job = await api('arc?action=job_get&id=' + jobId).catch(() => null);
if (job && job.status === 'done') {
const r = job.result || {};
document.getElementById('vision-lb-spinner').style.display = 'none';
if (r.has_image && r.screenshot_id) {
// Fetch full screenshot with image
const full = await api('arc?action=screenshot_get&id=' + r.screenshot_id).catch(() => null);
if (full && full.image_b64) {
const img = document.getElementById('vision-lb-img');
img.src = 'data:image/png;base64,' + full.image_b64;
img.style.display = 'block';
}
}
document.getElementById('vision-lb-analysis').textContent =
r.analysis || (r.has_image ? 'Screenshot captured — no analysis available.' : JSON.stringify(r.snapshot || r, null, 2));
} else if (job && job.status === 'failed') {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Screenshot failed: ' + (job.error || 'Unknown error');
} else if (tries < 30) {
setTimeout(poll, 2000);
} else {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Timed out waiting for screenshot.';
}
};
setTimeout(poll, 2000);
}
async function agentSysinfo(hostname) {
openVisionLightbox('⚡ FIELD SYSINFO — ' + hostname.toUpperCase());
const arcRes = await api('arc?action=job_create', 'POST', {
type: 'sysinfo',
payload: {agent: hostname, analyze: true},
priority: 7,
}).catch(() => null);
if (!arcRes || !arcRes.job_id) {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Failed to submit sysinfo job.';
return;
}
const jobId = arcRes.job_id;
let tries = 0;
const poll = async () => {
tries++;
const job = await api('arc?action=job_get&id=' + jobId).catch(() => null);
if (job && job.status === 'done') {
const r = job.result || {};
document.getElementById('vision-lb-spinner').style.display = 'none';
const snap = r.snapshot || {};
const snapText = Object.entries(snap)
.filter(([k]) => !['success','screenshot_available','snapshot_type'].includes(k))
.map(([k,v]) => `${k.toUpperCase().replace(/_/g,' ')}: ${Array.isArray(v) ? v.join('\n ') : v}`)
.join('\n');
document.getElementById('vision-lb-analysis').textContent =
(r.analysis ? r.analysis + '\n\n─────────────────────\n\n' : '') + (snapText || JSON.stringify(r, null, 2));
} else if (job && job.status === 'failed') {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Sysinfo failed: ' + (job.error || 'Unknown error');
} else if (tries < 20) {
setTimeout(poll, 2000);
} else {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Timed out.';
}
};
setTimeout(poll, 2000);
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeVisionLightbox();
});
// ── CHAT HISTORY SEARCH ───────────────────────────────────────────────────────
function openSearchModal() {
document.getElementById('searchModal').style.display = 'flex';
document.getElementById('searchInput').focus();
}
function closeSearchModal() {
document.getElementById('searchModal').style.display = 'none';
document.getElementById('searchResults').innerHTML = 'Type to search your JARVIS conversations
';
document.getElementById('searchInput').value = '';
}
async function runSearch() {
const q = document.getElementById('searchInput').value.trim();
if (!q) return;
const el = document.getElementById('searchResults');
el.innerHTML = 'Searching...
';
try {
const d = await api('history?q=' + encodeURIComponent(q));
if (!d.results || !d.results.length) {
el.innerHTML = 'No results for "' + q + '"
';
return;
}
el.innerHTML = d.results.map(r => {
const role = r.role === 'user' ? '👤' : '🤖';
const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content;
return `
${role} ${r.role.toUpperCase()}
${ts}
${snippet.replace(/
`;
}).join('');
} catch(e) {
el.innerHTML = '
Search failed
';
}
}
document.getElementById('searchModal')?.addEventListener('click', e => {
if (e.target === document.getElementById('searchModal')) closeSearchModal();
});
// ── PROACTIVE SUGGESTIONS ────────────────────────────────────────────────────
const _shownSuggestions = new Set();
async function checkSuggestions() {
const d = await api('suggestions').catch(() => null);
if (!d || !d.suggestions || !d.suggestions.length) return;
for (const s of d.suggestions) {
const key = s.intent + ':' + d.hour + ':' + d.dow;
if (_shownSuggestions.has(key)) continue;
_shownSuggestions.add(key);
// Show as a soft suggestion chip in chat
const log = document.getElementById('chatLog');
const chip = document.createElement('div');
chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0';
chip.innerHTML = `
◈ ${s.prompt} `;
log.appendChild(chip);
log.scrollTop = log.scrollHeight;
break; // show max one suggestion at a time
}
}
function sendSuggestion(intent, btn) {
btn.closest('div').remove();
const prompts = {
'network_scan': 'run a network scan',
'jellyfin_now_playing': 'what is playing on Jellyfin',
'ha_scene': 'what scenes are available',
'planner:briefing': 'daily briefing',
'vm_suggestions': 'VM resource suggestions',
'focus_mode': 'focus mode',
};
const msg = prompts[intent] || intent.replace(/_/g,' ');
document.getElementById('textInput').value = msg;
sendMessage();
}
// ── MOBILE PANEL SWITCHER ─────────────────────────────────────────────────────
function mobSwitch(which) {
if (window.innerWidth > 900) return;
const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'};
Object.entries(panels).forEach(([k, id]) => {
document.getElementById(id)?.classList.toggle('mob-active', k === which);
});
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('mob-btn-' + which)?.classList.add('active');
if (which === 'right') loadNews();
}
function initMobile() {
if (window.innerWidth > 900) return;
['leftPanel','centerPanel','rightPanel'].forEach(id =>
document.getElementById(id)?.classList.remove('mob-active'));
document.getElementById('leftPanel')?.classList.add('mob-active');
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('mob-btn-left')?.classList.add('active');
}
window.addEventListener('resize', initMobile);