diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php index e844478..831a45e 100644 --- a/api/endpoints/arc.php +++ b/api/endpoints/arc.php @@ -179,6 +179,40 @@ switch ($action) { echo json_encode(arc_request('DELETE', "/screenshots/{$id}")); break; + // ── GUARDIAN MODE ───────────────────────────────────────────────────────── + case 'guardian_status': + echo json_encode(arc_request('GET', '/guardian/status')); + break; + + case 'guardian_events': + $limit = (int)($_GET['limit'] ?? 30); + $unread = !empty($_GET['unread']) ? 'true' : ''; + $severity = $_GET['severity'] ?? ''; + $since = $_GET['since'] ?? ''; + $qs = http_build_query(array_filter([ + 'limit' => $limit, + 'unread' => $unread, + 'severity' => $severity, + 'since' => $since, + ])); + echo json_encode(arc_request('GET', '/guardian/events' . ($qs ? "?{$qs}" : ''))); + break; + + case 'guardian_ack': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if ($id) { + echo json_encode(arc_request('POST', "/guardian/events/{$id}/ack")); + } else { + echo json_encode(arc_request('POST', '/guardian/events/ack_all')); + } + break; + + case 'guardian_chat': + $since = $_GET['since'] ?? ''; + $qs = $since ? '?since=' . urlencode($since) : ''; + echo json_encode(arc_request('GET', '/guardian/chat' . $qs)); + break; + default: http_response_code(404); echo json_encode(['error' => "Unknown arc action: {$action}"]); diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index a0ccef1..17b6b07 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1190,6 +1190,35 @@ if (!$reply) { } } +// ── Tier 0.9d: Guardian Mode — sitrep detection ─────────────────────────── +if (!$reply) { + $sitrepPatterns = [ + '/^(?:jarvis[,\s]+)?(?:sitrep|sit\s+rep|situation\s+report|status\s+report)/i', + '/^(?:jarvis[,\s]+)?(?:give\s+me\s+a|run\s+a)\s+(?:sitrep|situation|status)\s*(?:report)?/i', + '/^(?:jarvis[,\s]+)?(?:how\s+(?:are|is)\s+(?:everything|all\s+systems?|things?)(?:\s+looking)?)/i', + '/^(?:jarvis[,\s]+)?(?:system\s+health|overall\s+status|all\s+systems\s+(?:check|status|go))/i', + '/^(?:jarvis[,\s]+)?what(?:\'s|\s+is)\s+(?:the\s+)?overall\s+(?:system\s+)?status/i', + ]; + $isSimple = (bool) preg_match('/\b(?:brief|quick|short|summary)\b/i', $message); + foreach ($sitrepPatterns as $pat) { + if (preg_match($pat, $message)) { + $arcRes = arcSubmitJob('sitrep', [ + 'detail' => $isSimple ? 'brief' : 'full', + 'provider' => 'claude', + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ GUARDIAN PROTOCOL — Generating situation report (Job #{$arcJobId}). Scanning all field stations and synthesizing a briefing now, {$userAddr}. Stand by."; + $source = 'arc:sitrep'; + } else { + $reply = "Guardian Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + // ── Tier 0.9e: Intel Protocol — research & tool_loop detection ──────────── $intelPatterns = [ '/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research', diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 6bdd64b..2181dbd 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -592,6 +592,57 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['ok'=>true]); + // ── GUARDIAN MODE ───────────────────────────────────────────────── + case 'guardian_status': + $ch = curl_init('http://127.0.0.1:7474/guardian/status'); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['error'=>'unreachable']); + + case 'guardian_events': + $limit = (int)($_GET['limit'] ?? 50); + $severity = $_GET['severity'] ?? ''; + $url = 'http://127.0.0.1:7474/guardian/events?' . http_build_query(array_filter(['limit'=>$limit,'severity'=>$severity])); + $ch = curl_init($url); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: []); + + case 'guardian_ack': + $id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); + if ($id) { + $ch = curl_init('http://127.0.0.1:7474/guardian/events/'.$id.'/ack'); + } else { + $ch = curl_init('http://127.0.0.1:7474/guardian/events/ack_all'); + } + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>'']); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['ok'=>true]); + + case 'guardian_sitrep': + $detail = $_GET['detail'] ?? 'full'; + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch,[ + CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode(['type'=>'sitrep','payload'=>['detail'=>$detail,'provider'=>'claude'],'priority'=>9,'created_by'=>'admin']), + CURLOPT_HTTPHEADER=>['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['error'=>'Arc Reactor unreachable']); + + case 'guardian_config_set': + $key = $_POST['key'] ?? $_GET['key'] ?? ''; + $val = $_POST['value'] ?? $_GET['value'] ?? ''; + if (!$key) bad('Missing key'); + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch,[ + CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode(['type'=>'guardian_config','payload'=>['action'=>'set','key'=>$key,'value'=>$val],'priority'=>9,'created_by'=>'admin']), + CURLOPT_HTTPHEADER=>['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['ok'=>true]); + case 'users_list': j(JarvisDB::query('SELECT id,username,display_name,last_seen,created_at FROM users ORDER BY username')); @@ -832,6 +883,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1147,6 +1199,49 @@ select.filter-sel:focus{border-color:var(--cyan)} + +
+
◈ GUARDIAN MODE
+ +
+
+
STATUS
+
CHECKING...
+
+
+
LAST SCAN
+
+
+
+
UNREAD
+
+
+
+
24H EVENTS
+
+
+
+
THRESHOLDS
+
+
+
+ +
+ + + + + +
+ +
LOADING...
+
+
◈ COMMS PROTOCOL — GMAIL TRIAGE
@@ -1282,6 +1377,7 @@ function loadTab(tab) { email: ()=>{ loadEmailInbox(); loadEmailActionItems(); }, triage: loadTriage, vision: loadVision, + guardian: loadGuardian, tasks: loadTasks, appointments: loadAppts, calendar: loadCalFeeds, @@ -2289,6 +2385,162 @@ async function visionPurge() { loadVision(); } +// ── GUARDIAN MODE ──────────────────────────────────────────────────────────── +const _SEV_COLOR = {critical:'var(--red)', warning:'#f5a623', info:'var(--cyan)'}; +const _SEV_ICON = {critical:'⚠', warning:'⚡', info:'◈'}; +const _EV_ICON = {agent_offline:'⚠',agent_online:'✓',cpu_high:'⚡',mem_high:'⚡', + disk_high:'💾',service_down:'✗',service_recovered:'✓',sitrep:'◈',anomaly:'◈'}; +let _guardianJobPoll = null; + +async function loadGuardian() { + const tbl = document.getElementById('guardian-events-tbl'); + if (!tbl) return; + + const [statusData, eventsData] = await Promise.all([ + api('guardian_status'), + api('guardian_events', {limit: 100, severity: document.getElementById('guardian-filter')?.value || ''}), + ]); + + // Update status bar + const s = statusData || {}; + const c = s.counts || {}; + const thresh = s.thresholds || {}; + setEl('guardian-stat-status', s.enabled ? '● ACTIVE' : '○ PAUSED', s.enabled ? 'var(--green)' : 'var(--red)'); + setEl('guardian-stat-scan', s.last_scan ? new Date(s.last_scan+'Z').toLocaleString() : '—', ''); + setEl('guardian-stat-unread', c.unread || '0', c.unread > 0 ? 'var(--red)' : 'var(--green)'); + setEl('guardian-stat-24h', c.events_24h || '0', ''); + setEl('guardian-stat-thresh', `CPU >${thresh.cpu}% · MEM >${thresh.memory}% · DISK >${thresh.disk}%`, ''); + + const navItem = document.getElementById('nav-guardian'); + if (navItem && c.critical_unread > 0) navItem.style.color = 'var(--red)'; + else if (navItem) navItem.style.color = ''; + + const events = Array.isArray(eventsData) ? eventsData : []; + if (!events.length) { + tbl.innerHTML = '
◈ ALL CLEAR — No events match filter
'; + return; + } + + const rows = events.map(ev => { + const sev = ev.severity || 'info'; + const color = _SEV_COLOR[sev] || 'var(--text)'; + const icon = _EV_ICON[ev.event_type] || '◈'; + const acked = ev.acknowledged; + const ts = ts(ev.created_at); + return ` + + ${_SEV_ICON[sev]||'◈'} ${sev.toUpperCase()} + + ${esc(ev.event_type||'').replace('_',' ').toUpperCase()} + ${esc(ev.hostname||ev.agent_id||'—')} + +
${icon} ${esc(ev.message||'')}
+ ${ev.ai_analysis ? `
${esc(ev.ai_analysis.substring(0,200))}
` : ''} + + ${ts} + + ${!acked ? `` : 'ACKED'} + + `; + }).join(''); + + tbl.innerHTML = ` + + ${rows}
SEVERITYTYPEAGENTMESSAGETIME
`; +} + +async function guardianRunSitrep() { + const d = await api('guardian_sitrep', {detail: 'full'}); + if (d.job_id) { + toast('SITREP job started — Job #' + d.job_id, 'ok'); + if (_guardianJobPoll) clearInterval(_guardianJobPoll); + _guardianJobPoll = setInterval(async () => { + const job = await api('arc_job_get', {id: d.job_id}); + if (job.status === 'done') { + clearInterval(_guardianJobPoll); _guardianJobPoll = null; + const r = typeof job.result === 'string' ? JSON.parse(job.result) : job.result; + openModal('◈ SITREP — ' + new Date().toLocaleString(), ` +
+ ONLINE: ${r.agents_online} · OFFLINE: ${r.agents_offline} · EVENTS 24H: ${r.events_24h} · CRITICAL: ${r.critical_24h} +
+
${esc(r.sitrep||'')}
+ `, null, null); + document.getElementById('modalSave').style.display = 'none'; + loadGuardian(); + } else if (job.status === 'failed') { + clearInterval(_guardianJobPoll); _guardianJobPoll = null; + toast('SITREP failed: ' + (job.error||'unknown'), 'err'); + } + }, 3000); + } else { + toast('Failed to start SITREP: ' + (d.error||'Arc offline'), 'err'); + } +} + +async function guardianAck(id) { + await api('guardian_ack', {id}); + toast('Acknowledged', 'ok'); + loadGuardian(); +} + +async function guardianAckAllAdmin() { + await api('guardian_ack'); + toast('All events acknowledged', 'ok'); + loadGuardian(); +} + +async function guardianConfigModal() { + const d = await api('guardian_status'); + const thresh = d.thresholds || {}; + const enabled = d.enabled; + openModal('⚙ GUARDIAN CONFIGURATION', ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `, async () => { + const updates = { + cpu_threshold: document.getElementById('gcfg-cpu')?.value, + mem_threshold: document.getElementById('gcfg-mem')?.value, + disk_threshold: document.getElementById('gcfg-disk')?.value, + offline_minutes: document.getElementById('gcfg-offline')?.value, + scan_interval: document.getElementById('gcfg-interval')?.value, + enabled: document.getElementById('gcfg-enabled')?.value, + }; + for (const [key, value] of Object.entries(updates)) { + await api('guardian_config_set', {key, value}); + } + toast('Guardian config saved', 'ok'); + closeModal(); + loadGuardian(); + }, 'SAVE CONFIG'); +} + // ── GMAIL TRIAGE ───────────────────────────────────────────────────────────── const _TRIAGE_COLORS = {urgent:'var(--red)',action:'var(--orange)',reply:'var(--cyan)',meeting:'#a78bfa',info:'var(--text-dim)',promo:'rgba(255,255,255,0.25)',spam:'rgba(255,255,255,0.15)'}; diff --git a/public_html/index.html b/public_html/index.html index 32a2397..6dd5007 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -875,6 +875,25 @@ body::after{ transition:filter 1.2s ease; } #app.sleeping #sleepOverlay{display:flex} +/* ── GUARDIAN MODE ────────────────────────────────────────────────── */ +.guardian-event{display:flex;align-items:flex-start;gap:8px;padding:7px 10px;border-bottom:1px solid var(--panel-border);cursor:pointer} +.guardian-event:hover{background:rgba(0,212,255,0.04)} +.guardian-event.critical{border-left:3px solid var(--red)} +.guardian-event.warning{border-left:3px solid #f5a623} +.guardian-event.info{border-left:3px solid rgba(0,212,255,0.3)} +.guardian-event.acked{opacity:0.45} +.guardian-sev{font-family:var(--font-mono);font-size:0.5rem;padding:2px 4px;border-radius:2px;flex-shrink:0;letter-spacing:1px;margin-top:1px} +.guardian-sev.critical{background:rgba(255,34,68,0.15);color:var(--red);border:1px solid rgba(255,34,68,0.3)} +.guardian-sev.warning{background:rgba(245,166,35,0.12);color:#f5a623;border:1px solid rgba(245,166,35,0.3)} +.guardian-sev.info{background:rgba(0,212,255,0.08);color:var(--cyan);border:1px solid rgba(0,212,255,0.2)} +.guardian-msg{flex:1;font-size:0.62rem;line-height:1.4;color:var(--text)} +.guardian-time{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);flex-shrink:0} +.guardian-ai{font-size:0.6rem;color:rgba(0,212,255,0.6);margin-top:3px;font-style:italic} +#bb-guardian-dot.all-clear{background:var(--green);box-shadow:0 0 5px var(--green)} +#bb-guardian-dot.warning{background:#f5a623;box-shadow:0 0 5px #f5a623} +#bb-guardian-dot.critical{background:var(--red);box-shadow:0 0 5px var(--red);animation:pulse 1.2s ease-in-out infinite} +.guardian-ack-btn{background:none;border:1px solid var(--panel-border);color:var(--text-dim);padding:1px 5px;border-radius:2px;font-size:0.5rem;cursor:pointer;font-family:var(--font-mono);letter-spacing:1px;flex-shrink:0} +.guardian-ack-btn:hover{color:var(--cyan);border-color:var(--cyan)} /* ── VISION PROTOCOL — screenshot lightbox ───────────────────────── */ #vision-lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:9999;flex-direction:column;align-items:center;justify-content:flex-start;padding:20px;overflow-y:auto} #vision-lightbox.open{display:flex} @@ -1134,6 +1153,7 @@ body::after{
SITES
INTEL
COMMS
+
GUARDIAN
@@ -1156,6 +1176,9 @@ body::after{
+
+
+
@@ -1189,6 +1212,12 @@ body::after{ ARC REACTOR OFFLINE +
+
+ GUARDIAN INIT + +
+
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED --:--:-- @@ -2363,6 +2392,13 @@ function showApp(name, greeting, silent = false) { loadAlerts(); loadWeather(); loadNews(); + // Guardian Mode — badge refresh + proactive chat + setTimeout(() => { + _refreshGuardianBadge(); + _pollProactiveChat(); + startGuardianPolling(); + setInterval(_pollProactiveChat, 30000); + }, 5000); } async function logout() { @@ -3061,7 +3097,8 @@ function switchTab(name) { if (name === 'news') loadNews(); if (name === 'agents') loadAgents(); if (name === 'intel') loadIntel(); - if (name === 'comms') loadComms(); + if (name === 'comms') loadComms(); + if (name === 'guardian') loadGuardian(); if (name === 'alerts') loadAlerts(); } @@ -3802,6 +3839,154 @@ function stopCommsPolling() { if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; } } +// ── GUARDIAN MODE ───────────────────────────────────────────────────────────── +let _guardianPollTimer = null; +let _guardianChatTimer = null; +let _guardianLastChat = ''; +let _guardianUnread = 0; + +async function loadGuardian() { + const el = document.getElementById('guardian-list'); + if (!el) return; + + try { + const [statusData, eventsData] = await Promise.all([ + api('arc?action=guardian_status').catch(() => ({})), + api('arc?action=guardian_events&limit=40').catch(() => []), + ]); + + const events = Array.isArray(eventsData) ? eventsData : []; + const status = statusData || {}; + const counts = status.counts || {}; + const unread = parseInt(counts.unread || 0); + const critU = parseInt(counts.critical_unread || 0); + + _guardianUnread = unread; + _updateGuardianBadge(unread, critU); + + const lastScan = status.last_scan + ? new Date(status.last_scan + 'Z').toLocaleTimeString() + : '—'; + + let html = `
+ ◈ GUARDIAN MODE + + ${status.enabled ? '● ACTIVE' : '○ INACTIVE'} + + SCAN: ${lastScan} + ${unread ? `` : ''} + +
`; + + 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) {} +} + async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'),