Add state management, WebSocket polling, html.js templating, and refactor pages

- lib/state.py: per-subsystem collectors with versioned state store
- daemon/server.py: state refresh on request, batch routing updates
- webui/static/hoover/html.js: new html tag template helper via htm.js
- webui/static/hoover/websocket.js: real-time state change notifications
- webui/static/hoover/vdom.js: VDOM improvements for keyed diff
- All frontend pages refactored to use html templates
- Add tests for state management and polling
- Update docs and AGENTS.md
This commit is contained in:
2026-06-23 21:11:45 +00:00
parent 5025dfaf30
commit 5ba0f31767
26 changed files with 1193 additions and 495 deletions
+17 -19
View File
@@ -1,4 +1,4 @@
import { h, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, certStatusBadge, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
import { html, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, certStatusBadge, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
const addDomain = QuickModal({
title: 'Add Proxy Domain',
@@ -66,26 +66,24 @@ export default definePage({
expired: d.cert_status === 'expired',
});
return h('tr', { key: d.domain },
h('td', null, h('strong', null, esc(d.domain))),
h('td', null, esc(d.backend_host || '-')),
h('td', null, d.backend_port || '-'),
h('td', null, Badge({ text: d.backend_proto || d.protocol || 'http', variant: 'info' })),
h('td', null, certBadge),
ActionCell({
editLabel: 'Edit',
editClick: () => editDomain(d),
removeUrl: '/api/proxy/domains/' + enc(d.domain),
removeMessage: 'Remove proxy for ' + d.domain + '?',
removeSuccess: 'Domain removed',
removeRefresh: ['nginx', 'acme'],
removeLabel: 'Delete',
}),
);
return html`<tr key=${d.domain}>
<td><strong>${esc(d.domain)}</strong></td>
<td>${esc(d.backend_host || '-')}</td>
<td>${d.backend_port || '-'}</td>
<td><${Badge} text=${d.backend_proto || d.protocol || 'http'} variant="info" /></td>
<td>${certBadge}</td>
<${ActionCell}
editLabel="Edit" editClick=${() => editDomain(d)}
removeUrl=${'/api/proxy/domains/' + enc(d.domain)}
removeMessage=${'Remove proxy for ' + d.domain + '?'}
removeSuccess="Domain removed"
removeRefresh={['nginx', 'acme']}
removeLabel="Delete" />
</tr>`;
});
const actions = ActionGroup(
h('button', { class: 'btn btn-primary', 'on:click': () => addDomain(state) }, 'Add Domain'),
html`<button class="btn btn-primary" onClick=${() => addDomain(state)}>Add Domain</button>`,
ActionButton({
url: '/api/proxy/apply',
successMsg: 'Nginx applied & reloaded',
@@ -104,4 +102,4 @@ export default definePage({
: Empty({ text: 'No proxy domains configured. Add a domain to start terminating SSL.' }),
];
},
});
});