const deviceListEl = document.getElementById('deviceList'); const machineInfoEl = document.getElementById('machineInfo'); const portsListEl = document.getElementById('portsList'); const statusText = document.getElementById('statusText'); const scanBtn = document.getElementById('scanBtn'); const subnetInput = document.getElementById('subnetInput'); const scanProgressCard = document.getElementById('scanProgressCard'); const scanProgressTitle = document.getElementById('scanProgressTitle'); const scanProgressPercent = document.getElementById('scanProgressPercent'); const scanProgressFill = document.getElementById('scanProgressFill'); const scanProgressDetail = document.getElementById('scanProgressDetail'); let devices = []; let selectedDeviceId = null; function setStatus(msg) { statusText.textContent = msg; } function formatDuration(totalSeconds) { if (totalSeconds == null || Number.isNaN(Number(totalSeconds))) return '-'; const seconds = Math.max(0, Math.floor(Number(totalSeconds))); const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) return `${h}h ${m}m ${s}s`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } function renderScanProgress(progress) { if (!progress || !progress.running) { scanProgressCard.classList.add('idle'); scanProgressTitle.textContent = 'No Active Scan'; scanProgressPercent.textContent = '0%'; scanProgressFill.style.width = '0%'; scanProgressDetail.textContent = 'Start a scan to see live progress.'; scanBtn.disabled = false; return; } const total = Number(progress.total_hosts || 0); const processed = Number(progress.processed_hosts || 0); const saved = Number(progress.saved_hosts || 0); const percent = Number(progress.percent || 0); const current = progress.current_host || 'waiting for next host'; const totalLabel = total > 0 ? total : '?'; const etaLabel = progress.eta_seconds == null ? 'calculating...' : formatDuration(progress.eta_seconds); const elapsedLabel = formatDuration(progress.elapsed_seconds || 0); scanProgressCard.classList.remove('idle'); scanProgressTitle.textContent = `Scan #${progress.scan_id} Running`; scanProgressPercent.textContent = `${percent}%`; scanProgressFill.style.width = `${percent}%`; scanProgressDetail.textContent = `Processed ${processed}/${totalLabel} hosts | Saved ${saved} | ETA ${etaLabel} | Elapsed ${elapsedLabel} | Current ${current}`; scanBtn.disabled = true; } async function api(path, options = {}) { const res = await fetch(path, options); if (!res.ok) { let detail = `${res.status}`; try { const data = await res.json(); if (data.detail) detail = data.detail; } catch (_) {} throw new Error(detail); } return res.json(); } function formatDate(iso) { if (!iso) return '-'; return new Date(iso).toLocaleString(); } function deviceTitle(d) { return d.hostname ? `${d.hostname} (${d.ip})` : d.ip; } function parseIPv4(ip) { if (typeof ip !== 'string') return null; const parts = ip.trim().split('.'); if (parts.length !== 4) return null; const nums = parts.map((p) => Number(p)); if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null; return nums; } function compareIpNumeric(a, b) { const pa = parseIPv4(a.ip); const pb = parseIPv4(b.ip); if (pa && pb) { for (let i = 0; i < 4; i += 1) { if (pa[i] !== pb[i]) return pa[i] - pb[i]; } return 0; } if (pa) return -1; if (pb) return 1; return String(a.ip || '').localeCompare(String(b.ip || '')); } function renderDeviceList() { deviceListEl.innerHTML = ''; if (!devices.length) { deviceListEl.innerHTML = '
  • No devices discovered yet.
  • '; return; } devices.forEach((d) => { const li = document.createElement('li'); li.className = `device-item ${selectedDeviceId === d.id ? 'active' : ''}`; li.innerHTML = `
    ${deviceTitle(d)}
    ${d.os_name || 'OS unknown'} | ${d.is_active ? 'Active' : 'Missing'}
    `; li.addEventListener('click', () => { selectedDeviceId = d.id; renderDeviceList(); loadDevice(d.id); }); deviceListEl.appendChild(li); }); } function renderMachineInfo(d) { machineInfoEl.classList.remove('empty'); machineInfoEl.innerHTML = `
    Hostname
    ${d.hostname || '-'}
    IP Address
    ${d.ip}
    MAC Address
    ${d.mac || '-'}
    Vendor
    ${d.vendor || '-'}
    Operating System
    ${d.os_name || '-'}
    Status
    ${d.is_active ? 'Active' : 'Not Seen in Last Scan'}
    First Seen
    ${formatDate(d.first_seen)}
    Last Seen
    ${formatDate(d.last_seen)}
    `; } function renderPorts(ports) { portsListEl.innerHTML = ''; portsListEl.classList.remove('empty'); if (!ports.length) { portsListEl.classList.add('empty'); portsListEl.textContent = 'No ports recorded for this machine.'; return; } ports.forEach((p) => { const details = document.createElement('details'); details.className = 'port'; const svc = [p.service, p.product, p.version].filter(Boolean).join(' '); const headers = Object.entries(p.headers || {}) .map(([k, v]) => `${k}: ${v}`) .join('\n') || 'No headers captured'; details.innerHTML = ` ${p.port}/${p.protocol} - ${p.state}${svc ? ` - ${svc}` : ''}
    Service: ${svc || 'Unknown'} Extra: ${p.extra_info || '-'} Banner: ${p.banner || '-'} First Seen: ${formatDate(p.first_seen)} Last Seen: ${formatDate(p.last_seen)} Headers:\n${headers}
    `; portsListEl.appendChild(details); }); } async function loadDevices() { devices = await api('/api/devices'); devices.sort((a, b) => { if (a.is_active !== b.is_active) return b.is_active - a.is_active; return compareIpNumeric(a, b); }); renderDeviceList(); if (!selectedDeviceId && devices.length) { selectedDeviceId = devices[0].id; renderDeviceList(); } if (selectedDeviceId) { await loadDevice(selectedDeviceId); } } async function loadDevice(deviceId) { const d = await api(`/api/devices/${deviceId}`); renderMachineInfo(d); renderPorts(d.ports || []); } async function runScan() { const subnet = subnetInput.value.trim(); if (!subnet) return; setStatus(`Starting scan on ${subnet}...`); try { const result = await api(`/api/scans/run?subnet=${encodeURIComponent(subnet)}`, { method: 'POST' }); renderScanProgress({ running: true, scan_id: result.scan_id, total_hosts: 0, processed_hosts: 0, saved_hosts: 0, current_host: null, percent: 0, }); setStatus(`Scan #${result.scan_id} running on ${result.subnet}. Refreshing automatically...`); await pollUntilComplete(result.scan_id); } catch (err) { setStatus(`Scan failed to start: ${err.message}`); await refreshScanProgress(); } } async function pollUntilComplete(scanId) { for (let i = 0; i < 240; i += 1) { await new Promise((r) => setTimeout(r, 2000)); const [health, scans] = await Promise.all([api('/api/health'), api('/api/scans?limit=1')]); renderScanProgress(health.scan_progress); const latest = scans[0]; if (!latest || latest.id !== scanId) continue; if (latest.status === 'running') { const progress = health.scan_progress || {}; const total = progress.total_hosts || 0; const processed = progress.processed_hosts || 0; const label = total > 0 ? `${processed}/${total}` : `${processed}/?`; const eta = progress.eta_seconds == null ? 'calculating ETA' : `ETA ${formatDuration(progress.eta_seconds)}`; setStatus(`Scan #${scanId} is running (${label} hosts processed, ${eta})...`); continue; } setStatus(`Scan #${scanId} ${latest.status} (${latest.host_count} hosts).`); renderScanProgress(null); await loadDevices(); return; } setStatus(`Scan #${scanId} is taking longer than expected.`); } async function refreshScanProgress() { try { const health = await api('/api/health'); renderScanProgress(health.scan_progress); } catch (_) { renderScanProgress(null); } } scanBtn.addEventListener('click', runScan); (async function init() { setStatus('Loading inventory...'); try { await Promise.all([loadDevices(), refreshScanProgress()]); setStatus('Ready.'); } catch (err) { setStatus(`Failed loading data: ${err.message}`); } })(); setInterval(refreshScanProgress, 3000);