diff --git a/api/endpoints/agent.php b/api/endpoints/agent.php index 32a2132..6611aa6 100644 --- a/api/endpoints/agent.php +++ b/api/endpoints/agent.php @@ -54,7 +54,7 @@ function update_agent_seen(string $agentId, string $status = 'online', ?string $ // ── Auth (all actions except register) ─────────────────────────────────────── $agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? ''; -$browserActions = ['list', 'status', 'myip']; +$browserActions = ['list', 'status', 'myip', 'regkey']; if ($agentAction !== 'register') { if (in_array($agentAction, $browserActions)) { @@ -212,6 +212,10 @@ switch ($agentAction) { ); agent_ok(); + // ── REGKEY (browser: session-authed fetch of registration key) ─────────── + case 'regkey': + agent_ok(['registration_key' => AGENT_REGISTRATION_KEY]); + // ── LIST (admin: get all agents status) ────────────────────────────────── case 'list': // Mark agents offline if last_seen > 2 minutes ago diff --git a/api/endpoints/netscan.php b/api/endpoints/netscan.php index cc92221..eefe78b 100644 --- a/api/endpoints/netscan.php +++ b/api/endpoints/netscan.php @@ -2,7 +2,7 @@ // Network scan push endpoint — called by PVE1 cron with nmap results // Authenticates via X-Registration-Key header (same key as agent installer) -define('NETSCAN_KEY', 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518'); +define('NETSCAN_KEY', AGENT_REGISTRATION_KEY); if ($method !== 'POST') { echo json_encode(['error' => 'POST only']); exit; diff --git a/public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md b/public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md index b802877..323cc14 100644 --- a/public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md +++ b/public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md @@ -663,7 +663,7 @@ Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1` ### Agent System Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics). -Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` +Registration key: `[rotated 2026-07-07 — stored in api/config.php on VM211, not documented here]` Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s ` ### Self-Healing Watchdog @@ -918,7 +918,7 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \ | Service | Key | |---------|-----| | GitHub PAT | `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` (rotated 2026-07-05, scopes `repo`+`workflow`) | -| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` | +| JARVIS Agent Registration | `[rotated 2026-07-07 — stored in api/config.php on VM211, not documented here]` | | Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` | | HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` | | Sonarr API | `b43e04350a594846b4ee95261c29e9e0` | diff --git a/public_html/agent/install.sh b/public_html/agent/install.sh index a37b1d3..dbf46e0 100644 --- a/public_html/agent/install.sh +++ b/public_html/agent/install.sh @@ -21,7 +21,14 @@ JARVIS_HOST="" INSTALL_DIR="/opt/jarvis-agent" CONFIG_DIR="/etc/jarvis-agent" STATE_DIR="/var/lib/jarvis-agent" -REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" +REG_KEY="${JARVIS_REG_KEY:-}" +if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then + read -rp "Enter JARVIS registration key: " REG_KEY &2 + exit 1 +fi SERVICE_FILE="/etc/systemd/system/jarvis-agent.service" echo "=== JARVIS Agent Installer v3.0 ===" diff --git a/public_html/api.php b/public_html/api.php index 24ad1b0..78bba65 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -1,128 +1,128 @@ - true, - $_e0 === 'netscan' => true, - $_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true, - default => false, -}; -if (!$_skipSession) { - session_start(); -} - -header('Content-Type: application/json'); -$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com']; -$_origin = $_SERVER['HTTP_ORIGIN'] ?? ''; -if (in_array($_origin, $_allowedOrigins, true)) { - header('Access-Control-Allow-Origin: ' . $_origin); - header('Access-Control-Allow-Credentials: true'); -} -header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); -header('Access-Control-Allow-Headers: Content-Type, X-Session-Token'); - -if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } - -$uri = $_SERVER['REQUEST_URI'] ?? '/'; -$method = $_SERVER['REQUEST_METHOD']; -$path = trim(parse_url($uri, PHP_URL_PATH), '/'); -$parts = explode('/', $path); - -if (($parts[0] ?? '') === 'api') array_shift($parts); -$endpoint = $parts[0] ?? ''; -$action = $parts[1] ?? ''; - -// ── Auth check (skip for auth / agent / netscan) ────────────────────── -if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) { - $token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? ''); - $isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? ''); - if (!$isValid) { - $ip = $_SERVER['REMOTE_ADDR'] ?? ''; - $isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true); - if (!$isLocal && $endpoint !== 'ping') { - http_response_code(401); - echo json_encode(['error' => 'Unauthorized', 'code' => 401]); - exit; - } - } -} - -if ($endpoint !== 'auth') session_write_close(); - -$body = file_get_contents('php://input'); -$data = json_decode($body, true) ?? []; - -// ── Fast ping (no file dispatch needed) ────────────────────────────── -if ($endpoint === 'ping') { - echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]); - exit; -} - -// ── Endpoint → file map ─────────────────────────────────────────────── -$endpoints = [ - 'auth' => 'auth.php', - 'chat' => 'chat.php', - 'system' => 'system.php', - 'netscan' => 'netscan.php', - 'network' => 'network.php', - 'proxmox' => 'proxmox.php', - 'ha' => 'ha.php', - 'tts' => 'tts.php', - 'email' => 'email.php', - 'do' => 'do_server.php', - 'alerts' => 'alerts.php', - 'facts' => 'facts_collector.php', - 'weather' => 'weather.php', - 'news' => 'news.php', - 'sites' => 'sites.php', - 'agent' => 'agent.php', - 'planner' => 'planner.php', - 'jellyfin' => 'jellyfin.php', - 'history' => 'history.php', - 'metrics' => 'metrics.php', - 'suggestions' => 'suggestions.php', - 'arc' => 'arc.php', - 'directives' => 'directives.php', - 'memory' => 'memory.php', - 'calendar' => 'calendar_sync.php', -]; - -if (!isset($endpoints[$endpoint])) { - http_response_code(404); - echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]); - exit; -} - -$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint]; - -// ── Fault-isolated dispatch ─────────────────────────────────────────── -// ob_start() buffers any partial output so a mid-execution fatal doesn't -// send a broken response. catch(Throwable) catches ParseError, TypeError, -// and all other Errors + Exceptions in PHP 7+. -ob_start(); -try { - require $file; - ob_end_flush(); -} catch (\Throwable $e) { - ob_end_clean(); - http_response_code(500); - echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]); - error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d', - $endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine() - )); -} + true, + $_e0 === 'netscan' => true, + $_e0 === 'agent' && !in_array($_e1, ['list','status','myip','regkey'], true) => true, + default => false, +}; +if (!$_skipSession) { + session_start(); +} + +header('Content-Type: application/json'); +$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com']; +$_origin = $_SERVER['HTTP_ORIGIN'] ?? ''; +if (in_array($_origin, $_allowedOrigins, true)) { + header('Access-Control-Allow-Origin: ' . $_origin); + header('Access-Control-Allow-Credentials: true'); +} +header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); +header('Access-Control-Allow-Headers: Content-Type, X-Session-Token'); + +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } + +$uri = $_SERVER['REQUEST_URI'] ?? '/'; +$method = $_SERVER['REQUEST_METHOD']; +$path = trim(parse_url($uri, PHP_URL_PATH), '/'); +$parts = explode('/', $path); + +if (($parts[0] ?? '') === 'api') array_shift($parts); +$endpoint = $parts[0] ?? ''; +$action = $parts[1] ?? ''; + +// ── Auth check (skip for auth / agent / netscan) ────────────────────── +if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) { + $token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? ''); + $isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? ''); + if (!$isValid) { + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + $isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true); + if (!$isLocal && $endpoint !== 'ping') { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized', 'code' => 401]); + exit; + } + } +} + +if ($endpoint !== 'auth') session_write_close(); + +$body = file_get_contents('php://input'); +$data = json_decode($body, true) ?? []; + +// ── Fast ping (no file dispatch needed) ────────────────────────────── +if ($endpoint === 'ping') { + echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]); + exit; +} + +// ── Endpoint → file map ─────────────────────────────────────────────── +$endpoints = [ + 'auth' => 'auth.php', + 'chat' => 'chat.php', + 'system' => 'system.php', + 'netscan' => 'netscan.php', + 'network' => 'network.php', + 'proxmox' => 'proxmox.php', + 'ha' => 'ha.php', + 'tts' => 'tts.php', + 'email' => 'email.php', + 'do' => 'do_server.php', + 'alerts' => 'alerts.php', + 'facts' => 'facts_collector.php', + 'weather' => 'weather.php', + 'news' => 'news.php', + 'sites' => 'sites.php', + 'agent' => 'agent.php', + 'planner' => 'planner.php', + 'jellyfin' => 'jellyfin.php', + 'history' => 'history.php', + 'metrics' => 'metrics.php', + 'suggestions' => 'suggestions.php', + 'arc' => 'arc.php', + 'directives' => 'directives.php', + 'memory' => 'memory.php', + 'calendar' => 'calendar_sync.php', +]; + +if (!isset($endpoints[$endpoint])) { + http_response_code(404); + echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]); + exit; +} + +$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint]; + +// ── Fault-isolated dispatch ─────────────────────────────────────────── +// ob_start() buffers any partial output so a mid-execution fatal doesn't +// send a broken response. catch(Throwable) catches ParseError, TypeError, +// and all other Errors + Exceptions in PHP 7+. +ob_start(); +try { + require $file; + ob_end_flush(); +} catch (\Throwable $e) { + ob_end_clean(); + http_response_code(500); + echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]); + error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d', + $endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine() + )); +} diff --git a/public_html/assets/js/panels/jarvis-agents.js b/public_html/assets/js/panels/jarvis-agents.js index 7a0a894..07a7859 100644 --- a/public_html/assets/js/panels/jarvis-agents.js +++ b/public_html/assets/js/panels/jarvis-agents.js @@ -1,726 +1,729 @@ -// ── 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 `
-
-
- ${escHtml(ag.hostname)} - ${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(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 jUrl = window.location.origin; - // Fixed 2026-07-07: this used to be hardcoded to https://jarvis.orbishosting.com/agent, - // which isn't reachable from outside the LAN at all (no FortiGate VIP forwards the - // default port there — confirmed HTTP:000). Using the current page's own origin - // instead means the download link always matches wherever the visitor actually - // reached this dashboard from, LAN or external. - const baseUrl = jUrl + '/agent'; - - 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', - // install-windows.ps1 takes no parameters — it reads the reg key from - // $env:JARVIS_REG_KEY and downloads the standalone exe itself (no - // Python/pywin32 needed on the target machine as of the 2026-07-07 rebuild). - cmd:'# Run PowerShell as Administrator:\n$env:JARVIS_REG_KEY=\''+regKey+'\'\nirm '+baseUrl+'/install-windows.ps1 | iex', - dl: baseUrl+'/install-windows.exe', - note:'Run PowerShell as Administrator. Installs as a real Windows Service (auto-starts at boot, no window to keep open).' - }, - 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', - // install.sh takes positional args (hostname, agent_type) and reads the - // JARVIS URL from an env var — the registration key is already baked in, - // no --key flag exists. - cmd:'curl -sSL '+baseUrl+'/install.sh | JARVIS_URL='+jUrl+' bash -s -- $(hostname) linux', - dl: baseUrl+'/install.sh', - note:'Run in terminal (sudo). Installs as a systemd service.' - }, - unknown: { - label:'Your System', - cmd:'# Couldn\'t detect your OS automatically. Installers are at:\n'+baseUrl+'/install.sh (Linux)\n'+baseUrl+'/install-mac.sh (macOS)\n'+baseUrl+'/install-windows.ps1 (Windows)', - dl: baseUrl+'/install.sh', - note:'Auto-detection didn\'t recognize this browser/OS — pick the matching installer above.' - } - }; - 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(); -}); - +// ── 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 `
+
+
+ ${escHtml(ag.hostname)} + ${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(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(''); +} + +async function openAgentModal() { + const os = detectOS(); + const title = document.getElementById('agentModalTitle'); + const content = document.getElementById('agentModalContent'); + const modal = document.getElementById('agentModal'); + let regKey = ''; + try { + const rkResp = await fetch('/api/agent/regkey'); + if (rkResp.ok) { const rk = await rkResp.json(); if (rk.registration_key) regKey = rk.registration_key; } + } catch (e) { /* not logged in — placeholder stays */ } + const jUrl = window.location.origin; + // Fixed 2026-07-07: this used to be hardcoded to https://jarvis.orbishosting.com/agent, + // which isn't reachable from outside the LAN at all (no FortiGate VIP forwards the + // default port there — confirmed HTTP:000). Using the current page's own origin + // instead means the download link always matches wherever the visitor actually + // reached this dashboard from, LAN or external. + const baseUrl = jUrl + '/agent'; + + 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', + // install-windows.ps1 takes no parameters — it reads the reg key from + // $env:JARVIS_REG_KEY and downloads the standalone exe itself (no + // Python/pywin32 needed on the target machine as of the 2026-07-07 rebuild). + cmd:'# Run PowerShell as Administrator:\n$env:JARVIS_REG_KEY=\''+regKey+'\'\nirm '+baseUrl+'/install-windows.ps1 | iex', + dl: baseUrl+'/install-windows.exe', + note:'Run PowerShell as Administrator. Installs as a real Windows Service (auto-starts at boot, no window to keep open).' + }, + 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', + // install.sh takes positional args (hostname, agent_type); JARVIS URL and + // registration key come from env vars (key is no longer baked into the script). + cmd:'curl -sSL '+baseUrl+'/install.sh | JARVIS_URL='+jUrl+' JARVIS_REG_KEY=\''+regKey+'\' bash -s -- $(hostname) linux', + dl: baseUrl+'/install.sh', + note:'Run in terminal (sudo). Installs as a systemd service.' + }, + unknown: { + label:'Your System', + cmd:'# Couldn\'t detect your OS automatically. Installers are at:\n'+baseUrl+'/install.sh (Linux)\n'+baseUrl+'/install-mac.sh (macOS)\n'+baseUrl+'/install-windows.ps1 (Windows)', + dl: baseUrl+'/install.sh', + note:'Auto-detection didn\'t recognize this browser/OS — pick the matching installer above.' + } + }; + 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/install-agent.sh b/public_html/install-agent.sh index a37b1d3..dbf46e0 100755 --- a/public_html/install-agent.sh +++ b/public_html/install-agent.sh @@ -21,7 +21,14 @@ JARVIS_HOST="" INSTALL_DIR="/opt/jarvis-agent" CONFIG_DIR="/etc/jarvis-agent" STATE_DIR="/var/lib/jarvis-agent" -REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" +REG_KEY="${JARVIS_REG_KEY:-}" +if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then + read -rp "Enter JARVIS registration key: " REG_KEY &2 + exit 1 +fi SERVICE_FILE="/etc/systemd/system/jarvis-agent.service" echo "=== JARVIS Agent Installer v3.0 ==="