refactor: extract shared utilities and standardize page patterns

- Add fmtBytes() and csvToArr() helpers to hoover/helpers.js
- Replace inline async patterns with ActionButton/ConfirmDelete in wireguard.js
- Convert addDomain/editDomain to QuickModal + apiSubmit in proxy.js
- Convert settingsModal handlers to formAction in certs.js
- Remove redundant synced handling from dhcp.js apply button
- Add onComplete callback to ConfirmDelete (fixes users.js onRefresh bug)
- Fix passkeys.js ActionCell/Table usage (invalid component API)
- Remove duplicate fmtBytes from dashboard.js
This commit is contained in:
2026-07-28 17:32:51 +00:00
parent 244576b8eb
commit 8bb3619ddc
10 changed files with 156 additions and 220 deletions
+64 -70
View File
@@ -1,5 +1,4 @@
import { html, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, ConfirmDelete, certStatusBadge, ActionGroup, formAction } from '/static/hoover/index.js';
import { openModal, formModal, closeModal } from '/static/hoover/components/modal.js';
import { html, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, ConfirmDelete, certStatusBadge, ActionGroup, QuickModal } from '/static/hoover/index.js';
import { openBackendModal } from '/static/pages/backends.js';
function certLookup(acmeData) {
@@ -82,48 +81,48 @@ function addDomain(state, preselectedBackend) {
const backends = state.backends ? (state.backends.data || {}) : {};
const certOptions = buildCertOptions(state.acme ? (state.acme.data.certs || []) : []);
const backendOptions = buildBackendOptions(backends);
openModal((inner) => {
formModal(inner, 'Add Proxy Domain', [
const modal = QuickModal({
title: 'Add Proxy Domain',
fields: [
{ label: 'Domain', id: 'p-domain', placeholder: 'example.com' },
{ label: 'Backend', id: 'p-backend', tag: 'select', options: backendOptions },
{ label: 'Cert', id: 'p-cert', tag: 'select', options: certOptions },
], [
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
{
label: 'Add',
cls: 'btn-primary',
action: 's',
handler: formAction(async () => {
const domain = ($val('p-domain') || '').trim();
if (!domain) throw 'Domain is required';
const backend = ($val('p-backend') || '').trim();
if (!backend) throw 'Backend is required';
const body = { domain, backend, force_ssl: true };
const certVal = certValueFromSelect($val('p-cert'));
if (certVal) body.cert = certVal;
const res = await apiFetch('/api/proxy/domains', { method: 'POST', body });
if (!res.ok) throw res.error || 'Failed';
toast('Domain added', 'success');
closeModal();
await Promise.all(['nginx', 'acme'].map(m => modelFetch(m)));
}),
],
submit: {
url: '/api/proxy/domains',
body: () => {
const body = {
domain: ($val('p-domain') || '').trim(),
backend: ($val('p-backend') || '').trim(),
force_ssl: true,
};
const certVal = certValueFromSelect($val('p-cert'));
if (certVal) body.cert = certVal;
return body;
},
]);
if (preselectedBackend) {
const backendSelect = inner.querySelector('#p-backend');
if (backendSelect) backendSelect.value = preselectedBackend;
}
const certSelect = inner.querySelector('#p-cert');
const domainInput = inner.querySelector('#p-domain');
if (certSelect && domainInput) {
certSelect.addEventListener('change', () => {
const val = certSelect.value;
if (val && val.startsWith('acme|')) {
domainInput.value = val.slice(5);
}
});
}
validate: (b) => !b.domain ? 'Domain is required' :
!b.backend ? 'Backend is required' : null,
successMsg: 'Domain added',
},
refresh: ['nginx', 'acme'],
postRender: (inner) => {
if (preselectedBackend) {
const backendSelect = inner.querySelector('#p-backend');
if (backendSelect) backendSelect.value = preselectedBackend;
}
const certSelect = inner.querySelector('#p-cert');
const domainInput = inner.querySelector('#p-domain');
if (certSelect && domainInput) {
certSelect.addEventListener('change', () => {
const val = certSelect.value;
if (val && val.startsWith('acme|')) {
domainInput.value = val.slice(5);
}
});
}
},
});
modal({});
}
function editDomain(d, state) {
@@ -139,45 +138,40 @@ function editDomain(d, state) {
selectedCert = d.cert;
}
const paths = backend.paths || {};
const pathKeys = Object.keys(paths);
const pathSummary = pathKeys.map(p => {
const pcfg = paths[p];
const pathSummary = Object.entries(paths).map(([p, pcfg]) => {
const be = pcfg.backend || {};
return `${esc(p)}${esc(be.host || '-')}:${be.port || '-'}`;
}).join('\n') || '—';
openModal((inner) => {
formModal(inner, 'Edit: ' + esc(d.domain), [
const modal = QuickModal({
title: 'Edit: ' + esc(d.domain),
fields: [
{ label: 'Domain', id: 'pe-domain', value: d.domain },
{ label: 'Backend', id: 'pe-backend', value: (d.backend_name || '-') + ' (' + (backend.label || '—') + ')' },
{ label: 'Paths', id: 'pe-paths', tag: 'textarea', value: pathSummary, readonly: true },
{ label: 'Paths', id: 'pe-paths', tag: 'textarea', value: pathSummary },
{ label: 'Cert', id: 'pe-cert', tag: 'select', options: certOptions },
{ label: 'Force SSL', id: 'pe-force-ssl', tag: 'checkbox', checked: d.force_ssl },
], [
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
{
label: 'Save',
cls: 'btn-primary',
action: 's',
handler: formAction(async () => {
const rawCert = $val('pe-cert');
if (!rawCert) throw 'Cert is required';
const forceSsl = document.getElementById('pe-force-ssl')?.checked ?? true;
const body = { cert: certValueFromSelect(rawCert), force_ssl: forceSsl };
const res = await apiFetch('/api/proxy/domains/' + enc(d.domain), { method: 'PUT', body });
if (!res.ok) throw res.error || 'Failed';
toast('Domain updated', 'success');
closeModal();
await Promise.all(['nginx', 'acme'].map(m => modelFetch(m)));
}),
},
]);
const certSelect = inner.querySelector('#pe-cert');
if (certSelect) certSelect.value = selectedCert;
const domainInput = inner.querySelector('#pe-domain');
if (domainInput) { domainInput.readOnly = true; domainInput.style.background = '#f5f5f5'; }
const backendInput = inner.querySelector('#pe-backend');
if (backendInput) { backendInput.readOnly = true; backendInput.style.background = '#f5f5f5'; }
],
submit: {
url: '/api/proxy/domains/' + enc(d.domain),
method: 'PUT',
body: () => ({
cert: certValueFromSelect($val('pe-cert')) || '',
force_ssl: document.getElementById('pe-force-ssl')?.checked ?? true,
}),
validate: (b) => !b.cert ? 'Cert is required' : null,
successMsg: 'Domain updated',
},
refresh: ['nginx', 'acme'],
postRender: (inner) => {
const certSelect = inner.querySelector('#pe-cert');
if (certSelect) certSelect.value = selectedCert;
for (const id of ['pe-domain', 'pe-backend']) {
const el = inner.querySelector('#' + id);
if (el) { el.readOnly = true; el.style.background = '#f5f5f5'; }
}
},
});
modal({});
}
function domainRow(domainName, domainPaths, state) {