mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -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:
@@ -179,6 +179,40 @@ switch ($action) {
|
|||||||
echo json_encode(arc_request('DELETE', "/screenshots/{$id}"));
|
echo json_encode(arc_request('DELETE', "/screenshots/{$id}"));
|
||||||
break;
|
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:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['error' => "Unknown arc action: {$action}"]);
|
echo json_encode(['error' => "Unknown arc action: {$action}"]);
|
||||||
|
|||||||
@@ -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 ────────────
|
// ── Tier 0.9e: Intel Protocol — research & tool_loop detection ────────────
|
||||||
$intelPatterns = [
|
$intelPatterns = [
|
||||||
'/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research',
|
'/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research',
|
||||||
|
|||||||
@@ -592,6 +592,57 @@ if ($action) {
|
|||||||
$raw = curl_exec($ch); curl_close($ch);
|
$raw = curl_exec($ch); curl_close($ch);
|
||||||
j(json_decode($raw, true) ?: ['ok'=>true]);
|
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':
|
case 'users_list':
|
||||||
j(JarvisDB::query('SELECT id,username,display_name,last_seen,created_at FROM users ORDER BY username'));
|
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-section">ARC REACTOR</div>
|
||||||
<div class="nav-item" data-tab="arc" onclick="nav(this)">⚡ 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="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-section">INFO</div>
|
||||||
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</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>
|
<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>
|
||||||
</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 -->
|
<!-- GMAIL TRIAGE -->
|
||||||
<div class="tab" id="tab-triage">
|
<div class="tab" id="tab-triage">
|
||||||
<div class="page-title">◈ COMMS PROTOCOL — GMAIL TRIAGE</div>
|
<div class="page-title">◈ COMMS PROTOCOL — GMAIL TRIAGE</div>
|
||||||
@@ -1282,6 +1377,7 @@ function loadTab(tab) {
|
|||||||
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
||||||
triage: loadTriage,
|
triage: loadTriage,
|
||||||
vision: loadVision,
|
vision: loadVision,
|
||||||
|
guardian: loadGuardian,
|
||||||
tasks: loadTasks,
|
tasks: loadTasks,
|
||||||
appointments: loadAppts,
|
appointments: loadAppts,
|
||||||
calendar: loadCalFeeds,
|
calendar: loadCalFeeds,
|
||||||
@@ -2289,6 +2385,162 @@ async function visionPurge() {
|
|||||||
loadVision();
|
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 ─────────────────────────────────────────────────────────────
|
// ── 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)'};
|
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)'};
|
||||||
|
|
||||||
|
|||||||
@@ -875,6 +875,25 @@ body::after{
|
|||||||
transition:filter 1.2s ease;
|
transition:filter 1.2s ease;
|
||||||
}
|
}
|
||||||
#app.sleeping #sleepOverlay{display:flex}
|
#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 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{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}
|
#vision-lightbox.open{display:flex}
|
||||||
@@ -1134,6 +1153,7 @@ body::after{
|
|||||||
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
<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-intel" onclick="switchTab('intel')">INTEL</div>
|
||||||
<div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</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>
|
||||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
<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="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 id="comms-list"><div class="loading-shimmer"></div></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>
|
||||||
</div>
|
</div>
|
||||||
@@ -1189,6 +1212,12 @@ body::after{
|
|||||||
<span>ARC REACTOR</span> <span id="bb-arc-status">OFFLINE</span>
|
<span>ARC REACTOR</span> <span id="bb-arc-status">OFFLINE</span>
|
||||||
</div>
|
</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 id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
|
||||||
<div style="font-size:0.65rem;flex-shrink:0">
|
<div style="font-size:0.65rem;flex-shrink:0">
|
||||||
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
||||||
@@ -2363,6 +2392,13 @@ function showApp(name, greeting, silent = false) {
|
|||||||
loadAlerts();
|
loadAlerts();
|
||||||
loadWeather();
|
loadWeather();
|
||||||
loadNews();
|
loadNews();
|
||||||
|
// Guardian Mode — badge refresh + proactive chat
|
||||||
|
setTimeout(() => {
|
||||||
|
_refreshGuardianBadge();
|
||||||
|
_pollProactiveChat();
|
||||||
|
startGuardianPolling();
|
||||||
|
setInterval(_pollProactiveChat, 30000);
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
@@ -3062,6 +3098,7 @@ function switchTab(name) {
|
|||||||
if (name === 'agents') loadAgents();
|
if (name === 'agents') loadAgents();
|
||||||
if (name === 'intel') loadIntel();
|
if (name === 'intel') loadIntel();
|
||||||
if (name === 'comms') loadComms();
|
if (name === 'comms') loadComms();
|
||||||
|
if (name === 'guardian') loadGuardian();
|
||||||
if (name === 'alerts') loadAlerts();
|
if (name === 'alerts') loadAlerts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3802,6 +3839,154 @@ function stopCommsPolling() {
|
|||||||
if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; }
|
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() {
|
async function loadAgents() {
|
||||||
const [listData, metricsData] = await Promise.all([
|
const [listData, metricsData] = await Promise.all([
|
||||||
api('agent/list'),
|
api('agent/list'),
|
||||||
|
|||||||
Reference in New Issue
Block a user