import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js'; function addDomainModal(state) { openModal((inner, idx) => { formModal(inner, 'Add Proxy Domain', [ { 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' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Add', cls: 'btn-primary', action: 's', handler: async () => { const 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, }; if (!body.domain || !body.backend_host || !body.backend_port) { toast('Domain, host, and port are required', 'error'); return; } const resp = await apiFetch('/api/proxy/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (resp.ok) { toast('Domain added', 'success'); closeModal(idx); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } function editDomainModal(domain, state) { openModal((inner, idx) => { formModal(inner, 'Edit: ' + domain.domain, [ { label: 'Backend Host', id: 'pe-host', value: domain.backend_host || '' }, { label: 'Backend Port', id: 'pe-port', type: 'number', value: domain.backend_port || '' }, { label: 'Protocol', id: 'pe-proto', value: domain.backend_proto || domain.protocol || 'http' }, { label: 'Cert (optional)', id: 'pe-cert', value: domain.cert || '' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Save', cls: 'btn-primary', action: 's', handler: async () => { const body = { 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, }; const resp = await apiFetch('/api/proxy/domains/' + enc(domain.domain), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (resp.ok) { toast('Domain updated', 'success'); closeModal(idx); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } async function load(state) { try { const domainsR = await apiFetch('/api/proxy/domains'); if (domainsR.ok) state.domains = domainsR.data || []; const certsR = await apiFetch('/api/certs/list'); if (certsR.ok) state.certs = certsR.data || []; } catch (e) { state.error = String(e); } state.loading = false; } export default definePage({ init() { return { domains: [], certs: [], loading: true, error: null }; }, subscribe: ['nginx', 'acme'], load, render(state) { if (state.loading) { return [ PageHeader({ title: 'Proxy' }), h('div', { class: 'card', key: 'loading' }, h('div', { class: 'card-body loading' }, 'Loading...'), ), ]; } if (state.error) { return [ PageHeader({ title: 'Proxy' }), h('div', { class: 'card', key: 'error' }, h('div', { class: 'card-body error-msg' }, state.error), ), ]; } const rows = state.domains.map(d => { let certBadge = Badge({ text: 'No cert', variant: 'info' }); if (d.cert_status === 'valid' || d.cert_status === 'active') { certBadge = Badge({ text: 'Valid', variant: 'success' }); } else if (d.cert_status === 'expired' || (d.days_remaining !== undefined && d.days_remaining <= 0)) { certBadge = Badge({ text: 'Expired', variant: 'danger' }); } else if (d.days_remaining !== undefined && d.days_remaining <= 30) { certBadge = Badge({ text: d.days_remaining + 'd', variant: 'warning' }); } else if (d.days_remaining !== undefined) { certBadge = Badge({ text: d.days_remaining + 'd', variant: 'success' }); } 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), h('td', null, h('button', { class: 'btn btn-sm btn-outline', style: 'margin-right:4px;', 'on:click': () => editDomainModal(d, state) }, 'Edit'), h('button', { class: 'btn btn-sm btn-danger', 'on:click': async () => { if (!confirm('Remove proxy for ' + d.domain + '?')) return; const r = await apiFetch('/api/proxy/domains/' + enc(d.domain), { method: 'DELETE' }); if (r.ok) { toast('Domain removed', 'success'); await load(state); } else { toast(r.error || 'Failed', 'error'); } }}, 'Delete'), ), ); }); const actions = h('div', { style: 'display:flex;gap:8px;' }, h('button', { class: 'btn btn-primary', 'on:click': () => addDomainModal(state) }, 'Add Domain'), h('button', { class: 'btn btn-outline', 'on:click': async () => { const resp = await apiFetch('/api/proxy/apply', { method: 'POST' }); if (resp.ok) toast('Nginx applied & reloaded', 'success'); else toast(resp.error || 'Failed', 'error'); }}, 'Apply'), ); return [ PageHeader({ title: 'Proxy', subtitle: 'Nginx reverse proxy', actions }), rows.length ? h('div', { class: 'card' }, h('table', { class: 'table' }, h('thead', null, h('tr', null, h('th', null, 'Domain'), h('th', null, 'Backend Host'), h('th', null, 'Port'), h('th', null, 'Proto'), h('th', null, 'Cert'), h('th', { style: 'width:140px;' }, 'Actions'), ), ), h('tbody', null, ...rows), )) : Empty({ text: 'No proxy domains configured. Add a domain to start terminating SSL.' }), ]; }, });