import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js'; async function changeZone(name, zone, state) { const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', { method: 'POST', body: { interfaces: [name] }, }); if (r.ok) { toast(name + ' \u2192 ' + zone, 'success'); // No modelFetch — daemon broadcasts both subsystems via WS delta. } else { toast(r.error || 'Failed', 'error'); } } const cfgModalFn = QuickModal({ title: (d) => 'Config: ' + d.name, fields: (d) => { const cfg = d.config || {}; return [ { label: 'Addresses (comma-separated)', id: 'cfg-addrs', value: (cfg.addresses || []).join(', '), placeholder: '192.168.1.1/24' }, { label: 'Gateway', id: 'cfg-gw', value: cfg.gateway || '' }, { label: 'DNS (comma-separated)', id: 'cfg-dns', value: (cfg.dns || []).join(', '), placeholder: '1.1.1.1, 8.8.8.8' }, ]; }, submit: { url: (d) => '/api/network/interfaces/' + enc(d.name), body: () => ({ addresses: ($val('cfg-addrs') || '').split(',').map(s => s.trim()).filter(Boolean), gateway: ($val('cfg-gw') || '').trim() || undefined, dns: ($val('cfg-dns') || '').split(',').map(s => s.trim()).filter(Boolean), }), successMsg: 'Config saved', }, }); export default definePage({ init() { return { firewall: getModel('firewall'), network: getModel('network'), }; }, render(state) { const guard = renderGuardMulti('Interfaces', 'Network interface management', state.firewall, state.network); if (guard) return guard; const fwZones = state.firewall.data?.zones || {}; const netData = state.network.data?.interfaces || {}; const zones = Object.keys(fwZones); const activeZones = state.firewall.data?.active_zones || {}; // Per-interface config lives in the top-level config (flat runtime // entries carry no per-interface config). const netCfgIfaces = state.network.data?.config?.interfaces || {}; // Loopback has no networkd config to manage — show real NICs only. const ifaces = Object.entries(netData).filter(([name]) => name !== 'lo').map(([name, entry]) => { let zone = null; for (const [zoneName, zIfaces] of Object.entries(activeZones)) { if ((zIfaces || []).includes(name)) { zone = zoneName; break; } } return { name, mac: entry?.mac || null, ips: [...(netCfgIfaces[name]?.addresses || []), ...(entry?.addresses || [])], state: (entry?.state || '').startsWith('routable') || (entry?.state || '').startsWith('carrier') ? 'up' : 'down', zone, config: netCfgIfaces[name] || {}, }; }); const rows = ifaces.map(iface => html` ${iface.name} ${String(iface.mac || 'N/A')} ${(iface.ips || []).join(', ') || 'N/A'} <${StatusText} status=${iface.state} /> <${ZoneSelect} zones=${zones} value=${iface.zone} onChange=${(z) => changeZone(iface.name, z, state)} /> ` ); return [ PageHeader({ title: 'Interfaces', subtitle: 'Network interface management' }), Table({ columns: ['Name', 'MAC', 'IPs', 'State', 'Zone / Actions'], rows, emptyText: 'No interfaces found', }), ]; }, });