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:
+55
-52
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Badge, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
|
||||
import { html, PageHeader, Badge, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
|
||||
|
||||
const addRange = QuickModal({
|
||||
title: 'Add DHCP Range',
|
||||
@@ -74,54 +74,51 @@ export default definePage({
|
||||
const dnsRecords = cfg.dns_records || [];
|
||||
const status = state.dnsmasq.data?.status || {};
|
||||
|
||||
const rangesRows = ranges.map((r) => h('tr', { key: (r.interface || '_g') + '-' + r.start + '-' + r.end },
|
||||
h('td', null, r.interface || '(global)'),
|
||||
h('td', null, esc(r.start)),
|
||||
h('td', null, esc(r.end)),
|
||||
h('td', null, esc(r.lease_time || '12h')),
|
||||
h('td', null,
|
||||
ConfirmDelete({
|
||||
url: '/api/dhcp/ranges',
|
||||
message: 'Remove range ' + r.start + ' - ' + r.end + '?',
|
||||
body: { interface: r.interface || '', start: r.start, end: r.end },
|
||||
success: 'Range removed',
|
||||
refresh: 'dnsmasq',
|
||||
}),
|
||||
),
|
||||
));
|
||||
const rangesRows = ranges.map((r) => html`<tr key=${(r.interface || '_g') + '-' + r.start + '-' + r.end}>
|
||||
<td>${r.interface || '(global)'}</td>
|
||||
<td>${esc(r.start)}</td>
|
||||
<td>${esc(r.end)}</td>
|
||||
<td>${esc(r.lease_time || '12h')}</td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
url="/api/dhcp/ranges"
|
||||
message=${'Remove range ' + r.start + ' - ' + r.end + '?'}
|
||||
body=${{ interface: r.interface || '', start: r.start, end: r.end }}
|
||||
success="Range removed"
|
||||
refresh="dnsmasq" />
|
||||
</td>
|
||||
</tr>`);
|
||||
|
||||
const leaseRows = staticLeases.map((l) => h('tr', { key: l.mac },
|
||||
h('td', null, esc(l.mac)),
|
||||
h('td', null, esc(l.ip)),
|
||||
h('td', null, l.hostname || '-'),
|
||||
h('td', null,
|
||||
ConfirmDelete({
|
||||
url: '/api/dhcp/static-lease/' + enc(l.mac),
|
||||
message: 'Remove lease ' + l.mac + '?',
|
||||
success: 'Lease removed',
|
||||
refresh: 'dnsmasq',
|
||||
}),
|
||||
),
|
||||
));
|
||||
const leaseRows = staticLeases.map((l) => html`<tr key=${l.mac}>
|
||||
<td>${esc(l.mac)}</td>
|
||||
<td>${esc(l.ip)}</td>
|
||||
<td>${l.hostname || '-'}</td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
url=${'/api/dhcp/static-lease/' + enc(l.mac)}
|
||||
message=${'Remove lease ' + l.mac + '?'}
|
||||
success="Lease removed"
|
||||
refresh="dnsmasq" />
|
||||
</td>
|
||||
</tr>`);
|
||||
|
||||
const dnsRows = dnsRecords.map((rec) => h('tr', { key: rec.name },
|
||||
h('td', null, h('strong', null, esc(rec.name || 'unnamed'))),
|
||||
h('td', { class: 'text-sm' }, esc(rec.address || '-')),
|
||||
h('td', null,
|
||||
ConfirmDelete({
|
||||
url: '/api/dhcp/dns-record/' + enc(rec.name || ''),
|
||||
message: 'Remove DNS record ' + (rec.name || 'unnamed') + '?',
|
||||
success: 'Record removed',
|
||||
refresh: 'dnsmasq',
|
||||
}),
|
||||
),
|
||||
));
|
||||
const dnsRows = dnsRecords.map((rec) => html`<tr key=${rec.name}>
|
||||
<td><strong>${esc(rec.name || 'unnamed')}</strong></td>
|
||||
<td class="text-sm">${esc(rec.address || '-')}</td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
url=${'/api/dhcp/dns-record/' + enc(rec.name || '')}
|
||||
message=${'Remove DNS record ' + (rec.name || 'unnamed') + '?'}
|
||||
success="Record removed"
|
||||
refresh="dnsmasq" />
|
||||
</td>
|
||||
</tr>`);
|
||||
|
||||
const tabNames = ['ranges', 'leases', 'dns', 'active'];
|
||||
const actions = ActionGroup(
|
||||
h('button', { class: 'btn btn-primary', 'on:click': () => addRange(state) }, 'Add Range'),
|
||||
h('button', { class: 'btn btn-outline', 'on:click': () => addLease(state) }, 'Static Lease'),
|
||||
h('button', { class: 'btn btn-outline', 'on:click': () => addDns(state) }, 'DNS Record'),
|
||||
html`<button class="btn btn-primary" onClick=${() => addRange(state)}>Add Range</button>`,
|
||||
html`<button class="btn btn-outline" onClick=${() => addLease(state)}>Static Lease</button>`,
|
||||
html`<button class="btn btn-outline" onClick=${() => addDns(state)}>DNS Record</button>`,
|
||||
ActionButton({
|
||||
url: '/api/dhcp/apply',
|
||||
successMsg: 'dnsmasq applied',
|
||||
@@ -130,6 +127,18 @@ export default definePage({
|
||||
}),
|
||||
);
|
||||
|
||||
const leaseTable = state.activeTab === 'active'
|
||||
? Table({
|
||||
columns: ['MAC', 'IP', 'Hostname', 'Expires'],
|
||||
rows: (state.dnsmasq.data?.leases || []).map((l) => html`<tr key=${l.mac || l.ip}>
|
||||
<td>${esc(l.mac || '-')}</td>
|
||||
<td>${esc(l.ip || '-')}</td>
|
||||
<td>${esc(l.hostname || '-')}</td>
|
||||
<td>${esc(l.expires || '-')}</td>
|
||||
</tr>`),
|
||||
emptyText: 'No active leases',
|
||||
}) : null;
|
||||
|
||||
return [
|
||||
PageHeader({ title: 'DHCP & DNS', subtitle: 'Dnsmasq management', actions }),
|
||||
ServiceStatus({ state: status.state || 'down', label: 'Dnsmasq' }),
|
||||
@@ -140,13 +149,7 @@ export default definePage({
|
||||
? Table({ columns: ['MAC', 'IP', 'Hostname', 'Action'], rows: leaseRows, emptyText: 'No static leases' }) : null,
|
||||
state.activeTab === 'dns'
|
||||
? Table({ columns: ['Name', 'Address', 'Action'], rows: dnsRows, emptyText: 'No custom DNS records' }) : null,
|
||||
state.activeTab === 'active'
|
||||
? Table({ columns: ['MAC', 'IP', 'Hostname', 'Expires'], rows: (state.dnsmasq.data?.leases || []).map((l) => h('tr', { key: l.mac || l.ip },
|
||||
h('td', null, esc(l.mac || '-')),
|
||||
h('td', null, esc(l.ip || '-')),
|
||||
h('td', null, esc(l.hostname || '-')),
|
||||
h('td', null, esc(l.expires || '-')),
|
||||
)), emptyText: 'No active leases' }) : null,
|
||||
leaseTable,
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user