import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js'; function makeAddRange(activeZones, interfaces) { 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]) }]; }), ]; const ranges = {}; (interfaces || []).forEach(iface => { for (const cidr of (iface.ips || [])) { if (cidr.indexOf('/') === -1) continue; const parts = cidr.split('/'); const addr = parts[0].split('.').map(Number); const prefix = parseInt(parts[1], 10); if (prefix > 30 || prefix < 8) continue; const mask = (0xFFFFFFFF << (32 - prefix)) >>> 0; const netA = ((addr[0] << 24) + (addr[1] << 16) + (addr[2] << 8) + addr[3]) & mask; const broadcast = (netA | (~mask >>> 0)) >>> 0; const s = (netA + 100) >>> 0; const e = Math.min((netA + 200) >>> 0, broadcast - 1); const toIp = (n) => [n >> 24 & 0xFF, n >> 16 & 0xFF, n >> 8 & 0xFF, n & 0xFF].join('.'); if (s <= e) { ranges[iface.name] = { start: toIp(s), end: toIp(e) }; } break; } }); 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' }, ], postRender: (inner) => { const select = inner.querySelector('#r-iface'); const startInput = inner.querySelector('#r-start'); const endInput = inner.querySelector('#r-end'); if (select && startInput && endInput) { select.addEventListener('change', () => { const r = ranges[select.value]; if (r) { startInput.value = r.start; endInput.value = r.end; } else { startInput.value = ''; endInput.value = ''; } }); } }, 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 dhcpCfg = cfg.dhcp || {}; const dnsCfg = cfg.dns || {}; const ranges = dhcpCfg.ranges || []; const staticLeases = dhcpCfg.static_leases || []; const dnsRecords = dnsCfg.custom_records || []; const status = state.dnsmasq.data?.status || {}; const rangesRows = ranges.map((r) => html`
${currentDomain ? esc(currentDomain) : '(not set)'}