From 0b7f2d013b921582a81f1085f78b0c289030ac21 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Wed, 17 Jun 2026 19:10:31 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=203=20=E2=80=94=20split=20jar?= =?UTF-8?q?vis-protocols.js=20into=203=20panel=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single SyntaxError in the 1668-line monolith kills every panel (proven by the apostrophe bug on 2026-06-17). Split into: panels/jarvis-arc.js (608 lines) — Arc Reactor, Intel, Comms, Guardian panels/jarvis-agents.js (715 lines) — Missions, Directives, Memory, Clearance, Agents tab, Sites, Vision panels/jarvis-assistant.js (345 lines) — Chat History, Suggestions, Mobile, Command Palette, Topo map A parse error in any one file now fails only that group of panels. escHtml() stays in jarvis-arc.js (loads first) and remains global. All other dependencies (api, speak, addMessage) come from jarvis-app.js. Version param bumped to ?v=20260617b to force Cloudflare cache miss. Co-Authored-By: Claude Sonnet 4.6 --- public_html/assets/js/jarvis-protocols.js | 1668 ----------------- public_html/assets/js/panels/jarvis-agents.js | 715 +++++++ public_html/assets/js/panels/jarvis-arc.js | 608 ++++++ .../assets/js/panels/jarvis-assistant.js | 345 ++++ public_html/index.html | 10 +- 5 files changed, 1674 insertions(+), 1672 deletions(-) delete mode 100644 public_html/assets/js/jarvis-protocols.js create mode 100644 public_html/assets/js/panels/jarvis-agents.js create mode 100644 public_html/assets/js/panels/jarvis-arc.js create mode 100644 public_html/assets/js/panels/jarvis-assistant.js diff --git a/public_html/assets/js/jarvis-protocols.js b/public_html/assets/js/jarvis-protocols.js deleted file mode 100644 index 0ab625e..0000000 --- a/public_html/assets/js/jarvis-protocols.js +++ /dev/null @@ -1,1668 +0,0 @@ -// ── 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) {} -} - -// ── 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 = ''; - - if (!list.length) { - html += '
◈ NO MISSIONS
Create 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
-
- -
-
-
-
`; - } - 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 = ''; - - if (!list.length) { - html += '
◈ NO ACTIVE DIRECTIVES
Create 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
- -
-
`; - } - 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 = ''; - - // 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))} -
-
- - -
-
-
`; - } - } - - // 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} - -
`; - } - 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 `
-
-
-
- ${v}${unit} -
`; - }; - - 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 ? `
-
CPU
${gauge(cpu)}
-
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 ? `
- - -
` : ''} -
`; - }).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}
-
-
-
FROM EMAIL
- -
-
-
FROM NAME
- -
-
-
ADMIN NOTIFICATION EMAIL
- -
-
- - -
-
`; - } - 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 = ``; - 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); - -// ── COMMAND PALETTE (Ctrl+K) ────────────────────────────────────────────── -const _PALETTE_COMMANDS = [ - { label: 'Run a network scan', q: 'run a network scan', group: 'Network' }, - { label: 'Show online devices', q: 'who is online on the network', group: 'Network' }, - { label: 'Proxmox status', q: 'proxmox status', group: 'Network' }, - { label: 'Check agent status', q: 'check all agents', group: 'Agents' }, - { label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' }, - { label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' }, - { label: 'Daily briefing', q: 'daily briefing', group: 'Planner' }, - { label: 'My tasks today', q: 'my tasks today', group: 'Planner' }, - { label: 'My calendar', q: 'my calendar', group: 'Planner' }, - { label: "What's playing on Jellyfin", q: 'what is playing on Jellyfin', group: 'Media' }, - { label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' }, - { label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' }, - { label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' }, - { label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'}, - { label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'}, - { label: 'Focus mode', q: 'focus mode', group: 'UI' }, - { label: 'Show all panels', q: 'show all panels', group: 'UI' }, - { label: 'Check alerts', q: 'check alerts', group: 'System' }, - { label: 'Site health', q: 'site health', group: 'System' }, - { label: 'System status', q: 'system status', group: 'System' }, - { label: 'Check inbox', q: 'check inbox', group: 'Comms' }, - { label: 'Search history…', q: '', group: 'Chat', search: true }, -]; - -let _paletteOpen = false; - -function openPalette() { - if (_paletteOpen) return; - _paletteOpen = true; - const ov = document.getElementById('cmdPalette'); - if (!ov) return; - ov.style.display = 'flex'; - const inp = document.getElementById('cmdPaletteInput'); - inp.value = ''; - renderPaletteItems(''); - requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); }); -} - -function closePalette() { - if (!_paletteOpen) return; - _paletteOpen = false; - const ov = document.getElementById('cmdPalette'); - if (!ov) return; - ov.classList.remove('open'); - setTimeout(() => { ov.style.display = 'none'; }, 180); -} - -function renderPaletteItems(q) { - const list = document.getElementById('cmdPaletteList'); - if (!list) return; - const low = q.toLowerCase().trim(); - const filtered = low - ? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low)) - : _PALETTE_COMMANDS; - - let currentGroup = null; - list.innerHTML = ''; - filtered.forEach((cmd, i) => { - if (cmd.group !== currentGroup) { - currentGroup = cmd.group; - const g = document.createElement('div'); - g.className = 'cp-group'; - g.textContent = cmd.group; - list.appendChild(g); - } - const row = document.createElement('div'); - row.className = 'cp-item' + (i === 0 ? ' cp-active' : ''); - row.dataset.q = cmd.q; - row.dataset.search = cmd.search ? '1' : ''; - const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), - m => `${m}`); - row.innerHTML = `${lbl}`; - row.addEventListener('click', () => firePaletteItem(row)); - list.appendChild(row); - }); -} - -function movePaletteSelection(dir) { - const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item')); - if (!items.length) return; - const cur = items.findIndex(el => el.classList.contains('cp-active')); - const next = (cur + dir + items.length) % items.length; - items.forEach(el => el.classList.remove('cp-active')); - items[next].classList.add('cp-active'); - items[next].scrollIntoView({ block: 'nearest' }); -} - -function firePaletteItem(el) { - if (!el) { - const active = document.querySelector('#cmdPaletteList .cp-active'); - if (!active) return; - el = active; - } - const q = el.dataset.q; - const isSearch = el.dataset.search === '1'; - closePalette(); - if (isSearch) { - if (typeof openSearchModal === 'function') openSearchModal(); - return; - } - if (q) { - document.getElementById('textInput').value = q; - sendMessage(); - } -} - -// Keyboard events -document.addEventListener('keydown', e => { - if ((e.ctrlKey || e.metaKey) && e.key === 'k') { - e.preventDefault(); - _paletteOpen ? closePalette() : openPalette(); - return; - } - if (!_paletteOpen) return; - if (e.key === 'Escape') { e.preventDefault(); closePalette(); } - if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); } - if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); } - if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); } -}); - -// Filter on type -document.getElementById('cmdPaletteInput')?.addEventListener('input', e => { - renderPaletteItems(e.target.value); -}); - -// Close on backdrop click -document.getElementById('cmdPalette')?.addEventListener('click', e => { - if (e.target.id === 'cmdPalette') closePalette(); -}); - -// ── AGENT TOPOLOGY MAP ───────────────────────────────────────────────────────────── -let _agentTopoMode = false, _agentTopoRaf = null, _agentTopoData = []; - -function toggleAgentTopo() { - _agentTopoMode = !_agentTopoMode; - const btn = document.getElementById('agent-topo-btn'); - const list = document.getElementById('agents-list'); - const cvs = document.getElementById('agentTopoCanvas'); - if (!btn || !list || !cvs) return; - btn.classList.toggle('active', _agentTopoMode); - if (_agentTopoMode) { - list.style.display = 'none'; cvs.style.display = 'block'; - _buildAgentTopoData(); _drawAgentTopo(); - } else { - list.style.display = 'block'; cvs.style.display = 'none'; - if (_agentTopoRaf) { cancelAnimationFrame(_agentTopoRaf); _agentTopoRaf = null; } - } -} - -function _buildAgentTopoData() { - // Build node list from rendered agent cards - _agentTopoData = [{id:'jarvis',label:'JARVIS',online:true,type:'hub'}]; - document.querySelectorAll('.agent-card').forEach(el => { - const nameEl = el.querySelector('.agent-name, [class*="name"]'); - if (!nameEl) return; - const name = nameEl.textContent.trim(); - const online = el.classList.contains('online') || !!el.querySelector('.agent-dot.online, .dot.online'); - const lname = name.toLowerCase(); - let type = 'linux'; - if (lname.includes('pve') || lname.includes('proxmox') || el.querySelector('[class*="proxmox"]')) type = 'proxmox'; - else if (lname.includes('ha') || lname.includes('homeassist')) type = 'homeassistant'; - else if (lname.includes('windows') || lname.includes('mini')) type = 'windows'; - _agentTopoData.push({id:name, label:name.substring(0,12), online, type}); - }); - // Fallback: use last known registered agent list if cards not rendered - if (_agentTopoData.length <= 1 && typeof _lastAgents !== 'undefined') { - (_lastAgents || []).forEach(a => { - _agentTopoData.push({id:a.agent_id,label:(a.hostname||a.agent_id).substring(0,12),online:a.status==='online',type:a.agent_type||'linux'}); - }); - } -} - -function _drawAgentTopo() { - const cvs = document.getElementById('agentTopoCanvas'); - if (!cvs || !_agentTopoMode) return; - const ctx = cvs.getContext('2d'); - const rect = cvs.getBoundingClientRect(); - const W = rect.width || 280, H = rect.height || 260; - const dpr = window.devicePixelRatio || 1; - cvs.width = W * dpr; cvs.height = H * dpr; - ctx.scale(dpr, dpr); - const typeRing = {hub:0, proxmox:0.28, homeassistant:0.48, linux:0.68, windows:0.68}; - const typeColor = {hub:'0,212,255', proxmox:'0,255,136', homeassistant:'255,215,0', linux:'0,190,255', windows:'180,120,255'}; - // Assign positions - const byType = {}; - _agentTopoData.slice(1).forEach(n => { (byType[n.type]=byType[n.type]||[]).push(n); }); - _agentTopoData[0].x = W/2; _agentTopoData[0].y = H/2; - Object.entries(byType).forEach(([tp, nodes]) => { - const rf = typeRing[tp] || 0.68; - const r = Math.min(W, H) / 2 * rf; - nodes.forEach((n, i) => { - const a = -Math.PI/2 + (i / nodes.length) * Math.PI * 2; - n.x = W/2 + Math.cos(a)*r; n.y = H/2 + Math.sin(a)*r; - }); - }); - let t = 0; - function frame() { - if (!_agentTopoMode) return; - t += 0.007; ctx.clearRect(0, 0, W, H); - // Orbit rings - [0.28, 0.48, 0.68].forEach(rf => { - ctx.beginPath(); ctx.arc(W/2, H/2, Math.min(W,H)/2*rf, 0, Math.PI*2); - ctx.strokeStyle = 'rgba(0,212,255,0.05)'; ctx.lineWidth = 0.5; ctx.stroke(); - }); - // Edges - _agentTopoData.slice(1).forEach(n => { - if (!n.x) return; - const col = typeColor[n.type] || '0,190,255'; - ctx.beginPath(); ctx.moveTo(W/2, H/2); ctx.lineTo(n.x, n.y); - ctx.strokeStyle = n.online ? 'rgba('+col+',0.18)' : 'rgba(255,50,80,0.08)'; - ctx.lineWidth = n.online ? 1 : 0.5; ctx.stroke(); - }); - // Particles - _agentTopoData.slice(1).filter(n=>n.online&&n.x).forEach((n,i) => { - const p = ((t*0.35+i*0.41)%1); - const col = typeColor[n.type]||'0,190,255'; - const px = W/2+(n.x-W/2)*p, py = H/2+(n.y-H/2)*p; - ctx.beginPath(); ctx.arc(px,py,1.4,0,Math.PI*2); - ctx.fillStyle='rgba('+col+',0.75)'; ctx.fill(); - }); - // Nodes - _agentTopoData.forEach((n,i) => { - if (!n.x) return; - const col = typeColor[n.type]||'0,190,255'; - const nr = n.type==='hub' ? 13 : 7; - const pulse = Math.sin(t+i*0.9)*0.25+0.75; - if (n.online||n.type==='hub') { - const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,nr*3.5); - g.addColorStop(0,'rgba('+col+','+(0.15*pulse)+')'); - g.addColorStop(1,'transparent'); - ctx.beginPath(); ctx.arc(n.x,n.y,nr*3.5,0,Math.PI*2); - ctx.fillStyle=g; ctx.fill(); - } - ctx.beginPath(); ctx.arc(n.x,n.y,nr,0,Math.PI*2); - ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.9)' : 'rgba(255,50,80,0.5)'; - ctx.fill(); - ctx.strokeStyle='rgba('+col+',0.6)'; ctx.lineWidth=1; ctx.stroke(); - ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.85)' : 'rgba(255,80,80,0.7)'; - ctx.font = (n.type==='hub'?'600 8px':'6px')+' "Share Tech Mono",monospace'; - ctx.textAlign='center'; - ctx.fillText(n.label, n.x, n.y+nr+9); - }); - _agentTopoRaf = requestAnimationFrame(frame); - } - frame(); -} diff --git a/public_html/assets/js/panels/jarvis-agents.js b/public_html/assets/js/panels/jarvis-agents.js new file mode 100644 index 0000000..f0bacd7 --- /dev/null +++ b/public_html/assets/js/panels/jarvis-agents.js @@ -0,0 +1,715 @@ +// ── 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 = ''; + + if (!list.length) { + html += '
◈ NO MISSIONS
Create 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
+
+ +
+
+
+
`; + } + 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 = ''; + + if (!list.length) { + html += '
◈ NO ACTIVE DIRECTIVES
Create 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
+ +
+
`; + } + 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 = ''; + + // 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))} +
+
+ + +
+
+
`; + } + } + + // 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} + +
`; + } + 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 `
+
+
+
+ ${v}${unit} +
`; + }; + + 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 ? `
+
CPU
${gauge(cpu)}
+
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 ? `
+ + +
` : ''} +
`; + }).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}
+
+
+
FROM EMAIL
+ +
+
+
FROM NAME
+ +
+
+
ADMIN NOTIFICATION EMAIL
+ +
+
+ + +
+
`; + } + 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(); +}); + diff --git a/public_html/assets/js/panels/jarvis-arc.js b/public_html/assets/js/panels/jarvis-arc.js new file mode 100644 index 0000000..8d9e428 --- /dev/null +++ b/public_html/assets/js/panels/jarvis-arc.js @@ -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 = '
◈ 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) {} +} + diff --git a/public_html/assets/js/panels/jarvis-assistant.js b/public_html/assets/js/panels/jarvis-assistant.js new file mode 100644 index 0000000..79cf66e --- /dev/null +++ b/public_html/assets/js/panels/jarvis-assistant.js @@ -0,0 +1,345 @@ +// ── 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 = ``; + 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); + +// ── COMMAND PALETTE (Ctrl+K) ────────────────────────────────────────────── +const _PALETTE_COMMANDS = [ + { label: 'Run a network scan', q: 'run a network scan', group: 'Network' }, + { label: 'Show online devices', q: 'who is online on the network', group: 'Network' }, + { label: 'Proxmox status', q: 'proxmox status', group: 'Network' }, + { label: 'Check agent status', q: 'check all agents', group: 'Agents' }, + { label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' }, + { label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' }, + { label: 'Daily briefing', q: 'daily briefing', group: 'Planner' }, + { label: 'My tasks today', q: 'my tasks today', group: 'Planner' }, + { label: 'My calendar', q: 'my calendar', group: 'Planner' }, + { label: "What's playing on Jellyfin", q: 'what is playing on Jellyfin', group: 'Media' }, + { label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' }, + { label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' }, + { label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' }, + { label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'}, + { label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'}, + { label: 'Focus mode', q: 'focus mode', group: 'UI' }, + { label: 'Show all panels', q: 'show all panels', group: 'UI' }, + { label: 'Check alerts', q: 'check alerts', group: 'System' }, + { label: 'Site health', q: 'site health', group: 'System' }, + { label: 'System status', q: 'system status', group: 'System' }, + { label: 'Check inbox', q: 'check inbox', group: 'Comms' }, + { label: 'Search history…', q: '', group: 'Chat', search: true }, +]; + +let _paletteOpen = false; + +function openPalette() { + if (_paletteOpen) return; + _paletteOpen = true; + const ov = document.getElementById('cmdPalette'); + if (!ov) return; + ov.style.display = 'flex'; + const inp = document.getElementById('cmdPaletteInput'); + inp.value = ''; + renderPaletteItems(''); + requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); }); +} + +function closePalette() { + if (!_paletteOpen) return; + _paletteOpen = false; + const ov = document.getElementById('cmdPalette'); + if (!ov) return; + ov.classList.remove('open'); + setTimeout(() => { ov.style.display = 'none'; }, 180); +} + +function renderPaletteItems(q) { + const list = document.getElementById('cmdPaletteList'); + if (!list) return; + const low = q.toLowerCase().trim(); + const filtered = low + ? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low)) + : _PALETTE_COMMANDS; + + let currentGroup = null; + list.innerHTML = ''; + filtered.forEach((cmd, i) => { + if (cmd.group !== currentGroup) { + currentGroup = cmd.group; + const g = document.createElement('div'); + g.className = 'cp-group'; + g.textContent = cmd.group; + list.appendChild(g); + } + const row = document.createElement('div'); + row.className = 'cp-item' + (i === 0 ? ' cp-active' : ''); + row.dataset.q = cmd.q; + row.dataset.search = cmd.search ? '1' : ''; + const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), + m => `${m}`); + row.innerHTML = `${lbl}`; + row.addEventListener('click', () => firePaletteItem(row)); + list.appendChild(row); + }); +} + +function movePaletteSelection(dir) { + const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item')); + if (!items.length) return; + const cur = items.findIndex(el => el.classList.contains('cp-active')); + const next = (cur + dir + items.length) % items.length; + items.forEach(el => el.classList.remove('cp-active')); + items[next].classList.add('cp-active'); + items[next].scrollIntoView({ block: 'nearest' }); +} + +function firePaletteItem(el) { + if (!el) { + const active = document.querySelector('#cmdPaletteList .cp-active'); + if (!active) return; + el = active; + } + const q = el.dataset.q; + const isSearch = el.dataset.search === '1'; + closePalette(); + if (isSearch) { + if (typeof openSearchModal === 'function') openSearchModal(); + return; + } + if (q) { + document.getElementById('textInput').value = q; + sendMessage(); + } +} + +// Keyboard events +document.addEventListener('keydown', e => { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + _paletteOpen ? closePalette() : openPalette(); + return; + } + if (!_paletteOpen) return; + if (e.key === 'Escape') { e.preventDefault(); closePalette(); } + if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); } + if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); } + if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); } +}); + +// Filter on type +document.getElementById('cmdPaletteInput')?.addEventListener('input', e => { + renderPaletteItems(e.target.value); +}); + +// Close on backdrop click +document.getElementById('cmdPalette')?.addEventListener('click', e => { + if (e.target.id === 'cmdPalette') closePalette(); +}); + +// ── AGENT TOPOLOGY MAP ───────────────────────────────────────────────────────────── +let _agentTopoMode = false, _agentTopoRaf = null, _agentTopoData = []; + +function toggleAgentTopo() { + _agentTopoMode = !_agentTopoMode; + const btn = document.getElementById('agent-topo-btn'); + const list = document.getElementById('agents-list'); + const cvs = document.getElementById('agentTopoCanvas'); + if (!btn || !list || !cvs) return; + btn.classList.toggle('active', _agentTopoMode); + if (_agentTopoMode) { + list.style.display = 'none'; cvs.style.display = 'block'; + _buildAgentTopoData(); _drawAgentTopo(); + } else { + list.style.display = 'block'; cvs.style.display = 'none'; + if (_agentTopoRaf) { cancelAnimationFrame(_agentTopoRaf); _agentTopoRaf = null; } + } +} + +function _buildAgentTopoData() { + // Build node list from rendered agent cards + _agentTopoData = [{id:'jarvis',label:'JARVIS',online:true,type:'hub'}]; + document.querySelectorAll('.agent-card').forEach(el => { + const nameEl = el.querySelector('.agent-name, [class*="name"]'); + if (!nameEl) return; + const name = nameEl.textContent.trim(); + const online = el.classList.contains('online') || !!el.querySelector('.agent-dot.online, .dot.online'); + const lname = name.toLowerCase(); + let type = 'linux'; + if (lname.includes('pve') || lname.includes('proxmox') || el.querySelector('[class*="proxmox"]')) type = 'proxmox'; + else if (lname.includes('ha') || lname.includes('homeassist')) type = 'homeassistant'; + else if (lname.includes('windows') || lname.includes('mini')) type = 'windows'; + _agentTopoData.push({id:name, label:name.substring(0,12), online, type}); + }); + // Fallback: use last known registered agent list if cards not rendered + if (_agentTopoData.length <= 1 && typeof _lastAgents !== 'undefined') { + (_lastAgents || []).forEach(a => { + _agentTopoData.push({id:a.agent_id,label:(a.hostname||a.agent_id).substring(0,12),online:a.status==='online',type:a.agent_type||'linux'}); + }); + } +} + +function _drawAgentTopo() { + const cvs = document.getElementById('agentTopoCanvas'); + if (!cvs || !_agentTopoMode) return; + const ctx = cvs.getContext('2d'); + const rect = cvs.getBoundingClientRect(); + const W = rect.width || 280, H = rect.height || 260; + const dpr = window.devicePixelRatio || 1; + cvs.width = W * dpr; cvs.height = H * dpr; + ctx.scale(dpr, dpr); + const typeRing = {hub:0, proxmox:0.28, homeassistant:0.48, linux:0.68, windows:0.68}; + const typeColor = {hub:'0,212,255', proxmox:'0,255,136', homeassistant:'255,215,0', linux:'0,190,255', windows:'180,120,255'}; + // Assign positions + const byType = {}; + _agentTopoData.slice(1).forEach(n => { (byType[n.type]=byType[n.type]||[]).push(n); }); + _agentTopoData[0].x = W/2; _agentTopoData[0].y = H/2; + Object.entries(byType).forEach(([tp, nodes]) => { + const rf = typeRing[tp] || 0.68; + const r = Math.min(W, H) / 2 * rf; + nodes.forEach((n, i) => { + const a = -Math.PI/2 + (i / nodes.length) * Math.PI * 2; + n.x = W/2 + Math.cos(a)*r; n.y = H/2 + Math.sin(a)*r; + }); + }); + let t = 0; + function frame() { + if (!_agentTopoMode) return; + t += 0.007; ctx.clearRect(0, 0, W, H); + // Orbit rings + [0.28, 0.48, 0.68].forEach(rf => { + ctx.beginPath(); ctx.arc(W/2, H/2, Math.min(W,H)/2*rf, 0, Math.PI*2); + ctx.strokeStyle = 'rgba(0,212,255,0.05)'; ctx.lineWidth = 0.5; ctx.stroke(); + }); + // Edges + _agentTopoData.slice(1).forEach(n => { + if (!n.x) return; + const col = typeColor[n.type] || '0,190,255'; + ctx.beginPath(); ctx.moveTo(W/2, H/2); ctx.lineTo(n.x, n.y); + ctx.strokeStyle = n.online ? 'rgba('+col+',0.18)' : 'rgba(255,50,80,0.08)'; + ctx.lineWidth = n.online ? 1 : 0.5; ctx.stroke(); + }); + // Particles + _agentTopoData.slice(1).filter(n=>n.online&&n.x).forEach((n,i) => { + const p = ((t*0.35+i*0.41)%1); + const col = typeColor[n.type]||'0,190,255'; + const px = W/2+(n.x-W/2)*p, py = H/2+(n.y-H/2)*p; + ctx.beginPath(); ctx.arc(px,py,1.4,0,Math.PI*2); + ctx.fillStyle='rgba('+col+',0.75)'; ctx.fill(); + }); + // Nodes + _agentTopoData.forEach((n,i) => { + if (!n.x) return; + const col = typeColor[n.type]||'0,190,255'; + const nr = n.type==='hub' ? 13 : 7; + const pulse = Math.sin(t+i*0.9)*0.25+0.75; + if (n.online||n.type==='hub') { + const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,nr*3.5); + g.addColorStop(0,'rgba('+col+','+(0.15*pulse)+')'); + g.addColorStop(1,'transparent'); + ctx.beginPath(); ctx.arc(n.x,n.y,nr*3.5,0,Math.PI*2); + ctx.fillStyle=g; ctx.fill(); + } + ctx.beginPath(); ctx.arc(n.x,n.y,nr,0,Math.PI*2); + ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.9)' : 'rgba(255,50,80,0.5)'; + ctx.fill(); + ctx.strokeStyle='rgba('+col+',0.6)'; ctx.lineWidth=1; ctx.stroke(); + ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.85)' : 'rgba(255,80,80,0.7)'; + ctx.font = (n.type==='hub'?'600 8px':'6px')+' "Share Tech Mono",monospace'; + ctx.textAlign='center'; + ctx.fillText(n.label, n.x, n.y+nr+9); + }); + _agentTopoRaf = requestAnimationFrame(frame); + } + frame(); +} diff --git a/public_html/index.html b/public_html/index.html index 4b512cf..3513da2 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -418,10 +418,12 @@ style="position:fixed;top:-9999px;left:-9999px;width:320px;height:240px"> - - - - + + + + + +