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:
+20
-20
@@ -1,4 +1,4 @@
|
||||
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';
|
||||
import { html, 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',
|
||||
@@ -35,27 +35,27 @@ export default definePage({
|
||||
});
|
||||
|
||||
const cards = Object.entries(zoneRules).map(([zone, rules]) => {
|
||||
const ruleRows = (Array.isArray(rules) ? rules : []).map((entry, i) => {
|
||||
const ruleId = typeof entry === 'object' ? entry.id : null;
|
||||
const ruleText = typeof entry === 'object' ? (entry.rule || '') : String(entry);
|
||||
return html`<tr key=${i}>
|
||||
<td class="text-muted">${i + 1}</td>
|
||||
<td class="mono-text td-fullwidth"><${MonoText} text=${ruleText} /></td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
url=${'/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || '')}
|
||||
message=${'Remove rule: ' + ruleText.substring(0, 40) + '...?'}
|
||||
success="Rule removed"
|
||||
refresh="firewall" />
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
return Card({
|
||||
header: 'Zone: ' + esc(zone),
|
||||
key: zone,
|
||||
children: [Table({
|
||||
columns: ['#', 'Rule', 'Action'],
|
||||
rows: (Array.isArray(rules) ? rules : []).map((entry, i) => {
|
||||
const ruleId = typeof entry === 'object' ? entry.id : null;
|
||||
const ruleText = typeof entry === 'object' ? (entry.rule || '') : String(entry);
|
||||
return h('tr', { key: i },
|
||||
h('td', { class: 'text-muted' }, i + 1),
|
||||
h('td', { class: 'mono-text td-fullwidth' }, MonoText({ text: ruleText })),
|
||||
h('td', null,
|
||||
ConfirmDelete({
|
||||
url: '/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''),
|
||||
message: 'Remove rule: ' + ruleText.substring(0, 40) + '...?',
|
||||
success: 'Rule removed',
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
rows: ruleRows,
|
||||
emptyText: 'No rules',
|
||||
wrapCard: false,
|
||||
})],
|
||||
@@ -66,10 +66,10 @@ export default definePage({
|
||||
PageHeader({
|
||||
title: 'Rules',
|
||||
subtitle: 'Firewall rich rules',
|
||||
actions: h('button', { class: 'btn btn-primary',
|
||||
'on:click': () => addRule({ zones }), }, 'Add Rule'),
|
||||
actions: html`<button class="btn btn-primary"
|
||||
onClick=${() => addRule({ zones })}>Add Rule</button>`,
|
||||
}),
|
||||
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user