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
+23 -26
View File
@@ -1,4 +1,4 @@
import { h, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob } from '/static/hoover/index.js?v=7';
import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob } from '/static/hoover/index.js?v=7';
const addPeer = QuickModal({
title: 'Add WireGuard Peer',
@@ -66,33 +66,30 @@ export default definePage({
const peerRows = (state.wireguard.data?.peers || []).map(p => {
const hasHandshake = !!p.latest_handshake;
return h('tr', { key: p.name },
h('td', null,
StatusDot({ status: hasHandshake ? 'success' : 'danger' }),
h('strong', null, esc(p.name || 'unnamed')),
),
h('td', null, MonoText({ text: p.public_key || 'N/A', maxLength: 20 })),
h('td', { class: 'text-sm' }, esc(p.allowed_ips || '-')),
h('td', { class: 'text-sm' }, esc(p.endpoint || '-')),
h('td', { class: 'text-sm' }, esc(p.latest_handshake || 'Never')),
h('td', { class: 'text-sm' },
'Recv: ' + esc(p.transfer_recv || '0'),
h('br'),
'Sent: ' + esc(p.transfer_sent || '0'),
),
ActionCell({
editLabel: 'Config',
editClick: () => downloadConfigModal(p.name, state.wireguard.data?.config, state),
removeUrl: '/api/wireguard/peers/' + enc(p.name),
removeMessage: 'Remove peer ' + p.name + '?',
removeSuccess: 'Peer removed',
removeRefresh: 'wireguard',
}),
);
return html`<tr key=${p.name}>
<td>
<${StatusDot} status=${hasHandshake ? 'success' : 'danger'} />
<strong>${esc(p.name || 'unnamed')}</strong>
</td>
<td><${MonoText} text=${p.public_key || 'N/A'} maxLength=20 /></td>
<td class="text-sm">${esc(p.allowed_ips || '-')}</td>
<td class="text-sm">${esc(p.endpoint || '-')}</td>
<td class="text-sm">${esc(p.latest_handshake || 'Never')}</td>
<td class="text-sm">
Recv: ${esc(p.transfer_recv || '0')}<br/>
Sent: ${esc(p.transfer_sent || '0')}
</td>
<${ActionCell}
editLabel="Config" editClick=${() => downloadConfigModal(p.name, state.wireguard.data?.config, state)}
removeUrl=${'/api/wireguard/peers/' + enc(p.name)}
removeMessage=${'Remove peer ' + p.name + '?'}
removeSuccess="Peer removed"
removeRefresh="wireguard" />
</tr>`;
});
const actions = ActionGroup(
h('button', { class: 'btn btn-primary', 'on:click': () => addPeer(state) }, 'Add Peer'),
html`<button class="btn btn-primary" onClick=${() => addPeer(state)}>Add Peer</button>`,
ActionButton({
url: '/api/wireguard/' + (isUp ? 'down' : 'up'),
labelOn: 'Stop', labelOff: 'Start', condition: isUp,
@@ -122,4 +119,4 @@ export default definePage({
: Empty({ text: 'No peers configured. Add a peer above.' }),
];
},
});
});