import { h, PageHeader, Table, renderGuard, enc, $val, apiFetch, toast, definePage, refactorLoad, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=6'; 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'); await load(state); } 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', }, reload: (s) => load(s), }); async function load(state, abortController, entry) { await refactorLoad(state, s => s.ifaces?.length, async (s, sig, isAborted) => { const [fw, net] = await Promise.all([ apiFetch('/api/firewall/zones', { signal: sig }), apiFetch('/api/network/interfaces', { signal: sig }), ]); if (isAborted()) return; if (fw.ok) s.zones = fw.data?.available || []; else s.error = fw.error; if (net.ok) { const ifaceZone = {}; for (const [zoneName, ifaces] of Object.entries(fw.data?.active || {})) { for (const name of (ifaces || [])) ifaceZone[name] = zoneName; } const ifacesObj = net.data?.interfaces || {}; s.ifaces = Object.entries(ifacesObj).map(([name, entry]) => ({ name, mac: entry?.runtime?.mac || null, ips: [...(entry?.config?.addresses || []), ...(entry?.runtime?.addresses || [])], state: (entry?.runtime?.state || '').startsWith('routable') || (entry?.runtime?.state || '').startsWith('carrier') ? 'up' : 'down', zone: ifaceZone[name] || null, config: entry?.config || {}, })); } else if (!s.error) { s.error = net.error; } }, { entry, abortController }, ); } export default definePage({ init() { return { ifaces: [], zones: [] }; }, subscribe: ['firewall', 'networkd'], load, render(state) { const guard = renderGuard(state, 'Interfaces', 'Network interface management', state.ifaces.length); if (guard) return guard; const rows = state.ifaces.map(iface => { return h('tr', { key: iface.name }, h('td', null, h('strong', null, iface.name)), h('td', { class: 'text-muted' }, String(iface.mac || 'N/A')), h('td', null, (iface.ips || []).join(', ') || 'N/A'), h('td', null, StatusText({ status: iface.state })), h('td', null, ZoneSelect({ zones: state.zones, value: iface.zone, onChange: (z) => changeZone(iface.name, z, state), }), h('button', { class: 'btn btn-sm btn-outline', style: 'margin-left:8px', 'on:click': () => cfgModalFn(iface), }, 'Config'), ), ); }); return [ PageHeader({ title: 'Interfaces', subtitle: 'Network interface management' }), Table({ columns: ['Name', 'MAC', 'IPs', 'State', 'Zone / Actions'], rows, emptyText: 'No interfaces found', }), ]; }, });