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:
+12
-28
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, refactorLoad, MonoText, QuickModal } from '/static/hoover/index.js?v=6';
|
||||
import { h, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=7';
|
||||
|
||||
const addRule = QuickModal({
|
||||
title: 'Add Rich Rule',
|
||||
@@ -8,41 +8,25 @@ const addRule = QuickModal({
|
||||
],
|
||||
submit: {
|
||||
url: '/api/firewall/rich-rules',
|
||||
body: (s) => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }),
|
||||
body: () => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }),
|
||||
validate: (b) => !b.zone || !b.rule ? 'Zone and rule are required' : null,
|
||||
successMsg: '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.zones = Object.keys(zr.data?.active || {});
|
||||
else if (!s.error) s.error = zr.error;
|
||||
},
|
||||
{ entry, abortController },
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage({
|
||||
init() {
|
||||
return { config: {}, zones: [] };
|
||||
return {
|
||||
firewall: getModel('firewall'),
|
||||
};
|
||||
},
|
||||
subscribe: ['firewall'],
|
||||
load,
|
||||
render(state) {
|
||||
const guard = renderGuard(state, 'Rules', 'Firewall rich rules', state.config);
|
||||
const guard = renderGuard(state.firewall, 'Rules', 'Firewall rich rules', state.firewall.data?.config);
|
||||
if (guard) return guard;
|
||||
|
||||
const cfg = state.config || {};
|
||||
const cfg = state.firewall.data?.config || {};
|
||||
const zones = state.firewall.data?.zones?.available || [];
|
||||
const zoneData = cfg.zones || {};
|
||||
const zoneRules = {};
|
||||
Object.entries(zoneData).forEach(([zname, zcfg]) => {
|
||||
@@ -67,7 +51,7 @@ export default definePage({
|
||||
url: '/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''),
|
||||
message: 'Remove rule: ' + ruleText.substring(0, 40) + '...?',
|
||||
success: 'Rule removed',
|
||||
reload: () => load(state),
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -83,9 +67,9 @@ export default definePage({
|
||||
title: 'Rules',
|
||||
subtitle: 'Firewall rich rules',
|
||||
actions: h('button', { class: 'btn btn-primary',
|
||||
'on:click': () => addRule({ zones: state.zones, _s: state }), }, 'Add Rule'),
|
||||
'on:click': () => addRule({ zones }), }, 'Add Rule'),
|
||||
}),
|
||||
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user