diff --git a/api/endpoints/facts_collector.php b/api/endpoints/facts_collector.php index 7af1404..a5c0eb4 100644 --- a/api/endpoints/facts_collector.php +++ b/api/endpoints/facts_collector.php @@ -311,6 +311,62 @@ function collect_all(): array { $results['sites'] = 'error: ' . $e->getMessage(); } + + // ── Network Device Scan (nmap via PVE1) ─────────────────────────────── + try { + $nmapRaw = shell_exec( + "sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 " . + "root@10.48.200.90 'nmap -sn --send-ip 10.48.200.0/24 2>/dev/null' 2>/dev/null" + ); + if ($nmapRaw) { + $discovered = []; + $cur = null; + foreach (explode("\n", $nmapRaw) as $line) { + $line = trim($line); + if (preg_match('/Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?/', $line, $m)) { + if ($cur) $discovered[] = $cur; + $cur = ['hostname' => ($m[1] && $m[1] !== $m[2]) ? $m[1] : null, 'ip' => $m[2], 'mac' => null, 'vendor' => null]; + } elseif ($cur && preg_match('/MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)/i', $line, $m)) { + $cur['mac'] = strtolower($m[1]); + $cur['vendor'] = $m[2] !== 'Unknown' ? $m[2] : null; + } + } + if ($cur) $discovered[] = $cur; + + $discoveredIPs = []; + foreach ($discovered as $d) { + $discoveredIPs[] = $d['ip']; + JarvisDB::execute( + 'INSERT INTO network_devices (ip, mac, hostname, status, last_seen) + VALUES (?,?,?,"online",NOW()) + ON DUPLICATE KEY UPDATE mac=VALUES(mac), hostname=COALESCE(VALUES(hostname),hostname), + status="online", last_seen=NOW()', + [$d['ip'], $d['mac'], $d['hostname'] ?? $d['vendor']] + ); + if ($d['vendor']) { + JarvisDB::execute( + 'UPDATE network_devices SET device_type=? WHERE ip=? AND (device_type IS NULL OR device_type="")', + [$d['vendor'], $d['ip']] + ); + } + } + + if (!empty($discoveredIPs)) { + $ph = implode(',', array_fill(0, count($discoveredIPs), '?')); + JarvisDB::execute( + "UPDATE network_devices SET status='offline' + WHERE ip NOT IN ($ph) AND last_seen < DATE_SUB(NOW(), INTERVAL 10 MINUTE)", + $discoveredIPs + ); + } + $results['nmap_scan'] = 'ok (' . count($discovered) . ' devices found)'; + } else { + $results['nmap_scan'] = 'skipped (PVE1 unreachable)'; + } + } catch (Exception $e) { + $results['nmap_scan'] = 'error: ' . $e->getMessage(); + } + return $results; } diff --git a/public_html/admin/index.php b/public_html/admin/index.php index d79d6a9..6799f2a 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -10,6 +10,17 @@ function loggedIn(): bool { return !empty($_SESSION['admin_user']); } function j(mixed $d): never { header('Content-Type: application/json'); echo json_encode($d); exit; } function bad(string $msg, int $code = 400): never { http_response_code($code); j(['error' => $msg]); } +function self_upsert_device(array $d): void { + JarvisDB::execute( + 'INSERT INTO network_devices (ip,mac,hostname,status,last_seen) VALUES (?,?,?,"online",NOW()) + ON DUPLICATE KEY UPDATE mac=VALUES(mac), hostname=COALESCE(VALUES(hostname),hostname), status="online", last_seen=NOW()', + [$d['ip'], $d['mac'], $d['hostname'] ?? $d['vendor']] + ); + if (!empty($d['vendor'])) { + JarvisDB::execute('UPDATE network_devices SET device_type=? WHERE ip=? AND (device_type IS NULL OR device_type="")', [$d['vendor'], $d['ip']]); + } +} + // ── BACKEND API ─────────────────────────────────────────────────────────────── $action = $_GET['action'] ?? $_POST['action'] ?? ''; if ($action) { @@ -74,7 +85,26 @@ if ($action) { // ── NETWORK ────────────────────────────────────────────────────────── case 'network_list': - j(JarvisDB::query('SELECT id,ip,mac,hostname,alias,device_type,status,last_seen FROM network_devices WHERE alias IS NOT NULL ORDER BY alias')); + j(JarvisDB::query('SELECT id,ip,mac,hostname,alias,device_type,status,last_seen FROM network_devices ORDER BY status="online" DESC, COALESCE(alias,hostname,ip)')); + + case 'network_scan': + // Trigger immediate nmap scan via PVE1 + $out = shell_exec("sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@10.48.200.90 'nmap -sn --send-ip 10.48.200.0/24 2>/dev/null' 2>/dev/null"); + $count = 0; + if ($out) { + $cur = null; + foreach (explode("\n", $out) as $line) { + $line = trim($line); + if (preg_match('/Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?/', $line, $m)) { + if ($cur) { self_upsert_device($cur); $count++; } + $cur = ['hostname' => ($m[1] && $m[1] !== $m[2]) ? $m[1] : null, 'ip' => $m[2], 'mac' => null, 'vendor' => null]; + } elseif ($cur && preg_match('/MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)/i', $line, $m)) { + $cur['mac'] = strtolower($m[1]); $cur['vendor'] = $m[2] !== 'Unknown' ? $m[2] : null; + } + } + if ($cur) { self_upsert_device($cur); $count++; } + } + j(['ok' => true, 'found' => $count]); case 'network_save': $id = (int)($_POST['id'] ?? 0); @@ -406,9 +436,18 @@ select.filter-sel:focus{border-color:var(--cyan)}
NETWORK DEVICES
+
+
+ FILTER: + + + + +   +
LOADING...
@@ -566,10 +605,12 @@ function nav(el) { } function loadTab(tab) { + // Stop any existing network auto-refresh when leaving + if (_netAutoRefresh) { clearInterval(_netAutoRefresh); _netAutoRefresh = null; } ({ dashboard: loadDashboard, agents: loadAgents, - network: loadNetwork, + network: ()=>{ loadNetwork(); _netAutoRefresh = setInterval(loadNetwork, 30000); }, alerts: loadAlerts, facts: ()=>{ loadFactCategories(); loadFacts(); }, intents: loadIntents, @@ -666,29 +707,74 @@ function delAgent(id, name) { } // ── NETWORK ─────────────────────────────────────────────────────────────────── +let _netFilter = 'all'; +let _allDevices = []; +let _netAutoRefresh = null; + +function setNetFilter(f, el) { + _netFilter = f; + document.querySelectorAll('#tab-network .filter-btn').forEach(b=>b.classList.remove('active')); + el.classList.add('active'); + renderNetwork(); +} + async function loadNetwork() { document.getElementById('network-tbl').innerHTML = '
LOADING...
'; - const devs = await api('network_list'); - if (!devs.length) { document.getElementById('network-tbl').innerHTML='
NO NAMED DEVICES
'; return; } - let rows = devs.map(d => ` - ${esc(d.alias)} - ${esc(d.ip)} - ${esc(d.device_type||'device').toUpperCase()} - ${statusBadge(d.status)} - ${ago(d.last_seen)} -
- - - -
- `).join(''); + _allDevices = await api('network_list'); + renderNetwork(); +} + +function renderNetwork() { + let devs = _allDevices; + if (_netFilter === 'online') devs = devs.filter(d => d.status === 'online'); + if (_netFilter === 'offline') devs = devs.filter(d => d.status === 'offline'); + if (_netFilter === 'named') devs = devs.filter(d => d.alias); + + const onlineCount = _allDevices.filter(d=>d.status==='online').length; + document.getElementById('net-count').textContent = `${onlineCount}/${_allDevices.length} ONLINE`; + + if (!devs.length) { document.getElementById('network-tbl').innerHTML='
NO DEVICES MATCH FILTER
'; return; } + let rows = devs.map(d => { + const name = d.alias || d.hostname || d.ip; + const isNamed = !!d.alias; + const vendor = d.device_type || '—'; + return ` + + + ${esc(name)}${isNamed?'':' (discovered)'} + + ${esc(d.ip)} + ${esc(d.mac||'—')} + ${esc(vendor)} + ${statusBadge(d.status)} + ${ago(d.last_seen)} +
+ + + +
+ `; + }).join(''); document.getElementById('network-tbl').innerHTML = ` - + ${rows}
NAMEIPTYPESTATUSLAST SEENACTIONS
NAMEIPMACVENDOR / TYPESTATUSLAST SEENACTIONS
`; } -function netModal(id=0, ip='', alias='', type='device') { - openModal(id?'EDIT DEVICE':'ADD DEVICE', ` +async function scanNow() { + const btn = document.getElementById('scanBtn'); + btn.textContent = 'SCANNING...'; btn.disabled = true; + const fd = new FormData(); fd.append('action','network_scan'); + try { + const r = await fetch(location.href,{method:'POST',body:fd}); + const d = await r.json(); + if (d.ok) { toast(`Scan complete — ${d.found} devices found`,'ok'); loadNetwork(); } + else toast(d.error||'Scan failed','err'); + } catch(e){ toast('Scan failed','err'); } + btn.textContent = 'SCAN NOW'; btn.disabled = false; +} + +function netModal(id=0, ip='', alias='', type='') { + openModal(id?'NAME / EDIT DEVICE':'ADD DEVICE', `
@@ -703,16 +789,7 @@ function netModal(id=0, ip='', alias='', type='device') { }); } -async function pingDev(id, ip, btn) { - btn.textContent = '...'; btn.disabled = true; - const res = await apiPost('network_ping', {ip}, null); - // Re-fetch to update status - loadNetwork(); - btn.textContent = 'PING'; btn.disabled = false; -} - -// Direct ping returning result -(window.pingDev = async function(id, ip, btn) { +async function pingDev(ip, btn) { btn.textContent='…'; btn.disabled=true; const fd=new FormData(); fd.append('action','network_ping'); fd.append('ip',ip); try { @@ -721,7 +798,7 @@ async function pingDev(id, ip, btn) { toast(d.alive ? `${ip} ONLINE (${d.latency_ms??'?'}ms)` : `${ip} OFFLINE`, d.alive?'ok':'err'); } catch(e){ toast('Ping failed','err'); } btn.textContent='PING'; btn.disabled=false; -}); +} function delNet(id, name) { if (!confirm(`Remove "${name}" from network devices?`)) return;