633505e7dc
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
123 lines
5.0 KiB
JavaScript
123 lines
5.0 KiB
JavaScript
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.' }),
|
|
];
|
|
},
|
|
});
|