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, ]; }, });