import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=10'; 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`
<${StatCard} label="Active Zones" value=${zoneNames.length} meta=${zoneNames.join(', ') || 'None'} /> <${StatCard} label="DHCP Leases" value=${activeLeaseCount} meta=${dnsmasqLeases.slice(0, 3).map(l => l.hostname || l.mac).join(', ') || 'None'} /> <${StatCard} label="Proxy Domains" value=${onlineDomains + '/' + (proxyDomains?.length || 0)} meta=${offlineDomains + ' offline'} /> <${StatCard} label="Certificates" value=${allCerts.length} meta=${expiringCerts.length ? expiringCerts.length + ' expiring' : 'All valid'} />
`; // ── Pending changes ── const pendingCard = pendKeys.length > 0 ? html`
Pending Changes <${Badge} text=${String(totalChanges)} variant="warning" />

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" />
` : html`
Pending Changes
<${Badge} text="All configured" variant="success" />
`; // ── System resources ── const memPct = sysMem.used_pct || 0; const memVariant = memPct > 80 ? 'danger' : memPct > 60 ? 'warning' : 'success'; const swapPct = sysSwap.used_pct || 0; const swapVariant = swapPct > 50 ? 'warning' : 'success'; const systemCard = html`
System Resources
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
`; // ── Interface table ── const ifaceRows = ifaces.map(i => html` ${i.name} ${i.mac || '—'} ${i.ips.join(', ') || '—'} ${i.zone !== '—' ? html`<${Badge} text=${i.zone} variant="info" />` : html``} <${ServiceStatus} state=${i.isUp ? 'up' : 'down'} label="" /> ${fmtBytes(i.rx)} ${fmtBytes(i.tx)} `); const interfacesCard = html`
Network Interfaces
<${Table} columns=${['Name', 'MAC', 'IP Addresses', 'Zone', 'State', 'RX', 'TX']} rows=${ifaceRows} emptyText="No interfaces found" />
`; // ── DHCP leases ── const leaseRows = dnsmasqLeases.slice(0, 10).map(l => html` ${l.ip} ${l.mac} ${l.hostname || '—'} ${l.interface || '—'} `); const dhcpCard = html`
Active DHCP Leases (${activeLeaseCount})
<${Table} columns=${['IP', 'MAC', 'Hostname', 'Interface']} rows=${leaseRows} emptyText="No active leases" /> ${dnsmasqLeases.length > 10 ? html`

Showing 10 of ${dnsmasqLeases.length}.

` : ''}
`; // ── Proxy domains ── const proxyCard = html`
Proxy Domains
<${Badge} text=${onlineDomains} variant="success" /> Online <${Badge} text=${offlineDomains} variant=${offlineDomains > 0 ? 'danger' : 'info'} /> Offline
${proxyDomains.slice(0, 3).map(d => html`
${d.domain} → ${d.backend_name || '—'} ${d.path || '/'}
`)} ${proxyDomains.length === 0 ? html`No proxy domains configured.` : ''}
`; // ── Services ── const upCount = ifaces.filter(i => i.isUp).length; const services = html`
Services
`; // ── Side-by-side layout ── const dualCards = html`
${dhcpCard} ${proxyCard}
`; return [ PageHeader({ title: 'Dashboard', subtitle: 'System overview' }), stats, pendingCard, systemCard, interfacesCard, dualCards, services, ]; }, });