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)}
ARC REACTOR
⚡ ARC REACTOR
◈ VISION PROTOCOL
+ ◈ GUARDIAN MODE
INFO
SITES
USERS
@@ -1147,6 +1199,49 @@ select.filter-sel:focus{border-color:var(--cyan)}
+
+
+
◈ GUARDIAN MODE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
◈ 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 = `
+ | SEVERITY | TYPE | AGENT | MESSAGE | TIME | |
+
${rows}
`;
+}
+
+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
+
+
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'),