mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 00:34:35 -05:00
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:
+186
-1
@@ -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{
|
||||
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
||||
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
|
||||
<div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</div>
|
||||
<div class="tab" id="tab-btn-guardian" onclick="switchTab('guardian')">GUARDIAN</div>
|
||||
</div>
|
||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
||||
@@ -1156,6 +1176,9 @@ body::after{
|
||||
<div id="tab-comms" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||
<div id="comms-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<div id="tab-guardian" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||
<div id="guardian-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1189,6 +1212,12 @@ body::after{
|
||||
<span>ARC REACTOR</span> <span id="bb-arc-status">OFFLINE</span>
|
||||
</div>
|
||||
|
||||
<div class="bb-item" id="bb-guardian-item" style="cursor:pointer" onclick="switchGuardianTab()">
|
||||
<div class="bb-dot" id="bb-guardian-dot" style="background:var(--text-dim)"></div>
|
||||
<span>GUARDIAN</span> <span id="bb-guardian-status" style="color:var(--text-dim)">INIT</span>
|
||||
<span id="bb-guardian-badge" style="display:none;background:var(--red);color:#fff;font-size:0.45rem;padding:1px 4px;border-radius:2px;font-family:var(--font-mono);letter-spacing:0">0</span>
|
||||
</div>
|
||||
|
||||
<div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
|
||||
<div style="font-size:0.65rem;flex-shrink:0">
|
||||
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
||||
@@ -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 = `<div style="padding:6px 10px 4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--cyan)">◈ GUARDIAN MODE</span>
|
||||
<span style="font-family:var(--font-mono);font-size:0.5rem;color:${status.enabled?'var(--green)':'var(--red)'}">
|
||||
${status.enabled ? '● ACTIVE' : '○ INACTIVE'}
|
||||
</span>
|
||||
<span style="font-family:var(--font-mono);font-size:0.5rem;color:var(--text-dim)">SCAN: ${lastScan}</span>
|
||||
${unread ? `<button onclick="guardianAckAll()" class="guardian-ack-btn" style="margin-left:auto">ACK ALL (${unread})</button>` : '<span style="margin-left:auto"></span>'}
|
||||
<button onclick="guardianSitrep()" style="background:rgba(0,212,255,0.08);border:1px solid var(--panel-border);color:var(--cyan);padding:3px 7px;border-radius:3px;font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SITREP</button>
|
||||
</div>`;
|
||||
|
||||
if (!events.length) {
|
||||
html += '<div style="text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px">◈ ALL CLEAR<br><span style="opacity:0.5">Guardian is watching...</span></div>';
|
||||
} 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 += `<div class="guardian-event ${sev}${acked?' acked':''}" id="gev-${ev.id}">
|
||||
<span class="guardian-sev ${sev}">${sev.toUpperCase()}</span>
|
||||
<div style="flex:1">
|
||||
<div class="guardian-msg">${typeIco} ${escHtml(ev.message||'')}</div>
|
||||
${ev.ai_analysis ? `<div class="guardian-ai">${escHtml(ev.ai_analysis.substring(0,200))}</div>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">
|
||||
<span class="guardian-time">${ts}</span>
|
||||
${!acked ? `<button class="guardian-ack-btn" onclick="guardianAck(${ev.id})">ACK</button>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
el.innerHTML = html;
|
||||
startGuardianPolling();
|
||||
|
||||
} catch(e) {
|
||||
if (el) el.innerHTML = '<div style="text-align:center;padding:20px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim)">GUARDIAN OFFLINE</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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'),
|
||||
|
||||
Reference in New Issue
Block a user