0 ? round(($dTotal - $dIdle) / $dTotal * 100, 1) : 0; KBEngine::storeFact('system', 'cpu_usage', $cpuPct, 'local', $ttl); $memLines = file('/proc/meminfo'); $mem = []; foreach ($memLines as $l) { if (preg_match('/^(\w+):\s+(\d+)/', $l, $m)) $mem[$m[1]] = (int)$m[2]; } $total = round($mem['MemTotal'] / 1048576, 1); $avail = round($mem['MemAvailable'] / 1048576, 1); $used = round($total - $avail, 1); $free = round($mem['MemFree'] / 1048576, 1); $memPct = $total > 0 ? round($used / $total * 100) : 0; KBEngine::storeFact('system', 'mem_total_gb', $total, 'local', $ttl); KBEngine::storeFact('system', 'mem_used_gb', $used, 'local', $ttl); KBEngine::storeFact('system', 'mem_free_gb', $free, 'local', $ttl); KBEngine::storeFact('system', 'mem_percent', $memPct, 'local', $ttl); $la = explode(' ', file_get_contents('/proc/loadavg')); KBEngine::storeFact('system', 'load_1m', $la[0], 'local', $ttl); KBEngine::storeFact('system', 'load_5m', $la[1], 'local', $ttl); KBEngine::storeFact('system', 'load_15m', $la[2], 'local', $ttl); $sec = (int) file_get_contents('/proc/uptime'); KBEngine::storeFact('system', 'uptime', intdiv($sec, 86400) . ' days, ' . intdiv($sec % 86400, 3600) . ' hours', 'local', $ttl); $df = disk_free_space('/'); $dt = disk_total_space('/'); KBEngine::storeFact('system', 'disk_total', round($dt / 1073741824, 1) . 'GB', 'local', $ttl); KBEngine::storeFact('system', 'disk_used', round(($dt - $df) / 1073741824, 1) . 'GB', 'local', $ttl); KBEngine::storeFact('system', 'disk_free', round($df / 1073741824, 1) . 'GB', 'local', $ttl); $results['system'] = "ok (CPU {$cpuPct}%, MEM {$memPct}%)"; } catch (Exception $e) { $results['system'] = 'error: ' . $e->getMessage(); } // ── Network ─────────────────────────────────────────────────────────── try { $watchlist = [ 'gateway' => '10.48.200.1', 'proxmox' => '10.48.200.90', 'ollama' => '10.48.200.95', 'fusionpbx' => '10.48.200.96', 'ha' => '10.48.200.97', 'do_server' => '165.22.1.228', ]; $online = 0; $total = count($watchlist); foreach ($watchlist as $name => $ip) { exec('ping -c1 -W1 ' . escapeshellarg($ip) . ' > /dev/null 2>&1', $o, $code); $up = ($code === 0); if ($up) $online++; KBEngine::storeFact('network', "host_{$name}", $up ? 'online' : 'offline', $ip, $ttl); } KBEngine::storeFact('network', 'online_count', $online, 'local', $ttl); KBEngine::storeFact('network', 'total_count', $total, 'local', $ttl); KBEngine::storeFact('network', 'gateway_status', $online > 0 ? 'online' : 'offline', 'local', $ttl); $results['network'] = "ok ({$online}/{$total} online)"; } catch (Exception $e) { $results['network'] = 'error: ' . $e->getMessage(); } // ── Proxmox (TTL 10 min) ───────────────────────────────────────────── if ($fresh('proxmox', 600)) { $results['proxmox'] = 'skipped (fresh)'; } else try { if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) { $base = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json'; $auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL; $nd = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/status", $auth); $vms = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/qemu", $auth); $cts = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/lxc", $auth); if (isset($nd['data'])) { $cpuPct = round(($nd['data']['cpu'] ?? 0) * 100, 1); $memU = round(($nd['data']['memory']['used'] ?? 0) / 1073741824, 1); $memT = round(($nd['data']['memory']['total'] ?? 0) / 1073741824, 1); $memPct = $memT > 0 ? round($memU / $memT * 100) : 0; KBEngine::storeFact('proxmox', 'pve_cpu_percent', $cpuPct, PROXMOX_HOST, $ttl); KBEngine::storeFact('proxmox', 'pve_mem_used_gb', $memU, PROXMOX_HOST, $ttl); KBEngine::storeFact('proxmox', 'pve_mem_total_gb', $memT, PROXMOX_HOST, $ttl); KBEngine::storeFact('proxmox', 'pve_mem_percent', $memPct, PROXMOX_HOST, $ttl); } $all = array_merge($vms['data'] ?? [], $cts['data'] ?? []); $running = count(array_filter($all, fn($v) => ($v['status'] ?? '') === 'running')); KBEngine::storeFact('proxmox', 'vm_total', count($all), PROXMOX_HOST, $ttl); KBEngine::storeFact('proxmox', 'vm_running', $running, PROXMOX_HOST, $ttl); $results['proxmox'] = "ok ({$running}/" . count($all) . " running)"; } else { $results['proxmox'] = 'skipped (no token)'; } } catch (Exception $e) { $results['proxmox'] = 'error: ' . $e->getMessage(); } // ── Home Assistant — skipped (HA agent pushes entities every 30s) ──── $results['ha'] = 'skipped (agent push active)'; // ── Digital Ocean ───────────────────────────────────────────────────── try { exec('ping -c1 -W2 165.22.1.228 > /dev/null 2>&1', $o2, $doCode); $doStatus = ($doCode === 0) ? 'online' : 'unreachable'; KBEngine::storeFact('do_server', 'do_status', $doStatus, '165.22.1.228', $ttl); $results['do_server'] = "ok ({$doStatus})"; } catch (Exception $e) { $results['do_server'] = 'error: ' . $e->getMessage(); } // ── Ollama (TTL 15 min) ─────────────────────────────────────────────── if ($fresh('ollama', 900)) { $results['ollama'] = 'skipped (fresh)'; } else try { $ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434'; $ch = curl_init($ollamaHost . '/api/tags'); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]); $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code === 200) { $models = json_decode($resp, true)['models'] ?? []; $names = array_column($models, 'name'); KBEngine::storeFact('ollama', 'available_models', implode(', ', $names) ?: 'none', 'proxmox', null); KBEngine::storeFact('ollama', 'model_count', count($names), 'proxmox', $ttl); KBEngine::storeFact('ollama', 'status', 'online', 'proxmox', $ttl); foreach ($models as $m) { JarvisDB::execute( 'INSERT INTO kb_ollama_models (model_name, size_gb) VALUES (?,?) ON DUPLICATE KEY UPDATE size_gb=VALUES(size_gb), pulled_at=NOW()', [$m['name'], round(($m['size'] ?? 0) / 1073741824, 1)] ); } $results['ollama'] = 'ok (' . (implode(', ', $names) ?: 'no models yet') . ')'; } else { KBEngine::storeFact('ollama', 'status', 'offline', 'proxmox', $ttl); $results['ollama'] = 'unreachable (VM may be booting)'; } } catch (Exception $e) { $results['ollama'] = 'error: ' . $e->getMessage(); } // ── Site Health (TTL 5 min) ─────────────────────────────────────────── if ($fresh('sites', 300)) { $results['sites'] = 'skipped (fresh)'; } else try { $sites = [ 'jarvis' => 'https://jarvis.orbishosting.com', 'tomsjavajive' => 'https://tomsjavajive.com', 'epictravelexp'=> 'https://epictravelexpeditions.com', 'parkersling' => 'https://parkerslingshotrentals.com', 'orbishosting' => 'https://orbishosting.com', 'orbisportal' => 'https://orbis.orbishosting.com', 'tomtomgames' => 'https://tomtomgames.com', ]; $down = []; foreach ($sites as $key => $url) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_NOBODY => true, ]); curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $status = ($code >= 200 && $code < 400) ? 'up' : "down-$code"; KBEngine::storeFact('sites', $key, $status, $url, 180); if ($status !== 'up') $down[] = "$key($code)"; } $results['sites'] = empty($down) ? 'all up' : 'DOWN: ' . implode(', ', $down); } catch (Exception $e) { $results['sites'] = 'error: ' . $e->getMessage(); } // Network device scan is handled by PVE1 cron (/usr/local/bin/jarvis-netscan.sh) // which POSTs nmap results to /api/netscan every 3 minutes. $results['nmap_scan'] = 'handled by PVE1 push (jarvis-netscan.sh)'; return $results; } function pve_api_get(string $url, string $authHeader): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [$authHeader], CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 8, ]); $resp = curl_exec($ch); curl_close($ch); return $resp ? (json_decode($resp, true) ?? []) : []; } // ── Entry point ─────────────────────────────────────────────────────────── $results = collect_all(); if ($isCLI) { echo date('Y-m-d H:i:s') . " JARVIS facts collected:\n"; foreach ($results as $k => $v) { echo " {$k}: {$v}\n"; } } else { echo json_encode(['status' => 'ok', 'results' => $results, 'timestamp' => date('c')]); }