/dev/null'; $out = shell_exec($cmd); $alive = $out && strpos($out, '1 received') !== false; $latency = null; if ($alive && preg_match('/time=([\d.]+)/', $out, $m)) { $latency = (float)$m[1]; } return ['alive' => $alive, 'latency_ms' => $latency]; } function scanSubnet(string $prefix, int $timeout = 10): array { $cmd = 'nmap -sn --host-timeout 1s ' . escapeshellarg($prefix . '.0/24') . ' -oG - 2>/dev/null | grep "Up$" | awk \'{print $2}\''; $out = shell_exec($cmd) ?? ''; $hosts = array_filter(explode("\n", trim($out))); return array_values($hosts); } function getArpTable(): array { $out = shell_exec('arp -n 2>/dev/null') ?? ''; $devices = []; foreach (explode("\n", trim($out)) as $line) { if (preg_match('/^([\d.]+)\s+\w+\s+([\w:]+)/', $line, $m)) { $devices[$m[1]] = strtolower($m[2]); } } return $devices; } $action = $action ?? 'status'; $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; if ($action === 'scan') { // JARVIS runs on DigitalOcean — cannot reach 10.48.200.x directly. // Scan is delegated to the PVE1 agent (jarvis-netscan.sh runs nmap locally). // This endpoint: queues the scan command to PVE1, returns current DB state immediately. // Queue netscan to PVE1 agent $pve1 = JarvisDB::single( "SELECT agent_id FROM registered_agents WHERE ip_address='10.48.200.90' AND status='online' LIMIT 1" ); $queued = false; if ($pve1) { JarvisDB::execute( "INSERT INTO agent_commands (agent_id, command_type, command_data, status) VALUES (?,?,?,?)", [$pve1['agent_id'], 'shell', json_encode(['command'=>'/usr/local/bin/jarvis-netscan.sh','allowed'=>true]), 'pending'] ); $queued = true; } // Return current online devices from DB (populated by PVE1 netscan every 3 min) $devices = JarvisDB::query( "SELECT ip, mac, hostname, alias, device_type as type, status, last_seen FROM network_devices WHERE status='online' AND last_seen > DATE_SUB(NOW(), INTERVAL 15 MINUTE) ORDER BY COALESCE(alias,hostname,ip)" ); echo json_encode([ 'devices' => $devices, 'count' => count($devices), 'queued' => $queued, 'scanned_at' => date('c'), 'note' => $queued ? 'Scan dispatched to PVE1 — results update in ~40 seconds.' : 'Returning cached scan data. PVE1 auto-scans every 3 minutes.', ]); } elseif ($action === 'add' && $method === 'POST') { $ip = filter_var($data['ip'] ?? '', FILTER_VALIDATE_IP); $alias = substr(trim($data['alias'] ?? ''), 0, 100); $type = preg_replace('/[^a-z0-9_\-]/', '', strtolower($data['type'] ?? 'device')); if (!$ip) { echo json_encode(['error' => 'Invalid IP address']); exit; } if (!$alias) { echo json_encode(['error' => 'Name is required']); exit; } JarvisDB::execute( 'INSERT INTO network_devices (ip, alias, device_type, status) VALUES (?,?,?,\'unknown\') ON DUPLICATE KEY UPDATE alias=VALUES(alias), device_type=VALUES(device_type)', [$ip, $alias, $type] ); echo json_encode(['success' => true]); } elseif ($action === 'delete' && $method === 'POST') { $ip = filter_var($data['ip'] ?? '', FILTER_VALIDATE_IP); if (!$ip) { echo json_encode(['error' => 'Invalid IP']); exit; } // Don't allow deleting agent-managed entries $isAgent = JarvisDB::query('SELECT id FROM registered_agents WHERE ip_address=? LIMIT 1', [$ip]); if (!empty($isAgent)) { echo json_encode(['error' => 'Cannot delete agent-managed device']); exit; } JarvisDB::execute('DELETE FROM network_devices WHERE ip=?', [$ip]); echo json_encode(['success' => true]); } else { // Status: unified device list from agents + user-managed DB entries + external services $devices = []; // Mark agents offline if not heard from in 2 minutes JarvisDB::execute( 'UPDATE registered_agents SET status="offline" WHERE last_seen < DATE_SUB(NOW(), INTERVAL 2 MINUTE) AND status = "online"' ); // 1. Agent-based devices — status from heartbeat, no ping from DO needed $agents = JarvisDB::query( 'SELECT agent_id, hostname, ip_address, status, last_seen, agent_type FROM registered_agents ORDER BY hostname' ); $agentIPs = []; foreach ($agents as $ag) { $agentIPs[] = $ag['ip_address']; $devices[] = [ 'ip' => $ag['ip_address'], 'name' => $ag['hostname'], 'type' => 'agent', 'agent_id' => $ag['agent_id'], 'agent_type' => $ag['agent_type'], 'alive' => $ag['status'] === 'online', 'status' => $ag['status'], 'last_seen' => $ag['last_seen'], 'source' => 'agent', 'deletable' => false, ]; } // 2. User-managed devices from DB (named/aliased entries not covered by agents) $pinned = JarvisDB::query( 'SELECT ip, alias, device_type, status, last_seen FROM network_devices WHERE alias IS NOT NULL AND alias != "" ORDER BY alias' ); foreach ($pinned as $dev) { if (in_array($dev['ip'], $agentIPs)) continue; // agent already covers this IP $ping = pingHost($dev['ip']); $newStatus = $ping['alive'] ? 'online' : 'offline'; JarvisDB::execute( 'UPDATE network_devices SET status=?, last_seen=NOW() WHERE ip=?', [$newStatus, $dev['ip']] ); $devices[] = [ 'ip' => $dev['ip'], 'name' => $dev['alias'], 'type' => $dev['device_type'] ?: 'device', 'alive' => $ping['alive'], 'latency_ms' => $ping['latency_ms'], 'status' => $newStatus, 'last_seen' => $dev['last_seen'], 'source' => 'db', 'deletable' => true, ]; } // 3. Netscan-discovered devices (PVE1 nmap push — status from last scan) $discovered = JarvisDB::query( 'SELECT ip, mac, hostname, device_type, status, last_seen FROM network_devices WHERE (alias IS NULL OR alias = "") AND last_seen > DATE_SUB(NOW(), INTERVAL 15 MINUTE) ORDER BY ip' ); foreach ($discovered as $dev) { if (in_array($dev['ip'], $agentIPs)) continue; $devices[] = [ 'ip' => $dev['ip'], 'name' => $dev['hostname'] ?: ($dev['device_type'] ?: $dev['ip']), 'mac' => $dev['mac'], 'type' => $dev['device_type'] ?: 'device', 'alive' => $dev['status'] === 'online', 'status' => $dev['status'], 'last_seen' => $dev['last_seen'], 'source' => 'netscan', 'deletable' => false, ]; } // 4. External services we can actually ping from DO $external = [ ['ip' => '134.209.72.226', 'name' => 'FusionPBX DO', 'type' => 'server'], ]; foreach ($external as $host) { if (in_array($host['ip'], $agentIPs)) continue; $ping = pingHost($host['ip']); $devices[] = array_merge($host, [ 'alive' => $ping['alive'], 'latency_ms' => $ping['latency_ms'], 'status' => $ping['alive'] ? 'online' : 'offline', 'source' => 'static', 'deletable' => false, ]); } echo json_encode(['devices' => $devices, 'timestamp' => date('c')]); }