import { h, PageHeader, Badge, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, refactorLoad, ActionButton, ActionCell, certStatusBadge, ActionGroup, QuickModal } from '/static/hoover/index.js?v=6'; const addDomain = QuickModal({ title: 'Add Proxy Domain', fields: [ { label: 'Domain', id: 'p-domain', placeholder: 'example.com' }, { label: 'Backend Host', id: 'p-host', placeholder: '192.168.1.10' }, { label: 'Backend Port', id: 'p-port', type: 'number', placeholder: '8080' }, { label: 'Protocol', id: 'p-proto', placeholder: 'http or https' }, { label: 'Cert (optional)', id: 'p-cert', placeholder: 'acme' }, ], submit: { url: '/api/proxy/domains', body: () => ({ domain: ($val('p-domain') || '').trim(), backend_host: ($val('p-host') || '').trim(), backend_port: parseInt($val('p-port')), backend_proto: ($val('p-proto') || 'http').trim() || 'http', cert: ($val('p-cert') || '').trim() || undefined, }), validate: (b) => !b.domain || !b.backend_host || !b.backend_port ? 'Domain, host, and port are required' : null, successMsg: 'Domain added', }, reload: (s) => load(s), }); const editDomain = QuickModal({ title: (d) => 'Edit: ' + d.domain, fields: (d) => [ { label: 'Backend Host', id: 'pe-host', value: d.backend_host || '' }, { label: 'Backend Port', id: 'pe-port', type: 'number', value: d.backend_port || '' }, { label: 'Protocol', id: 'pe-proto', value: d.backend_proto || d.protocol || 'http' }, { label: 'Cert (optional)', id: 'pe-cert', value: d.cert || '' }, ], submit: { url: (d) => '/api/proxy/domains/' + enc(d.domain), method: 'PUT', body: (d) => ({ backend_host: ($val('pe-host') || '').trim(), backend_port: parseInt($val('pe-port')), backend_proto: ($val('pe-proto') || 'http').trim(), cert: ($val('pe-cert') || '').trim() || undefined, }), validate: (b) => !b.backend_host || !b.backend_port ? 'Host and port are required' : null, successMsg: 'Domain updated', }, reload: (s) => load(s._s), }); async function load(state, abortController, entry) { await refactorLoad(state, s => s.domains?.length, async (s, sig, isAborted) => { const domainsR = await apiFetch('/api/proxy/domains', { signal: sig }); if (isAborted()) return; if (domainsR.ok) s.domains = domainsR.data || []; else s.error = domainsR.error; const certsR = await apiFetch('/api/certs/list', { signal: sig }); if (isAborted()) return; if (certsR.ok) s.certs = certsR.data || []; else if (!s.error) s.error = certsR.error; }, { entry, abortController }, ); } export default definePage({ init() { return { domains: [], certs: [] }; }, subscribe: ['nginx', 'acme'], load, render(state) { const guard = renderGuard(state, 'Proxy', 'Nginx reverse proxy', state.domains); if (guard) return guard; const rows = state.domains.map(d => { const certBadge = certStatusBadge({ certStatus: d.cert_status, daysRemaining: d.days_remaining, expired: d.cert_status === 'expired', }); return h('tr', { key: d.domain }, h('td', null, h('strong', null, esc(d.domain))), h('td', null, esc(d.backend_host || '-')), h('td', null, d.backend_port || '-'), h('td', null, Badge({ text: d.backend_proto || d.protocol || 'http', variant: 'info' })), h('td', null, certBadge), ActionCell({ editLabel: 'Edit', editClick: () => editDomain({ ...d, _s: state }), removeUrl: '/api/proxy/domains/' + enc(d.domain), removeMessage: 'Remove proxy for ' + d.domain + '?', removeSuccess: 'Domain removed', removeReload: () => load(state), removeLabel: 'Delete', }), ); }); const actions = ActionGroup( h('button', { class: 'btn btn-primary', 'on:click': () => addDomain(state) }, 'Add Domain'), ActionButton({ url: '/api/proxy/apply', successMsg: 'Nginx applied & reloaded', label: 'Apply', reload: () => load(state), }), ); return [ PageHeader({ title: 'Proxy', subtitle: 'Nginx reverse proxy', actions }), rows.length ? Table({ columns: ['Domain', 'Backend Host', 'Port', 'Proto', 'Cert', 'Actions'], rows, }) : Empty({ text: 'No proxy domains configured. Add a domain to start terminating SSL.' }), ]; }, });