Phase 5: Guardian Mode — continuous awareness + proactive AI alerts

- reactor.py: v5.0.0; guardian_loop() background task scans all agents every
  120s; checks CPU/mem/disk thresholds + agent offline transitions + failed
  services; 10min cooldown per metric to debounce repeat alerts; AI analysis
  of critical findings via Claude; proactive chat injection into conversations
  table; handle_sitrep() generates Iron Man-style full/brief situation reports;
  handle_guardian_config() reads/writes guardian_config table; FastAPI endpoints:
  /guardian/status, /guardian/events, /guardian/events/{id}/ack, /guardian/chat
- arc.php: guardian_status, guardian_events, guardian_ack, guardian_chat actions
- chat.php: Tier 0.9d detects sitrep/situation report/how are things commands
- index.html: GUARDIAN tab in right panel; guardian event list with severity
  badges + AI analysis; ACK / ACK ALL buttons; Guardian badge in bottom bar
  (green/amber/red pulse based on unread critical events); proactive chat
  polling every 30s surfacing guardian-injected messages as JARVIS speech
- admin/index.php: GUARDIAN MODE tab; status bar + events table + config modal;
  inline SITREP runner with result modal; threshold configuration
This commit is contained in:
2026-06-11 04:52:08 +00:00
parent 56c9e2d914
commit f15225994a
4 changed files with 501 additions and 1 deletions
+252
View File
@@ -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)}
<div class="nav-section">ARC REACTOR</div>
<div class="nav-item" data-tab="arc" onclick="nav(this)">⚡ ARC REACTOR</div>
<div class="nav-item" data-tab="vision" onclick="nav(this)">◈ VISION PROTOCOL</div>
<div class="nav-item" data-tab="guardian" onclick="nav(this)" id="nav-guardian">◈ GUARDIAN MODE</div>
<div class="nav-section">INFO</div>
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
@@ -1147,6 +1199,49 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div>
</div>
<!-- GUARDIAN MODE -->
<div class="tab" id="tab-guardian">
<div class="page-title" id="guardian-title">◈ GUARDIAN MODE</div>
<div id="guardian-status-bar" style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap">
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:10px 18px;border-radius:4px">
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:3px">STATUS</div>
<div id="guardian-stat-status" style="font-size:1rem;font-family:var(--mono);color:var(--green)">CHECKING...</div>
</div>
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:10px 18px;border-radius:4px">
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:3px">LAST SCAN</div>
<div id="guardian-stat-scan" style="font-size:0.75rem;font-family:var(--mono)">—</div>
</div>
<div class="stat-box" style="background:rgba(255,34,68,0.06);border:1px solid var(--border);padding:10px 18px;border-radius:4px">
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:3px">UNREAD</div>
<div id="guardian-stat-unread" style="font-size:1rem;font-family:var(--mono);color:var(--red)">—</div>
</div>
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:10px 18px;border-radius:4px">
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:3px">24H EVENTS</div>
<div id="guardian-stat-24h" style="font-size:1rem;font-family:var(--mono)">—</div>
</div>
<div class="stat-box" style="background:rgba(0,212,255,0.06);border:1px solid var(--border);padding:10px 18px;border-radius:4px">
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:3px">THRESHOLDS</div>
<div id="guardian-stat-thresh" style="font-size:0.62rem;font-family:var(--mono);line-height:1.6">—</div>
</div>
</div>
<div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
<button class="btn btn-sm btn-green" onclick="guardianRunSitrep()">◈ RUN SITREP</button>
<button class="btn btn-sm" onclick="loadGuardian()">↻ REFRESH</button>
<button class="btn btn-sm" onclick="guardianAckAllAdmin()">✓ ACK ALL</button>
<select id="guardian-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadGuardian()">
<option value="">ALL EVENTS</option>
<option value="critical">CRITICAL</option>
<option value="warning">WARNING</option>
<option value="info">INFO</option>
</select>
<button class="btn btn-sm" onclick="guardianConfigModal()" style="margin-left:auto">⚙ CONFIGURE</button>
</div>
<div class="tbl-wrap" id="guardian-events-tbl"><div class="loading">LOADING...</div></div>
</div>
<!-- GMAIL TRIAGE -->
<div class="tab" id="tab-triage">
<div class="page-title">◈ COMMS PROTOCOL — GMAIL TRIAGE</div>
@@ -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 = '<div class="loading" style="text-align:center;padding:30px">◈ ALL CLEAR — No events match filter</div>';
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 `<tr style="${acked?'opacity:0.4':''}">
<td style="width:70px">
<span style="color:${color};font-size:0.62rem;font-weight:700">${_SEV_ICON[sev]||'◈'} ${sev.toUpperCase()}</span>
</td>
<td style="width:70px;font-size:0.6rem;color:var(--dim)">${esc(ev.event_type||'').replace('_',' ').toUpperCase()}</td>
<td style="font-size:0.62rem">${esc(ev.hostname||ev.agent_id||'—')}</td>
<td style="max-width:300px">
<div style="font-size:0.65rem">${icon} ${esc(ev.message||'')}</div>
${ev.ai_analysis ? `<div style="font-size:0.58rem;color:var(--cyan);opacity:0.8;margin-top:3px;font-style:italic">${esc(ev.ai_analysis.substring(0,200))}</div>` : ''}
</td>
<td style="font-size:0.6rem;color:var(--dim);white-space:nowrap">${ts}</td>
<td style="white-space:nowrap">
${!acked ? `<button class="btn btn-xs" onclick="guardianAck(${ev.id})">ACK</button>` : '<span style="color:var(--border2);font-size:0.55rem">ACKED</span>'}
</td>
</tr>`;
}).join('');
tbl.innerHTML = `<table><thead><tr>
<th>SEVERITY</th><th>TYPE</th><th>AGENT</th><th>MESSAGE</th><th>TIME</th><th></th>
</tr></thead><tbody>${rows}</tbody></table>`;
}
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(), `
<div style="font-size:0.6rem;color:var(--dim);margin-bottom:10px;font-family:var(--mono)">
ONLINE: ${r.agents_online} · OFFLINE: ${r.agents_offline} · EVENTS 24H: ${r.events_24h} · CRITICAL: ${r.critical_24h}
</div>
<pre style="white-space:pre-wrap;font-size:0.7rem;line-height:1.7;color:var(--text)">${esc(r.sitrep||'')}</pre>
`, 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', `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<label class="lbl">CPU THRESHOLD (%)</label>
<input id="gcfg-cpu" class="inp" type="number" value="${thresh.cpu||85}" min="50" max="99">
</div>
<div>
<label class="lbl">MEMORY THRESHOLD (%)</label>
<input id="gcfg-mem" class="inp" type="number" value="${thresh.memory||88}" min="50" max="99">
</div>
<div>
<label class="lbl">DISK THRESHOLD (%)</label>
<input id="gcfg-disk" class="inp" type="number" value="${thresh.disk||88}" min="50" max="99">
</div>
<div>
<label class="lbl">OFFLINE TIMEOUT (min)</label>
<input id="gcfg-offline" class="inp" type="number" value="${thresh.offline_minutes||3}" min="1" max="30">
</div>
<div>
<label class="lbl">SCAN INTERVAL (sec)</label>
<input id="gcfg-interval" class="inp" type="number" value="${d.scan_interval||120}" min="30" max="600">
</div>
<div>
<label class="lbl">GUARDIAN ENABLED</label>
<select id="gcfg-enabled" class="inp">
<option value="1"${enabled?' selected':''}>ENABLED</option>
<option value="0"${!enabled?' selected':''}>PAUSED</option>
</select>
</div>
</div>
`, 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)'};