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, Badge, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, refactorLoad, ActionCell, certStatusBadge, poll } from '/static/hoover/index.js?v=6';
|
||||
import { h, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll } from '/static/hoover/index.js?v=7';
|
||||
|
||||
function issueCertModal(state) {
|
||||
openModal((inner, idx) => {
|
||||
@@ -40,7 +40,7 @@ async function pollCertIssue(rid, state) {
|
||||
onErrorKey: (d) => d.status === 'failed',
|
||||
onComplete: (d) => {
|
||||
toast('Certificate issued for ' + (d.domain || rid), 'success');
|
||||
load(state);
|
||||
modelFetch('acme');
|
||||
},
|
||||
onError: (d) => {
|
||||
toast('Issuance failed: ' + (d?.error || 'unknown'), 'error');
|
||||
@@ -48,30 +48,17 @@ async function pollCertIssue(rid, state) {
|
||||
});
|
||||
}
|
||||
|
||||
async function load(state, abortController, entry) {
|
||||
await refactorLoad(state,
|
||||
s => s.certs?.length,
|
||||
async (s, sig, isAborted) => {
|
||||
const r = await apiFetch('/api/certs/list', { signal: sig });
|
||||
if (isAborted()) return;
|
||||
if (r.ok) s.certs = r.data || [];
|
||||
else s.error = r.error;
|
||||
},
|
||||
{ entry, abortController },
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage({
|
||||
init() {
|
||||
return { certs: [] };
|
||||
return {
|
||||
acme: getModel('acme'),
|
||||
};
|
||||
},
|
||||
subscribe: ['acme'],
|
||||
load,
|
||||
render(state) {
|
||||
const guard = renderGuard(state, 'Certificates', 'ACME certificate management', state.certs);
|
||||
const guard = renderGuard(state.acme, 'Certificates', 'ACME certificate management', state.acme.data);
|
||||
if (guard) return guard;
|
||||
|
||||
const rows = state.certs.map(c => {
|
||||
const rows = (state.acme.data || []).map(c => {
|
||||
const badge = certStatusBadge({ expired: c.expired, daysRemaining: c.days_remaining });
|
||||
|
||||
return h('tr', { key: c.domain },
|
||||
@@ -89,7 +76,7 @@ export default definePage({
|
||||
removeUrl: '/api/certs/' + enc(c.domain),
|
||||
removeMessage: 'Remove certificate for ' + c.domain + '?',
|
||||
removeSuccess: 'Certificate removed',
|
||||
removeReload: () => load(state),
|
||||
removeRefresh: 'acme',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -109,4 +96,4 @@ export default definePage({
|
||||
: Empty({ text: 'No certificates found. Issue a certificate to get started.' }),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user