diff --git a/webui/static/hoover/components/modal.js b/webui/static/hoover/components/modal.js index 4fa9b8c..98806f6 100644 --- a/webui/static/hoover/components/modal.js +++ b/webui/static/hoover/components/modal.js @@ -95,6 +95,8 @@ export function refreshModals() { * * Field shape: * { label, id, [tag: 'input'|'select'|'textarea'], [type], [value], [placeholder], [options] } + * - options can be string[], [value, selected][] tuples, or objects with { group, options } + * for grouping. Nested options follow the same string/tuple format. * * Action shape: * { label, cls, action, handler } @@ -105,11 +107,28 @@ export function formModal(inner, title, fields, actions) { if (f.tag === 'select') return '
'; + + (f.options || []).map(function(o) { + if (o === null || o === undefined) return ''; + if (typeof o === 'string') + return ''; + if (Array.isArray(o)) { + // Support [value, label] and [value, selectedBoolean] + if (o.length < 2) + return ''; + if (typeof o[1] === 'boolean') + return ''; + return ''; + } + if ('group' in o) { + return '' + + o.options.map(function(item) { + return typeof item === 'string' + ? '' + : ''; + }).join('') + ''; + } + return ''; + }).join('') + ''; const tag = f.tag || 'input'; return '
<' + tag + ' id="' + att_esc(f.id) + '"' diff --git a/webui/static/pages/dashboard.js b/webui/static/pages/dashboard.js index 00c2893..e8d523b 100644 --- a/webui/static/pages/dashboard.js +++ b/webui/static/pages/dashboard.js @@ -26,7 +26,7 @@ export default definePage({ 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')} + <${StatCard} label="WireGuard" value=${String((d.wg?.status?.up) ? 'up' : 'down')} meta=${wP.length + ' peers'} /> <${StatCard} label="Certificates" value=${certs.length} meta=${certW.length + ' expiring/expired'} /> @@ -37,8 +37,8 @@ export default definePage({
Services
    -
  • <${ServiceStatus} state=${dmsk.state || 'down'} label="Dnsmasq" />
  • -
  • <${ServiceStatus} state=${d.wg?.state || 'down'} label="WireGuard" />
  • +
  • <${ServiceStatus} state=${dmsk.service_active ? 'up' : 'down'} label="Dnsmasq" />
  • +
  • <${ServiceStatus} state=${(d.wg?.status?.up) ? 'up' : 'down'} label="WireGuard" />
diff --git a/webui/static/pages/dhcp.js b/webui/static/pages/dhcp.js index e64bd5b..94431ce 100644 --- a/webui/static/pages/dhcp.js +++ b/webui/static/pages/dhcp.js @@ -1,26 +1,40 @@ -import { html, 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'; +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'; -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', -}); +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', @@ -61,11 +75,12 @@ export default definePage({ init() { return { dnsmasq: getModel('dnsmasq'), + firewall: getModel('firewall'), activeTab: 'ranges', }; }, render(state) { - const guard = renderGuard(state.dnsmasq, 'DHCP & DNS', 'Dnsmasq management', state.dnsmasq.data); + const guard = renderGuardMulti('DHCP & DNS', 'Dnsmasq management', state.dnsmasq, state.firewall); if (guard) return guard; const cfg = state.dnsmasq.data?.config || {}; @@ -116,7 +131,7 @@ export default definePage({ const tabNames = ['ranges', 'leases', 'dns', 'active']; const actions = ActionGroup( - html``, + html``, html``, html``, ActionButton({ @@ -141,7 +156,7 @@ export default definePage({ return [ PageHeader({ title: 'DHCP & DNS', subtitle: 'Dnsmasq management', actions }), - ServiceStatus({ state: status.state || 'down', label: 'Dnsmasq' }), + 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, diff --git a/webui/static/pages/proxy.js b/webui/static/pages/proxy.js index 11bd5ba..a8b4860 100644 --- a/webui/static/pages/proxy.js +++ b/webui/static/pages/proxy.js @@ -98,8 +98,8 @@ function addDomain(state) { }, ]); - const certSelect = document.getElementById('p-cert'); - const domainInput = document.getElementById('p-domain'); + const certSelect = inner.querySelector('#p-cert'); + const domainInput = inner.querySelector('#p-domain'); if (certSelect && domainInput) { certSelect.addEventListener('change', () => { const val = certSelect.value; diff --git a/webui/static/pages/wireguard.js b/webui/static/pages/wireguard.js index 259f7c3..6b6191d 100644 --- a/webui/static/pages/wireguard.js +++ b/webui/static/pages/wireguard.js @@ -61,7 +61,7 @@ export default definePage({ if (guard) return guard; const st = state.wireguard.data?.status || {}; - const isUp = st.state === 'up'; + const isUp = st.up || false; const listenPort = (state.wireguard.data?.config?.interface || {}).listen_port || '-'; const peerRows = (state.wireguard.data?.peers || []).map(p => { @@ -107,10 +107,10 @@ export default definePage({ return [ PageHeader({ title: 'WireGuard', - subtitle: 'Tunnel: ' + (st.state || 'unknown') + ', Listen: ' + listenPort, + subtitle: 'Tunnel: ' + (st.up ? 'up' : 'down') + ', Listen: ' + listenPort, actions, }), - ServiceStatus({ state: st.state || 'down' }), + ServiceStatus({ state: st.up ? 'up' : 'down' }), peerRows.length ? Table({ columns: ['Peer', 'Public Key', 'Allowed IPs', 'Endpoint', 'Handshake', 'Transfer', 'Actions'],