DATE_SUB(NOW(), INTERVAL 5 MINUTE)" ); foreach ($latest as $row) { $d = json_decode($row['metric_data'] ?? '{}', true); $hn = $row['hostname']; $id = $row['agent_id']; // CPU $cpu = (float)($d['cpu_percent'] ?? 0); if ($cpu >= $CPU_WARN) { $key = 'agent:' . $id . ':cpu_high'; $sev = $cpu >= 95 ? 'critical' : 'warning'; upsert_alert($key, $sev, 'High CPU: ' . $hn, round($cpu, 1) . '% CPU utilization on ' . $hn . '. Sustained high load detected.'); $still_active[$key] = true; } // Memory $mem_pct = (float)($d['memory']['percent'] ?? 0); if ($mem_pct >= $MEM_WARN) { $key = 'agent:' . $id . ':mem_high'; $sev = $mem_pct >= 95 ? 'critical' : 'warning'; upsert_alert($key, $sev, 'High Memory: ' . $hn, round($mem_pct, 1) . '% memory used on ' . $hn . ' (' . round($d['memory']['used_mb'] ?? 0) . '/' . round($d['memory']['total_mb'] ?? 0) . ' MB).'); $still_active[$key] = true; } // Disk foreach (($d['disk'] ?? []) as $disk) { $pct = (int)($disk['percent'] ?? 0); if ($pct >= $DISK_WARN) { $mount = $disk['mount'] ?? '/'; $key = 'agent:' . $id . ':disk:' . str_replace('/', '_', $mount); $sev = $pct >= $DISK_CRIT ? 'critical' : 'warning'; upsert_alert($key, $sev, 'Disk Full: ' . $hn . ' ' . $mount, $mount . ' is ' . $pct . '% full on ' . $hn . ' (' . ($disk['used'] ?? '?') . ' of ' . ($disk['size'] ?? '?') . ' used).'); $still_active[$key] = true; } } // Services down foreach (($d['services'] ?? []) as $svc) { if (($svc['status'] ?? '') === 'active') continue; if (($svc['status'] ?? '') === 'unknown') continue; // not watched/installed $svcName = $svc['service'] ?? ''; $key = 'agent:' . $id . ':svc:' . $svcName; upsert_alert($key, 'warning', 'Service Down: ' . $svcName . ' on ' . $hn, $svcName . ' is ' . ($svc['status'] ?? 'inactive') . ' on ' . $hn . '.'); $still_active[$key] = true; } } // ── Auto-resolve alerts whose condition has cleared ──────────────────────── if (!empty($still_active)) { $active_keys = array_keys($still_active); // Get all auto-resolvable alerts that are unresolved $open_auto = JarvisDB::query( "SELECT id, source_key FROM alerts WHERE resolved=0 AND auto_resolve=1 AND source_key IS NOT NULL" ); foreach ($open_auto as $row) { if (!isset($still_active[$row['source_key']])) { JarvisDB::query( 'UPDATE alerts SET resolved=1, resolved_at=NOW() WHERE id=?', [$row['id']] ); } } } else { // Nothing active — resolve all auto alerts JarvisDB::query( "UPDATE alerts SET resolved=1, resolved_at=NOW() WHERE resolved=0 AND auto_resolve=1" ); } } function upsert_alert(string $key, string $sev, string $title, string $msg): void { $existing = JarvisDB::query( 'SELECT id, severity FROM alerts WHERE source_key=? AND resolved=0 LIMIT 1', [$key] ); if ($existing) { // Update severity/message if changed (e.g., warning → critical) if ($existing[0]['severity'] !== $sev) { JarvisDB::query( 'UPDATE alerts SET severity=?, title=?, message=?, created_at=NOW() WHERE id=?', [$sev, $title, $msg, $existing[0]['id']] ); } } else { JarvisDB::query( 'INSERT INTO alerts (alert_type, title, message, severity, source_key, auto_resolve) VALUES (?,?,?,?,?,1)', ['agent', $title, $msg, $sev, $key] ); } } // ── Route ───────────────────────────────────────────────────────────────────── if ($method === 'GET') { // Rate-limit agent alert refresh to once per 60 seconds via kb_facts lock $last_refresh = JarvisDB::query("SELECT fact_value FROM kb_facts WHERE category='agent' AND fact_key='alert_refresh' LIMIT 1"); $last_ts = !empty($last_refresh) ? (int)$last_refresh[0]['fact_value'] : 0; if (time() - $last_ts >= 60) { JarvisDB::query( "INSERT INTO kb_facts (category, fact_key, fact_value, host) VALUES ('agent', 'alert_refresh', ?, 'local') ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()", [time()] ); refresh_agent_alerts(); } $alerts = JarvisDB::query( 'SELECT * FROM alerts WHERE resolved=0 ORDER BY severity DESC, created_at DESC LIMIT 30' ); echo json_encode(['alerts' => $alerts ?: [], 'count' => count($alerts ?: [])]); } elseif ($method === 'POST' && ($action === 'resolve' || ($data['action'] ?? '') === 'resolve')) { $id = (int)($data['id'] ?? 0); JarvisDB::query('UPDATE alerts SET resolved=1, resolved_at=NOW() WHERE id=?', [$id]); echo json_encode(['success' => true]); } elseif ($method === 'POST') { JarvisDB::query( 'INSERT INTO alerts (alert_type, title, message, severity) VALUES (?,?,?,?)', [$data['type'] ?? 'system', $data['title'] ?? 'Alert', $data['message'] ?? '', $data['severity'] ?? 'info'] ); echo json_encode(['success' => true]); }