import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js'; function fmtBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(i > 0 ? 1 : 0) + ' ' + sizes[i]; } export default definePage({ init() { return { firewall: getModel('firewall'), network: getModel('network'), dnsmasq: getModel('dnsmasq'), wireguard: getModel('wireguard'), acme: getModel('acme'), nginx: getModel('nginx'), status: getModel('status'), }; }, render(state) { const guard = renderGuardMulti('Dashboard', 'System overview', state.firewall, state.network, state.dnsmasq, state.wireguard, state.acme, state.nginx, state.status); if (guard) return guard; // Extract data const fwIfaces = Array.isArray(state.firewall.data?.interfaces) ? state.firewall.data.interfaces : []; const fwZones = state.firewall.data?.zones || {}; const netIfaces = state.network.data?.interfaces || {}; const dnsmasqStatus = state.dnsmasq.data?.status || {}; const dnsmasqLeases = state.dnsmasq.data?.leases || []; const activeLeaseCount = dnsmasqStatus.active_leases || dnsmasqLeases.length; const proxyDomains = state.nginx.data?.domains || []; const onlineDomains = proxyDomains ? proxyDomains.filter(d => d.online).length : 0; const offlineDomains = (proxyDomains?.length || 0) - onlineDomains; const wgp = state.wireguard.data?.peers || []; const wgStatus = state.wireguard.data?.status || {}; const allCerts = state.acme.data?.certs || []; const expiringCerts = allCerts.filter(c => c.expired || (c.days_remaining !== undefined && c.days_remaining <= 30)); // System metrics from status model const sysMetrics = state.status.data?.metrics || {}; const sysLoad = sysMetrics.load || {}; const sysMem = sysMetrics.memory || {}; const sysSwap = sysMetrics.swap || {}; const sysTraffic = sysMetrics.traffic || {}; // Pending changes const pend = state.status.data?.pending || {}; const totalChanges = pend.total_changes || 0; const pendKeys = ['firewall', 'dnsmasq', 'nginx', 'wireguard', 'networkd'].filter(k => k === 'firewall' ? (pend[k]?.needs_apply) : (pend[k]?.pending_changes) ); const pendLabels = { firewall: 'Firewall', dnsmasq: 'DHCP', nginx: 'Proxy', wireguard: 'WireGuard', networkd: 'Network' }; // Build merged interface list const allNames = [...new Set([...fwIfaces.map(f => f.name), ...Object.keys(netIfaces)])]; const ifaces = allNames.map(name => { const fw = fwIfaces.find(f => f.name === name); const netEntry = netIfaces[name] || {}; const traffic = sysTraffic[name] || {}; const ips = fw ? [...(fw.ips || []), ...(fw.ipv6 || [])] : []; const addrs = netEntry?.addresses || []; const isUp = ['routable', 'degraded'].some(s => (netEntry.state || '').startsWith(s)); return { name, mac: fw?.mac || null, ips: ips.length ? ips : addrs, isUp, zone: fw?.zone || '—', rx: traffic.rx_bytes || 0, tx: traffic.tx_bytes || 0, }; }); // ── Stat cards ── const zoneNames = Object.keys(fwZones); const stats = html`
Unapplied changes in: ${pendKeys.map(k => pendLabels[k]).join(', ')}
<${ActionButton} url="/api/status/apply-all" label="Apply All Changes" successMsg="All changes applied" refresh="status" cls="btn btn-sm btn-primary" />| CPU Load | ${(sysLoad.load1 || 0).toFixed(2)} / ${(sysLoad.load5 || 0).toFixed(2)} / ${(sysLoad.load15 || 0).toFixed(2)} | 1m / 5m / 15m |
| Memory | <${Badge} text=${Math.round(memPct) + '%'} variant=${memVariant} /> ${fmtBytes(sysMem.used || 0)} / ${fmtBytes(sysMem.total || 0)} | used / total |
| Swap | ${sysSwap.total > 0 ? html`<${Badge} text=${Math.round(swapPct) + '%'} variant=${swapVariant} /> ${fmtBytes(sysSwap.used || 0)} / ${fmtBytes(sysSwap.total)}` : html`Disabled`} | used / total |
Showing 10 of ${dnsmasqLeases.length}.
` : ''}