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:
+43
-48
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=7';
|
||||
import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=7';
|
||||
|
||||
const addFwd = QuickModal({
|
||||
title: 'Add Port Forward',
|
||||
@@ -47,37 +47,34 @@ export default definePage({
|
||||
const lanIface = sIface.filter((i) => i.zone && !masqZones.has(i.zone));
|
||||
|
||||
const ifaceRows = (ifaces) =>
|
||||
ifaces.map((iface) =>
|
||||
h('tr', { key: 'ii-' + iface.name },
|
||||
h('td', null,
|
||||
h('div', { class: 'd-flex align-items-center gap-2' },
|
||||
StatusDot({ status: iface.state === 'UP' ? 'up' : 'down' }),
|
||||
h('strong', null, iface.name),
|
||||
),
|
||||
),
|
||||
h('td', null, (iface.ips || []).join(', ') || h('span', { class: 'text-muted' }, '—')),
|
||||
h('td', null, (iface.ipv6 || []).join(', ') || h('span', { class: 'text-muted' }, '—')),
|
||||
h('td', null, iface.mac || h('span', { class: 'text-muted' }, '—')),
|
||||
h('td', null, Badge({ text: iface.zone || '—', variant: 'secondary' })),
|
||||
)
|
||||
);
|
||||
ifaces.map((iface) => html`<tr key=${'ii-' + iface.name}>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<${StatusDot} status=${iface.state === 'UP' ? 'up' : 'down'} />
|
||||
<strong>${iface.name}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>${(iface.ips || []).join(', ') || html`<span class="text-muted">—</span>`}</td>
|
||||
<td>${(iface.ipv6 || []).join(', ') || html`<span class="text-muted">—</span>`}</td>
|
||||
<td>${iface.mac || html`<span class="text-muted">—</span>`}</td>
|
||||
<td><${Badge} text=${iface.zone || '—'} variant="secondary" /></td>
|
||||
</tr>`);
|
||||
|
||||
const masqRows = Object.entries(zoneData).map(([zone, zcfg]) => {
|
||||
const masq = !!zcfg.masquerade;
|
||||
return h('tr', { key: 'm-' + zone },
|
||||
h('td', null, h('strong', null, zone)),
|
||||
h('td', null, Badge({ text: masq ? 'Enabled' : 'Disabled', variant: masq ? 'success' : 'info' })),
|
||||
h('td', null,
|
||||
ActionButton({
|
||||
url: '/api/firewall/masquerade',
|
||||
cls: 'btn btn-sm btn-outline',
|
||||
labelOn: 'Disable', labelOff: 'Enable', condition: masq,
|
||||
body: () => ({ zone, enable: !masq }),
|
||||
successMsg: 'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone,
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
);
|
||||
return html`<tr key=${'m-' + zone}>
|
||||
<td><strong>${zone}</strong></td>
|
||||
<td><${Badge} text=${masq ? 'Enabled' : 'Disabled'} variant=${masq ? 'success' : 'info'} /></td>
|
||||
<td>
|
||||
<${ActionButton}
|
||||
url="/api/firewall/masquerade"
|
||||
cls="btn btn-sm btn-outline"
|
||||
labelOn="Disable" labelOff="Enable" condition=${masq}
|
||||
body=${() => ({ zone, enable: !masq })}
|
||||
successMsg=${'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone}
|
||||
refresh="firewall" />
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
const fwRows = [];
|
||||
@@ -86,21 +83,20 @@ export default definePage({
|
||||
forwards.forEach((fwd, i) => {
|
||||
const port = fwd.port;
|
||||
const proto = fwd['proxy-protocol'] || fwd.proto;
|
||||
fwRows.push(h('tr', { key: 'f-' + zone + '-' + i },
|
||||
h('td', null, h('strong', null, zone)),
|
||||
h('td', null, Badge({ text: proto || 'tcp', variant: 'info' })),
|
||||
h('td', null, port),
|
||||
h('td', null, fwd['to-addr'] || fwd.toaddr || '-'),
|
||||
h('td', null, fwd['to-port'] || fwd.toport || '-'),
|
||||
h('td', null,
|
||||
ConfirmDelete({
|
||||
url: '/api/firewall/forward-port/' + enc(zone) + '/' + port + '/' + enc(proto),
|
||||
message: 'Remove forward ' + zone + ':' + port + '/' + proto + '?',
|
||||
success: 'Rule removed',
|
||||
refresh: 'firewall',
|
||||
}),
|
||||
),
|
||||
));
|
||||
fwRows.push(html`<tr key=${'f-' + zone + '-' + i}>
|
||||
<td><strong>${zone}</strong></td>
|
||||
<td><${Badge} text=${proto || 'tcp'} variant="info" /></td>
|
||||
<td>${port}</td>
|
||||
<td>${fwd['to-addr'] || fwd.toaddr || '-'}</td>
|
||||
<td>${fwd['to-port'] || fwd.toport || '-'}</td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
url=${'/api/firewall/forward-port/' + enc(zone) + '/' + port + '/' + enc(proto)}
|
||||
message=${'Remove forward ' + zone + ':' + port + '/' + proto + '?'}
|
||||
success="Rule removed"
|
||||
refresh="firewall" />
|
||||
</td>
|
||||
</tr>`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,9 +123,8 @@ export default definePage({
|
||||
SectionTitle({ title: 'Port Forwarding' }),
|
||||
Card({ children: [
|
||||
ActionGroup(
|
||||
h('button', { class: 'btn btn-sm btn-primary',
|
||||
'on:click': () => addFwd({ zones: Object.keys(zoneData) })
|
||||
}, 'Add Forward'),
|
||||
html`<button class="btn btn-sm btn-primary"
|
||||
onClick=${() => addFwd({ zones: Object.keys(zoneData) })}>Add Forward</button>`,
|
||||
),
|
||||
Table({
|
||||
columns: ['Zone', 'Proto', 'Port', 'To Addr', 'To Port', 'Action'],
|
||||
@@ -140,4 +135,4 @@ export default definePage({
|
||||
]}),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user