Files
vacuum-wall/webui/static/pages/interfaces.js
T
mteehan 633505e7dc refactor: modernize frontend with hoover framework components and docs
- Add quick modal, table, service status, and confirmation dialog components
- Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns
- Introduce refactor load utility and render guard for consistent UX
- Add hoover documentation and update AGENTS.md, architecture, overview
2026-06-21 04:29:27 +00:00

112 lines
4.4 KiB
JavaScript

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',
}),
];
},
});