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:
+14
-33
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, refactorLoad, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete, ZoneSelect } from '/static/hoover/index.js?v=6';
|
||||
import { h, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=7';
|
||||
|
||||
const addFwd = QuickModal({
|
||||
title: 'Add Port Forward',
|
||||
@@ -11,7 +11,7 @@ const addFwd = QuickModal({
|
||||
],
|
||||
submit: {
|
||||
url: '/api/firewall/forward-port',
|
||||
body: (s) => ({
|
||||
body: () => ({
|
||||
zone: $val('fwd-zone'),
|
||||
port: parseInt($val('fwd-port')),
|
||||
proto: ($val('fwd-proto') || 'tcp').trim(),
|
||||
@@ -21,43 +21,23 @@ const addFwd = QuickModal({
|
||||
validate: (b) => !b.zone || !b.port || !b.proto ? 'Zone, port, and proto are required' : null,
|
||||
successMsg: 'Forward rule added',
|
||||
},
|
||||
reload: (s) => load(s._s),
|
||||
refresh: 'firewall',
|
||||
});
|
||||
|
||||
async function load(state, abortController, entry) {
|
||||
await refactorLoad(state,
|
||||
s => Object.keys(s.config || {}).length,
|
||||
async (s, sig, isAborted) => {
|
||||
const r = await apiFetch('/api/firewall/config', { signal: sig });
|
||||
if (isAborted()) return;
|
||||
if (r.ok) s.config = r.data || {};
|
||||
else s.error = r.error;
|
||||
const zr = await apiFetch('/api/firewall/zones', { signal: sig });
|
||||
if (isAborted()) return;
|
||||
if (zr.ok) s.activeZones = Object.keys(zr.data?.active || {});
|
||||
else if (!s.error) s.error = zr.error;
|
||||
const sr = await apiFetch('/api/firewall/state', { signal: sig });
|
||||
if (isAborted()) return;
|
||||
if (sr.ok) s.stateData = sr.data;
|
||||
},
|
||||
{ entry, abortController },
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage({
|
||||
init() {
|
||||
return { config: {}, activeZones: [], stateData: null };
|
||||
return {
|
||||
firewall: getModel('firewall'),
|
||||
};
|
||||
},
|
||||
subscribe: ['firewall'],
|
||||
load,
|
||||
render(state) {
|
||||
const guard = renderGuard(state, 'NAT', 'Masquerade & port forwarding', state.config);
|
||||
const guard = renderGuard(state.firewall, 'NAT', 'Masquerade & port forwarding', state.firewall.data?.config);
|
||||
if (guard) return guard;
|
||||
|
||||
const cfg = state.config || {};
|
||||
const cfg = state.firewall.data?.config || {};
|
||||
const zoneData = cfg.zones || {};
|
||||
|
||||
const sIface = (state.stateData || {}).interfaces || [];
|
||||
const sIface = (state.firewall.data?.state || {}).interfaces || [];
|
||||
const masqZones = new Set(
|
||||
Object.entries(zoneData)
|
||||
.filter(([, zcfg]) => !!zcfg.masquerade)
|
||||
@@ -94,7 +74,7 @@ export default definePage({
|
||||
labelOn: 'Disable', labelOff: 'Enable', condition: masq,
|
||||
body: () => ({ zone, enable: !masq }),
|
||||
successMsg: 'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone,
|
||||
reload: () => load(state),
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -117,7 +97,7 @@ export default definePage({
|
||||
url: '/api/firewall/forward-port/' + enc(zone) + '/' + port + '/' + enc(proto),
|
||||
message: 'Remove forward ' + zone + ':' + port + '/' + proto + '?',
|
||||
success: 'Rule removed',
|
||||
reload: () => load(state),
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
));
|
||||
@@ -148,7 +128,8 @@ export default definePage({
|
||||
Card({ children: [
|
||||
ActionGroup(
|
||||
h('button', { class: 'btn btn-sm btn-primary',
|
||||
'on:click': () => addFwd({ zones: state.activeZones, _s: state }) }, 'Add Forward'),
|
||||
'on:click': () => addFwd({ zones: Object.keys(zoneData) })
|
||||
}, 'Add Forward'),
|
||||
),
|
||||
Table({
|
||||
columns: ['Zone', 'Proto', 'Port', 'To Addr', 'To Port', 'Action'],
|
||||
@@ -159,4 +140,4 @@ export default definePage({
|
||||
]}),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user