b673e87c9b
Add hoover model.js as a central reactive store per subsystem, replacing per-component data fetching with a single source of truth. - Add hoover/model.js with modelRegister, modelFetch, and WS invalidation - Refactor websocket.js to route messages to model refresh (drop per-component subscribe/unsubscribe) - Simplify component.js by removing WS subscription management - Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort - Rewrite all pages to use getModel() instead of inline data fetching - Bootstrap model registrations in app.js - Add GET /api/firewall/state endpoint - Fix restart-services.sh restart order and add service health verification - Update hoover.md docs with model layer architecture
103 lines
4.0 KiB
JavaScript
103 lines
4.0 KiB
JavaScript
import { h, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=7';
|
|
|
|
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');
|
|
modelFetch('firewall');
|
|
modelFetch('network');
|
|
} 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',
|
|
},
|
|
refresh: ['firewall', 'network'],
|
|
});
|
|
|
|
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 = fwZones.available || [];
|
|
const activeZones = fwZones.active || {};
|
|
|
|
const ifaces = Object.entries(netData).map(([name, entry]) => {
|
|
let zone = null;
|
|
for (const [zoneName, ifaces] of Object.entries(activeZones)) {
|
|
if ((ifaces || []).includes(name)) {
|
|
zone = zoneName;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
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,
|
|
config: entry?.config || {},
|
|
};
|
|
});
|
|
|
|
const rows = 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,
|
|
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',
|
|
}),
|
|
];
|
|
},
|
|
}); |