import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js'; async function changeZone(name, zone, state) { const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interfaces: [name] }), }); if (r.ok) { toast(name + ' \u2192 ' + zone, 'success'); await load(state); } else { toast(r.error || 'Failed', 'error'); } } function cfgModal(name, state) { openModal((inner, idx) => { formModal(inner, 'Config: ' + name, [ { label: 'Addresses (comma-separated)', id: 'cfg-addrs', placeholder: '192.168.1.1/24' }, { label: 'Gateway', id: 'cfg-gw' }, { label: 'DNS (comma-separated)', id: 'cfg-dns', placeholder: '1.1.1.1, 8.8.8.8' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Save', cls: 'btn-primary', action: 's', handler: async () => { const 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), }; const r = await apiFetch('/api/network/interfaces/' + enc(name), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (r.ok) { toast('Config saved', 'success'); closeModal(idx); await load(state); } else { toast(r.error || 'Failed', 'error'); } }, }, ], ); }); } async function load(state) { try { const [fw, net] = await Promise.all([ apiFetch('/api/firewall/zones'), apiFetch('/api/network/interfaces'), ]); // Extract zone names from available zones (for the dropdown) state.zones = fw.ok ? (fw.data?.available || []) : []; if (net.ok) { // Build reverse zone map: interface name → zone name, from active zones const ifaceZone = {}; for (const [zoneName, ifaces] of Object.entries(fw.data?.active || {})) { for (const name of (ifaces || [])) ifaceZone[name] = zoneName; } // Transform { interfaces: { name: { config, runtime } }, timestamp } // → array of { name, mac, ips, state, zone } const ifacesObj = net.data?.interfaces || {}; state.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, })); } else { state.error = net.error; } } catch (e) { state.error = String(e); } state.loading = false; } export default definePage({ init() { return { ifaces: [], zones: [], loading: true, error: null }; }, subscribe: ['firewall', 'networkd'], load, render(state) { if (state.loading) { return [ PageHeader({ title: 'Interfaces' }), h('div', { class: 'card', key: 'loading' }, h('div', { class: 'card-body loading' }, 'Loading...'), ), ]; } if (state.error) { return [ PageHeader({ title: 'Interfaces' }), h('div', { class: 'card', key: 'error' }, h('div', { class: 'card-body error-msg' }, state.error), ), ]; } 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, StatusDot({ status: iface.state }), ' ' + (iface.state === 'up' ? 'Up' : 'Down'), ), h('td', null, h('select', { 'on:change': (e) => changeZone(iface.name, e.target.value, state), }, state.zones.map(z => h('option', { value: z, selected: z === iface.zone }, z), )), h('button', { class: 'btn btn-sm btn-outline', style: 'margin-left:8px', 'on:click': () => cfgModal(iface.name, state), }, 'Config'), ), ); }); return [ PageHeader({ title: 'Interfaces', subtitle: 'Network interface management' }), h('div', { class: 'card' }, h('table', { class: 'table' }, h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'MAC'), h('th', null, 'IPs'), h('th', null, 'State'), h('th', null, 'Zone / Actions'), ), ), h('tbody', null, ...(rows.length ? rows : [ h('tr', null, h('td', { colspan: 5, class: 'text-muted text-sm' }, 'No interfaces found'), ), ]), ), ), ), ]; }, });