refactor: modernize frontend with hoover framework components and docs
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
This commit is contained in:
@@ -1,55 +1,27 @@
|
||||
import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js';
|
||||
import { h, PageHeader, apiFetch, definePage, renderGuard, refactorLoad, ServiceStatus, StatCard } from '/static/hoover/index.js?v=6';
|
||||
|
||||
export default definePage({
|
||||
init() {
|
||||
return { data: null, loading: true, refreshing: false, error: null };
|
||||
return { data: null };
|
||||
},
|
||||
subscribe: ['*'],
|
||||
subscribe: ['firewall', 'dnsmasq', 'wireguard', 'acme', 'networkd'],
|
||||
async load(state, abortController, entry) {
|
||||
const myId = entry ? entry.requestId : 0;
|
||||
if (state.data) state.refreshing = true;
|
||||
else state.loading = true;
|
||||
try {
|
||||
const res = await apiFetch('/api/status/all', { signal: abortController?.signal });
|
||||
if (abortController?.signal.aborted || entry.requestId !== myId) return;
|
||||
if (res.ok) state.data = res.data;
|
||||
else state.error = res.error;
|
||||
} catch (e) {
|
||||
if (abortController?.signal.aborted) return;
|
||||
state.error = String(e);
|
||||
}
|
||||
state.loading = false;
|
||||
state.refreshing = false;
|
||||
await refactorLoad(state,
|
||||
s => s.data,
|
||||
async (s, sig, isAborted) => {
|
||||
const res = await apiFetch('/api/status/all', { signal: sig });
|
||||
if (isAborted()) return;
|
||||
if (res.ok) s.data = res.data;
|
||||
else s.error = res.error;
|
||||
},
|
||||
{ entry, abortController },
|
||||
);
|
||||
},
|
||||
render(state) {
|
||||
if (state.loading && !state.refreshing) {
|
||||
return [
|
||||
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
||||
h('div', { class: 'card', key: 'loading' },
|
||||
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
return [
|
||||
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
||||
h('div', { class: 'card', key: 'error' },
|
||||
h('div', { class: 'card-body error-msg' }, state.error),
|
||||
),
|
||||
];
|
||||
}
|
||||
const guard = renderGuard(state, 'Dashboard', 'System overview', state.data);
|
||||
if (guard) return guard;
|
||||
|
||||
const d = state.data;
|
||||
if (!d) {
|
||||
return [
|
||||
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
||||
h('div', { class: 'card', key: 'no-data' },
|
||||
h('div', { class: 'card-body loading' }, 'No data available'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const fwZones = (d.firewall?.zones) || {};
|
||||
const net = d.net || {};
|
||||
const nCount = Object.keys(net).length;
|
||||
@@ -58,49 +30,39 @@ export default definePage({
|
||||
const certs = d.certs || [];
|
||||
const certW = certs.filter(c => c.expired || c.days_remaining <= 30);
|
||||
const dmsk = d.dnsmasq?.status || {};
|
||||
const dmskUp = dmsk.state === 'up';
|
||||
const wUp = (d.wg?.state || 'down') === 'up';
|
||||
const wP = (d.wg || {}).peers || [];
|
||||
|
||||
return [
|
||||
PageHeader({ title: 'Dashboard', subtitle: 'System overview' }),
|
||||
h('div', { class: 'grid grid-4' },
|
||||
h('div', { class: 'stat-card' },
|
||||
h('div', { class: 'label' }, 'Active Zones'),
|
||||
h('div', { class: 'value' }, Object.keys(fwZones).length),
|
||||
h('div', { class: 'meta' }, Object.keys(fwZones).join(', ') || 'None'),
|
||||
),
|
||||
h('div', { class: 'stat-card' },
|
||||
h('div', { class: 'label' }, 'Interfaces Up'),
|
||||
h('div', { class: 'value' }, upC + '/' + nCount),
|
||||
h('div', { class: 'meta' }, upI.map(i => i.name).join(', ') || 'None up'),
|
||||
),
|
||||
h('div', { class: 'stat-card' },
|
||||
h('div', { class: 'label' }, 'WireGuard'),
|
||||
h('div', { class: 'value' }, String(d.wg?.state || 'unknown')),
|
||||
h('div', { class: 'meta' }, wP.length + ' peers'),
|
||||
),
|
||||
h('div', { class: 'stat-card' },
|
||||
h('div', { class: 'label' }, 'Certificates'),
|
||||
h('div', { class: 'value' }, certs.length),
|
||||
h('div', { class: 'meta' }, certW.length + ' expiring/expired'),
|
||||
),
|
||||
StatCard({
|
||||
label: 'Active Zones',
|
||||
value: Object.keys(fwZones).length,
|
||||
meta: Object.keys(fwZones).join(', ') || 'None',
|
||||
}),
|
||||
StatCard({
|
||||
label: 'Interfaces Up',
|
||||
value: upC + '/' + nCount,
|
||||
meta: upI.map(i => i.name).join(', ') || 'None up',
|
||||
}),
|
||||
StatCard({
|
||||
label: 'WireGuard',
|
||||
value: String(d.wg?.state || 'unknown'),
|
||||
meta: wP.length + ' peers',
|
||||
}),
|
||||
StatCard({
|
||||
label: 'Certificates',
|
||||
value: certs.length,
|
||||
meta: certW.length + ' expiring/expired',
|
||||
}),
|
||||
),
|
||||
h('div', { class: 'grid grid-2' },
|
||||
h('div', { class: 'card' },
|
||||
h('div', { class: 'card-header' }, 'Services'),
|
||||
h('div', { class: 'card-body' },
|
||||
h('ul', { class: 'service-list' },
|
||||
h('li', null,
|
||||
StatusDot({ status: dmskUp ? 'success' : 'danger' }),
|
||||
' Dnsmasq ',
|
||||
Badge({ text: dmsk.state || 'down', variant: dmskUp ? 'success' : 'danger' }),
|
||||
),
|
||||
h('li', null,
|
||||
StatusDot({ status: wUp ? 'success' : 'danger' }),
|
||||
' WireGuard ',
|
||||
Badge({ text: String(d.wg?.state || 'down'), variant: wUp ? 'success' : 'danger' }),
|
||||
),
|
||||
h('li', null, ServiceStatus({ state: dmsk.state || 'down', label: 'Dnsmasq' })),
|
||||
h('li', null, ServiceStatus({ state: d.wg?.state || 'down', label: 'WireGuard' })),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user