Files
vacuum-wall/webui/static/pages/proxy.js
T
mteehan b673e87c9b refactor: introduce model layer for centralized data synchronization
Add hoover model.js as a central reactive store per subsystem, replacing
per-component data fetching with a single source of truth.

- Add hoover/model.js with modelRegister, modelFetch, and WS invalidation
- Refactor websocket.js to route messages to model refresh (drop per-component
  subscribe/unsubscribe)
- Simplify component.js by removing WS subscription management
- Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort
- Rewrite all pages to use getModel() instead of inline data fetching
- Bootstrap model registrations in app.js
- Add GET /api/firewall/state endpoint
- Fix restart-services.sh restart order and add service health verification
- Update hoover.md docs with model layer architecture
2026-06-22 22:54:29 +00:00

107 lines
4.4 KiB
JavaScript

import { h, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, certStatusBadge, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
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',
},
refresh: ['nginx', 'acme'],
});
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: () => ({
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',
},
refresh: ['nginx', 'acme'],
});
export default definePage({
init() {
return {
nginx: getModel('nginx'),
acme: getModel('acme'),
};
},
render(state) {
const guard = renderGuardMulti('Proxy', 'Nginx reverse proxy', state.nginx, state.acme);
if (guard) return guard;
const domains = state.nginx.data || [];
const rows = 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),
removeUrl: '/api/proxy/domains/' + enc(d.domain),
removeMessage: 'Remove proxy for ' + d.domain + '?',
removeSuccess: 'Domain removed',
removeRefresh: ['nginx', 'acme'],
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',
refresh: ['nginx', 'acme'],
}),
);
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.' }),
];
},
});