b673e87c9b
Add hoover model.js as a central reactive store per subsystem, replacing per-component data fetching with a single source of truth. - Add hoover/model.js with modelRegister, modelFetch, and WS invalidation - Refactor websocket.js to route messages to model refresh (drop per-component subscribe/unsubscribe) - Simplify component.js by removing WS subscription management - Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort - Rewrite all pages to use getModel() instead of inline data fetching - Bootstrap model registrations in app.js - Add GET /api/firewall/state endpoint - Fix restart-services.sh restart order and add service health verification - Update hoover.md docs with model layer architecture
152 lines
6.4 KiB
JavaScript
152 lines
6.4 KiB
JavaScript
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';
|
|
|
|
const addRange = QuickModal({
|
|
title: 'Add DHCP Range',
|
|
fields: [
|
|
{ label: 'Interface (optional)', id: 'r-iface', placeholder: 'Leave empty for global' },
|
|
{ label: 'Start IP', id: 'r-start', placeholder: '192.168.1.100' },
|
|
{ label: 'End IP', id: 'r-end', placeholder: '192.168.1.200' },
|
|
{ label: 'Lease Time', id: 'r-lease', placeholder: '12h' },
|
|
],
|
|
submit: {
|
|
url: '/api/dhcp/ranges',
|
|
body: () => ({
|
|
interface: ($val('r-iface') || '').trim() || undefined,
|
|
start: ($val('r-start') || '').trim(),
|
|
end: ($val('r-end') || '').trim(),
|
|
lease_time: ($val('r-lease') || '').trim() || '12h',
|
|
}),
|
|
validate: (b) => !b.start || !b.end ? 'Start and end are required' : null,
|
|
successMsg: 'Range added',
|
|
},
|
|
refresh: 'dnsmasq',
|
|
});
|
|
|
|
const addLease = QuickModal({
|
|
title: 'Add Static Lease',
|
|
fields: [
|
|
{ label: 'MAC', id: 'l-mac', placeholder: 'aa:bb:cc:dd:ee:ff' },
|
|
{ label: 'IP', id: 'l-ip', placeholder: '192.168.1.50' },
|
|
{ label: 'Hostname (optional)', id: 'l-host', placeholder: 'device-name' },
|
|
],
|
|
submit: {
|
|
url: '/api/dhcp/static-lease',
|
|
body: () => ({
|
|
mac: ($val('l-mac') || '').trim(),
|
|
ip: ($val('l-ip') || '').trim(),
|
|
hostname: ($val('l-host') || '').trim() || undefined,
|
|
}),
|
|
validate: (b) => !b.mac || !b.ip ? 'MAC and IP are required' : null,
|
|
successMsg: 'Lease added',
|
|
},
|
|
refresh: 'dnsmasq',
|
|
});
|
|
|
|
const addDns = QuickModal({
|
|
title: 'Add DNS Record',
|
|
fields: [
|
|
{ label: 'Name', id: 'd-name', placeholder: 'host.local' },
|
|
{ label: 'Address', id: 'd-addr', placeholder: '192.168.1.10' },
|
|
],
|
|
submit: {
|
|
url: '/api/dhcp/dns-record',
|
|
body: () => ({ name: ($val('d-name') || '').trim(), address: ($val('d-addr') || '').trim() }),
|
|
validate: (b) => !b.name || !b.address ? 'Name and address are required' : null,
|
|
successMsg: 'DNS record added',
|
|
},
|
|
refresh: 'dnsmasq',
|
|
});
|
|
|
|
export default definePage({
|
|
init() {
|
|
return {
|
|
dnsmasq: getModel('dnsmasq'),
|
|
activeTab: 'ranges',
|
|
};
|
|
},
|
|
render(state) {
|
|
const guard = renderGuard(state.dnsmasq, 'DHCP & DNS', 'Dnsmasq management', state.dnsmasq.data);
|
|
if (guard) return guard;
|
|
|
|
const cfg = state.dnsmasq.data?.config || {};
|
|
const ranges = cfg.ranges || [];
|
|
const staticLeases = cfg.static_leases || [];
|
|
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 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 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 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'),
|
|
ActionButton({
|
|
url: '/api/dhcp/apply',
|
|
successMsg: 'dnsmasq applied',
|
|
label: 'Apply',
|
|
refresh: 'dnsmasq',
|
|
}),
|
|
);
|
|
|
|
return [
|
|
PageHeader({ title: 'DHCP & DNS', subtitle: 'Dnsmasq management', actions }),
|
|
ServiceStatus({ state: status.state || 'down', label: 'Dnsmasq' }),
|
|
Tabs({ state, tabs: tabNames }),
|
|
state.activeTab === 'ranges'
|
|
? Table({ columns: ['Interface', 'Start', 'End', 'Lease', 'Action'], rows: rangesRows, emptyText: 'No DHCP ranges' }) : null,
|
|
state.activeTab === 'leases'
|
|
? 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,
|
|
];
|
|
},
|
|
}); |