feat: auto-generate self-signed certs, hoover modal processing guard, refactor backends/proxy pages

This commit is contained in:
2026-07-13 14:30:35 +00:00
parent 05524f3756
commit 2e49dec633
34 changed files with 790 additions and 411 deletions
+59 -21
View File
@@ -3,10 +3,12 @@
*
* JSON-friendly fetch wrapper with automatic header management.
* Toast notification system with auto-dismiss.
* Modal processing guard for async form submissions.
*/
import { modelFetch } from './model.js?v=8';
import { requestUpdate } from './reactivity.js?v=8';
import { modelFetch } from './model.js?v=9';
import { requestUpdate } from './reactivity.js?v=9';
import { isModalProcessing, setModalProcessing, refreshModals } from './components/modal.js?v=9';
/**
* JSON-friendly fetch wrapper.
@@ -178,6 +180,33 @@ export async function poll(opts) {
}, interval);
}
/**
* Centralized handler wrapper that encapsulates processing guard,
* processing state, error handling, and modal re-render.
*
* Used by any handler not using `apiSubmit`. The async function receives
* no arguments and should perform validation (via `throw`), API calls,
* success/error toasting, modal closing, and data refreshing.
*
* @param {function} fn Async handler function
* @returns {function} Wrapped handler
*/
export function formAction(fn) {
return async () => {
if (isModalProcessing()) return;
setModalProcessing(true);
refreshModals();
try {
await fn();
} catch (e) {
toast(e.message || 'Failed', 'error');
} finally {
setModalProcessing(false);
refreshModals();
}
};
}
/**
* Generate action button descriptors for modal form submission.
*
@@ -211,28 +240,37 @@ export function apiSubmit(opts) {
label: submitText,
cls: 'btn-primary',
action: 's',
processing: true,
handler: async () => {
const b = body ? body() : {};
if (validate) {
const err = validate(b);
if (err) { toast(err, 'error'); return; }
}
const res = await apiFetch(url, { method, body: b });
if (res.ok) {
const synced = res.data?.synced;
let msg = successMsg;
if (synced && synced.length) {
msg += ' (auto-synced: ' + synced.join(', ') + ')';
synced.forEach(s => modelFetch(s));
if (isModalProcessing()) return;
setModalProcessing(true);
refreshModals();
try {
const b = body ? body() : {};
if (validate) {
const err = validate(b);
if (err) { toast(err, 'error'); return; }
}
toast(msg, 'success');
if (closeModal) closeModal();
if (refresh) {
const models = Array.isArray(refresh) ? refresh : [refresh];
await Promise.all(models.map(m => modelFetch(m)));
const res = await apiFetch(url, { method, body: b });
if (res.ok) {
const synced = res.data?.synced;
let msg = successMsg;
if (synced && synced.length) {
msg += ' (auto-synced: ' + synced.join(', ') + ')';
synced.forEach(s => modelFetch(s));
}
toast(msg, 'success');
if (closeModal) closeModal();
if (refresh) {
const models = Array.isArray(refresh) ? refresh : [refresh];
await Promise.all(models.map(m => modelFetch(m)));
}
} else {
toast(res.error || 'Failed', 'error');
}
} else {
toast(res.error || 'Failed', 'error');
} finally {
setModalProcessing(false);
refreshModals();
}
},
},