Files
vacuum-wall/webui/static/pages/dhcp.js
T
mteehan 391466664e Update status checks and add optgroup support to form modals
- modal.js: add optgroup support for select options in formModal
- dhcp.js: use zone-based interface selector for DHCP ranges
- dashboard.js, wireguard.js: use status.up for state checks
- proxy.js: use inner.querySelector for modal field lookup
2026-06-28 12:47:59 +00:00

171 lines
7.0 KiB
JavaScript

import { html, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
function makeAddRange(activeZones) {
const opts = [
['', '(global)'],
...Object.entries(activeZones || {})
.filter(([, ifaces]) => Array.isArray(ifaces) && ifaces.length > 0)
.flatMap(([zone, ifaces]) => {
const label = zone + ' \u2192 ';
if (ifaces.length === 1)
return [[ifaces[0], label + ifaces[0]]];
return [{ group: zone, options: ifaces.map(i => [i, label + i]) }];
}),
];
return QuickModal({
title: 'Add DHCP Range',
fields: [
{ label: 'Interface (optional)', id: 'r-iface', tag: 'select', options: opts },
{ 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'),
firewall: getModel('firewall'),
activeTab: 'ranges',
};
},
render(state) {
const guard = renderGuardMulti('DHCP & DNS', 'Dnsmasq management', state.dnsmasq, state.firewall);
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) => 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) => 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) => 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(
html`<button class="btn btn-primary" onClick=${() => makeAddRange(state.firewall.data?.zones?.active)(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',
label: 'Apply',
refresh: 'dnsmasq',
}),
);
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.service_active ? 'up' : '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,
leaseTable,
];
},
});