refactor: introduce model layer for centralized data synchronization
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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Table, renderGuard, enc, $val, apiFetch, toast, definePage, refactorLoad, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=6';
|
||||
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', {
|
||||
@@ -7,7 +7,8 @@ async function changeZone(name, zone, state) {
|
||||
});
|
||||
if (r.ok) {
|
||||
toast(name + ' \u2192 ' + zone, 'success');
|
||||
await load(state);
|
||||
modelFetch('firewall');
|
||||
modelFetch('network');
|
||||
} else {
|
||||
toast(r.error || 'Failed', 'error');
|
||||
}
|
||||
@@ -32,53 +33,44 @@ const cfgModalFn = QuickModal({
|
||||
}),
|
||||
successMsg: 'Config saved',
|
||||
},
|
||||
reload: (s) => load(s),
|
||||
refresh: ['firewall', 'network'],
|
||||
});
|
||||
|
||||
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: [] };
|
||||
return {
|
||||
firewall: getModel('firewall'),
|
||||
network: getModel('network'),
|
||||
};
|
||||
},
|
||||
subscribe: ['firewall', 'networkd'],
|
||||
load,
|
||||
render(state) {
|
||||
const guard = renderGuard(state, 'Interfaces', 'Network interface management', state.ifaces.length);
|
||||
const guard = renderGuardMulti('Interfaces', 'Network interface management', state.firewall, state.network);
|
||||
if (guard) return guard;
|
||||
|
||||
const rows = state.ifaces.map(iface => {
|
||||
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')),
|
||||
@@ -86,7 +78,7 @@ export default definePage({
|
||||
h('td', null, StatusText({ status: iface.state })),
|
||||
h('td', null,
|
||||
ZoneSelect({
|
||||
zones: state.zones,
|
||||
zones,
|
||||
value: iface.zone,
|
||||
onChange: (z) => changeZone(iface.name, z, state),
|
||||
}),
|
||||
@@ -108,4 +100,4 @@ export default definePage({
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user