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:
+41
-44
@@ -1,4 +1,4 @@
|
||||
import { h, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=7';
|
||||
import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=7';
|
||||
|
||||
const addZone = QuickModal({
|
||||
title: 'Add Zone',
|
||||
@@ -37,30 +37,30 @@ export default definePage({
|
||||
const z = typeof zdata === 'object' ? zdata : {};
|
||||
const ifacesArr = Array.isArray(z.interfaces) ? z.interfaces : [];
|
||||
const svcsArr = Array.isArray(z.services) ? z.services : [];
|
||||
return h('div', { class: 'card', key: name, style: 'position:relative;' },
|
||||
h('div', { style: 'display:flex;justify-content:space-between;align-items:flex-start;' },
|
||||
h('div', null,
|
||||
h('h3', { style: 'font-size:16px;color:var(--accent);' }, name),
|
||||
h('div', { class: 'text-muted text-sm', style: 'margin-bottom:10px;' },
|
||||
z.target ? 'Target: ' + esc(z.target) : '',
|
||||
),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'text-sm mb-4' },
|
||||
h('div', { class: 'text-muted', style: 'margin-bottom:4px;' }, 'Interfaces'),
|
||||
ifacesArr.length
|
||||
? ifacesArr.map(i => Badge({ text: esc(i) }))
|
||||
: h('span', { class: 'text-muted' }, 'None'),
|
||||
),
|
||||
h('div', { class: 'text-sm mb-4' },
|
||||
h('div', { class: 'text-muted', style: 'margin-bottom:4px;' }, 'Services'),
|
||||
svcsArr.length
|
||||
? svcsArr.map(s => Badge({ text: esc(s), variant: 'success' }))
|
||||
: h('span', { class: 'text-muted' }, 'None'),
|
||||
),
|
||||
h('div', { style: 'display:flex;gap:6px;' },
|
||||
h('button', { class: 'btn btn-sm btn-outline',
|
||||
'on:click': () => MultiSelectModal({
|
||||
return html`<div class="card" key=${name} style="position:relative">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start">
|
||||
<div>
|
||||
<h3 style="font-size:16px;color:var(--accent)">${name}</h3>
|
||||
<div class="text-muted text-sm" style="margin-bottom:10px">
|
||||
${z.target ? 'Target: ' + esc(z.target) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm mb-4">
|
||||
<div class="text-muted" style="margin-bottom:4px">Interfaces</div>
|
||||
${ifacesArr.length
|
||||
? ifacesArr.map(i => html`<${Badge} text=${esc(i)} />`)
|
||||
: html`<span class="text-muted">None</span>`}
|
||||
</div>
|
||||
<div class="text-sm mb-4">
|
||||
<div class="text-muted" style="margin-bottom:4px">Services</div>
|
||||
${svcsArr.length
|
||||
? svcsArr.map(s => html`<${Badge} text=${esc(s)} variant="success" />`)
|
||||
: html`<span class="text-muted">None</span>`}
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
onClick=${() => MultiSelectModal({
|
||||
title: 'Interfaces: ' + name,
|
||||
url: '/api/firewall/zones/' + enc(name) + '/interfaces',
|
||||
options: state.firewall.data?.interfaces || [],
|
||||
@@ -68,10 +68,9 @@ export default definePage({
|
||||
fieldKey: 'interfaces',
|
||||
successMsg: 'Interfaces updated',
|
||||
refresh: 'firewall',
|
||||
})(),
|
||||
}, 'Interfaces'),
|
||||
h('button', { class: 'btn btn-sm btn-outline',
|
||||
'on:click': () => MultiSelectModal({
|
||||
})()}>Interfaces</button>
|
||||
<button class="btn btn-sm btn-outline"
|
||||
onClick=${() => MultiSelectModal({
|
||||
title: 'Services: ' + name,
|
||||
url: '/api/firewall/zones/' + enc(name) + '/services',
|
||||
options: state.firewall.data?.services || [],
|
||||
@@ -79,29 +78,27 @@ export default definePage({
|
||||
fieldKey: 'services',
|
||||
successMsg: 'Services updated',
|
||||
refresh: 'firewall',
|
||||
})(),
|
||||
}, 'Services'),
|
||||
ConfirmDelete({
|
||||
url: '/api/firewall/zones/' + enc(name),
|
||||
message: 'Delete zone ' + name + '?',
|
||||
success: 'Zone ' + name + ' deleted',
|
||||
refresh: 'firewall',
|
||||
label: 'Delete',
|
||||
}),
|
||||
),
|
||||
);
|
||||
})()}>Services</button>
|
||||
<${ConfirmDelete}
|
||||
url=${'/api/firewall/zones/' + enc(name)}
|
||||
message=${'Delete zone ' + name + '?'}
|
||||
success=${'Zone ' + name + ' deleted'}
|
||||
refresh="firewall"
|
||||
label="Delete" />
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return [
|
||||
PageHeader({
|
||||
title: 'Zones',
|
||||
subtitle: 'Firewall zones',
|
||||
actions: h('button', { class: 'btn btn-primary',
|
||||
'on:click': () => addZone(), }, 'Add Zone'),
|
||||
actions: html`<button class="btn btn-primary"
|
||||
onClick=${() => addZone()}>Add Zone</button>`,
|
||||
}),
|
||||
zoneCards.length
|
||||
? h('div', { class: 'card-grid' }, ...zoneCards)
|
||||
? html`<div class="card-grid">${zoneCards}</div>`
|
||||
: Empty({ text: 'No zones configured. Add a zone to get started.' }),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user