feat: auto-generate self-signed certs, hoover modal processing guard, refactor backends/proxy pages
This commit is contained in:
+59
-21
@@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
* });
|
||||
*/
|
||||
|
||||
import { reactive } from './reactivity.js?v=8';
|
||||
import { h } from './vdom.js?v=8';
|
||||
import { _compExpandedCache } from './render.js?v=8';
|
||||
import { reactive } from './reactivity.js?v=9';
|
||||
import { h } from './vdom.js?v=9';
|
||||
import { _compExpandedCache } from './render.js?v=9';
|
||||
|
||||
/** Registry of mounted components: key → { state } */
|
||||
const _mounted = new Map();
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
* expandable modal, then applies all via /api/status/apply-all.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=8';
|
||||
import { html } from '../html.js?v=8';
|
||||
import { reactive } from '../reactivity.js?v=8';
|
||||
import { apiFetch, toast } from '../api.js?v=8';
|
||||
import { modelFetch } from '../model.js?v=8';
|
||||
import { openModal, closeModal, modalVNodes } from './modal.js?v=8';
|
||||
import { h } from '../vdom.js?v=9';
|
||||
import { html } from '../html.js?v=9';
|
||||
import { reactive } from '../reactivity.js?v=9';
|
||||
import { apiFetch, toast } from '../api.js?v=9';
|
||||
import { modelFetch } from '../model.js?v=9';
|
||||
import { openModal, closeModal, modalVNodes, isModalProcessing, setModalProcessing, refreshModals } from './modal.js?v=9';
|
||||
|
||||
export const SUBSYSTEM_LIST = [
|
||||
{ key: 'firewall', label: 'Firewall' },
|
||||
@@ -59,16 +59,23 @@ ${hasPending ? html`<span class="apply-expand-icon${isExpanded ? ' expanded' : '
|
||||
* POST apply-all, toast result, close modal, refresh models.
|
||||
*/
|
||||
async function doApply(successMsg, refreshTargets) {
|
||||
const resp = await apiFetch('/api/status/apply-all', { method: 'POST' });
|
||||
if (resp.ok) {
|
||||
toast(successMsg, 'success');
|
||||
closeModal();
|
||||
if (refreshTargets) {
|
||||
const names = Array.isArray(refreshTargets) ? refreshTargets : [refreshTargets];
|
||||
names.forEach(n => modelFetch(n));
|
||||
if (isModalProcessing()) return;
|
||||
setModalProcessing(true);
|
||||
try {
|
||||
const resp = await apiFetch('/api/status/apply-all', { method: 'POST' });
|
||||
if (resp.ok) {
|
||||
toast(successMsg, 'success');
|
||||
closeModal();
|
||||
if (refreshTargets) {
|
||||
const names = Array.isArray(refreshTargets) ? refreshTargets : [refreshTargets];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Apply failed', 'error');
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Apply failed', 'error');
|
||||
} finally {
|
||||
setModalProcessing(false);
|
||||
refreshModals();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
* Data display components: Badge, StatusDot, Empty, Card.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=8';
|
||||
import { esc } from '../helpers.js?v=8';
|
||||
import { apiFetch, toast } from '../api.js?v=8';
|
||||
import { modelFetch } from '../model.js?v=8';
|
||||
import { h } from '../vdom.js?v=9';
|
||||
import { esc } from '../helpers.js?v=9';
|
||||
import { apiFetch, toast } from '../api.js?v=9';
|
||||
import { modelFetch } from '../model.js?v=9';
|
||||
import { requestUpdate } from '../reactivity.js?v=9';
|
||||
|
||||
const _actionPending = new Map();
|
||||
const _confirmPending = new Map();
|
||||
|
||||
export const _deleting = new Set();
|
||||
|
||||
/**
|
||||
* Colored badge/span.
|
||||
@@ -75,30 +81,59 @@ export function Card(props = {}) {
|
||||
* @param {string|string[]} [props.refresh] - Model name(s) to refresh via modelFetch
|
||||
* @param {string} [props.label] - Button text (default: 'Remove')
|
||||
* @param {object} [props.body] - Optional JSON body to send with DELETE
|
||||
* @param {string} [props.deleteKey] - Unique ID for pending-delete row styling
|
||||
*/
|
||||
export function ConfirmDelete(props = {}) {
|
||||
const opts = { method: 'DELETE' };
|
||||
if (props.body) opts.body = props.body;
|
||||
return h('button', { class: 'btn btn-sm btn-danger',
|
||||
const deleteKey = props.url + (props.body ? '::' + JSON.stringify(props.body) : '');
|
||||
const pending = _confirmPending.get(deleteKey) || false;
|
||||
|
||||
return h('button', {
|
||||
class: 'btn btn-sm btn-danger',
|
||||
disabled: pending,
|
||||
'on:click': async () => {
|
||||
if (!confirm(props.message)) return;
|
||||
const r = await apiFetch(props.url, opts);
|
||||
if (r.ok) {
|
||||
const synced = r.data?.synced;
|
||||
let msg = props.success || 'Removed';
|
||||
if (synced && synced.length) {
|
||||
msg += ' (auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
_confirmPending.set(deleteKey, true);
|
||||
requestUpdate();
|
||||
try {
|
||||
const r = await apiFetch(props.url, opts);
|
||||
if (r.ok) {
|
||||
const synced = r.data?.synced;
|
||||
let msg = props.success || 'Removed';
|
||||
if (synced && synced.length) {
|
||||
msg += ' (auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
}
|
||||
toast(msg, 'success');
|
||||
|
||||
if (props.deleteKey) {
|
||||
_deleting.add(props.deleteKey);
|
||||
}
|
||||
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
const promises = names.map(n => modelFetch(n));
|
||||
|
||||
if (props.deleteKey && promises.length) {
|
||||
Promise.all(promises).finally(() => {
|
||||
_deleting.delete(props.deleteKey);
|
||||
});
|
||||
}
|
||||
} else if (props.deleteKey) {
|
||||
// Cleanup dimming synchronously on DELETE completion rather than
|
||||
// relying on a heuristic timeout that breaks when tabs are throttled.
|
||||
_deleting.delete(props.deleteKey);
|
||||
}
|
||||
} else {
|
||||
toast(r.error || 'Failed', 'error');
|
||||
}
|
||||
toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(r.error || 'Failed', 'error');
|
||||
} finally {
|
||||
_confirmPending.delete(deleteKey);
|
||||
requestUpdate();
|
||||
}
|
||||
}}, props.label || 'Remove');
|
||||
}
|
||||
}, pending ? h('span', { class: 'btn-spinner' }) : (props.label || 'Remove'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,32 +163,42 @@ export function ActionButton(props = {}) {
|
||||
? (props.condition ? props.labelOn : props.labelOff)
|
||||
: 'Action');
|
||||
const cls = props.cls || 'btn btn-outline';
|
||||
const pending = _actionPending.get(props.url) || false;
|
||||
|
||||
return h('button', {
|
||||
class: cls,
|
||||
disabled: props.disabled,
|
||||
disabled: !!props.disabled || pending,
|
||||
'on:click': async () => {
|
||||
const body = props.body ? props.body() : undefined;
|
||||
const opts = { method: props.method || 'POST' };
|
||||
if (body !== undefined) opts.body = body;
|
||||
const resp = await apiFetch(props.url, opts);
|
||||
if (resp.ok) {
|
||||
const synced = resp.data?.synced;
|
||||
let msg = props.successMsg || '';
|
||||
if (synced && synced.length) {
|
||||
if (msg) msg += ' ';
|
||||
msg += '(auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
if (pending) return;
|
||||
_actionPending.set(props.url, true);
|
||||
requestUpdate();
|
||||
try {
|
||||
const body = props.body ? props.body() : undefined;
|
||||
const opts = { method: props.method || 'POST' };
|
||||
if (body !== undefined) opts.body = body;
|
||||
const resp = await apiFetch(props.url, opts);
|
||||
if (resp.ok) {
|
||||
const synced = resp.data?.synced;
|
||||
let msg = props.successMsg || '';
|
||||
if (synced && synced.length) {
|
||||
if (msg) msg += ' ';
|
||||
msg += '(auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
}
|
||||
if (msg) toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Failed', props.errorType || 'error');
|
||||
}
|
||||
if (msg) toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Failed', props.errorType || 'error');
|
||||
} finally {
|
||||
_actionPending.delete(props.url);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
}, label);
|
||||
}, pending ? h('span', { class: 'btn-spinner' }) : label);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +324,7 @@ export function ServiceStatus(props = {}) {
|
||||
* @param {string} [props.removeLabel] - Delete button label (default: 'Remove')
|
||||
* @param {object} [props.removeBody] - Optional JSON body to send with DELETE
|
||||
* @param {string} [props.editCls] - Override classes for edit button (default: 'btn btn-sm btn-outline')
|
||||
* @param {string} [props.deleteKey] - Unique ID forwarded to ConfirmDelete for pending-delete styling
|
||||
*/
|
||||
export function ActionCell(props = {}) {
|
||||
return h('td', null,
|
||||
@@ -294,6 +340,7 @@ export function ActionCell(props = {}) {
|
||||
refresh: props.removeRefresh,
|
||||
label: props.removeLabel || 'Remove',
|
||||
body: props.removeBody,
|
||||
deleteKey: props.deleteKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* Layout components: PageHeader, Tabs, SectionTitle, DataTableSection, ActionGroup.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=8';
|
||||
import { Table } from './data.js?v=8';
|
||||
import { collectLoadingModels } from '../model.js?v=8';
|
||||
import { h } from '../vdom.js?v=9';
|
||||
import { Table } from './data.js?v=9';
|
||||
import { collectLoadingModels } from '../model.js?v=9';
|
||||
|
||||
/**
|
||||
* Page header with title, optional subtitle, and action buttons.
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
* avoid fighting with the main render cycle.
|
||||
*/
|
||||
|
||||
import { esc } from '../helpers.js?v=8';
|
||||
import { att_esc } from '../helpers.js?v=8';
|
||||
import { apiSubmit } from '../api.js?v=8';
|
||||
import { createDom } from '../vdom.js?v=8';
|
||||
import { esc, att_esc } from '../helpers.js?v=9';
|
||||
import { apiSubmit } from '../api.js?v=9';
|
||||
import { createDom } from '../vdom.js?v=9';
|
||||
|
||||
/**
|
||||
* Render Hoover VNodes into a modal content element.
|
||||
@@ -33,7 +32,13 @@ function _renderModals() {
|
||||
_modalQueue.forEach((m, idx) => {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'modal-overlay active';
|
||||
wrap.onclick = (e) => { if (e.target === wrap) closeModal(idx); };
|
||||
wrap.onclick = (e) => {
|
||||
if (e.target === wrap) {
|
||||
if (m._processing) return;
|
||||
if (m._hasInputs && !confirm('Discard changes?')) return;
|
||||
closeModal(idx);
|
||||
}
|
||||
};
|
||||
const content = document.createElement('div');
|
||||
content.className = 'modal';
|
||||
content.onclick = (e) => e.stopPropagation();
|
||||
@@ -55,12 +60,36 @@ function _renderModals() {
|
||||
*/
|
||||
export function openModal(content) {
|
||||
const entry = typeof content === 'function'
|
||||
? { renderFn: content, id: _modalQueue.length }
|
||||
: { id: _modalQueue.length, renderFn: (inner) => modalVNodes(inner, content) };
|
||||
? { renderFn: content, id: _modalQueue.length, _processing: false, _hasInputs: false }
|
||||
: { id: _modalQueue.length, renderFn: (inner) => modalVNodes(inner, content), _processing: false, _hasInputs: false };
|
||||
_modalQueue.push(entry);
|
||||
_renderModals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the topmost modal (or specified index) has an active async operation.
|
||||
*
|
||||
* @param {number} [idx] – Modal index (defaults to topmost)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isModalProcessing(idx) {
|
||||
if (idx === undefined) idx = _modalQueue.length - 1;
|
||||
if (idx < 0 || idx >= _modalQueue.length) return false;
|
||||
return _modalQueue[idx]._processing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the processing flag on the topmost modal (or specified index).
|
||||
*
|
||||
* @param {boolean} flag – Whether the modal is currently processing
|
||||
* @param {number} [idx] – Modal index (defaults to topmost)
|
||||
*/
|
||||
export function setModalProcessing(flag, idx) {
|
||||
if (idx === undefined) idx = _modalQueue.length - 1;
|
||||
if (idx < 0 || idx >= _modalQueue.length) return;
|
||||
_modalQueue[idx]._processing = flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a modal by index. Closes the topmost modal if index is omitted.
|
||||
*
|
||||
@@ -135,16 +164,39 @@ export function formModal(inner, title, fields, actions) {
|
||||
+ (f.type !== 'text' ? ' type="' + att_esc(f.type) + '"' : '')
|
||||
+ (f.value !== undefined ? ' value="' + att_esc(f.value) + '"' : '')
|
||||
+ (f.placeholder ? ' placeholder="' + att_esc(f.placeholder) + '"' : '')
|
||||
+ (f.checked ? ' checked' : '')
|
||||
+ '></' + tag + '></div>';
|
||||
}).join('') + '</div><div class="modal-actions">'
|
||||
+ actions.map(a =>
|
||||
'<button class="btn ' + att_esc(a.cls) + '" data-action="' + att_esc(a.action) + '">' + esc(a.label) + '</button>',
|
||||
).join('') + '</div>';
|
||||
}).join('') + '</div><div class="modal-actions"></div>';
|
||||
|
||||
actions.forEach(a => {
|
||||
const btn = inner.querySelector('[data-action="' + att_esc(a.action) + '"]');
|
||||
if (btn) btn.addEventListener('click', a.handler);
|
||||
});
|
||||
// Mark modal as having editable inputs
|
||||
const topEntry = _modalQueue[_modalQueue.length - 1];
|
||||
if (topEntry) topEntry._hasInputs = true;
|
||||
|
||||
// Build action buttons with processing-aware rendering
|
||||
const actionsBar = inner.querySelector('.modal-actions');
|
||||
const actionBtns = [];
|
||||
for (const a of actions) {
|
||||
const actionId = 'am-' + a.action + '-' + (_modalQueue.length - 1);
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn ' + a.cls;
|
||||
btn.id = actionId;
|
||||
if (a.processing && isModalProcessing(_modalQueue.length - 1)) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="btn-spinner"></span>';
|
||||
} else {
|
||||
btn.appendChild(document.createTextNode(a.label));
|
||||
}
|
||||
// Store reference for later re-binding
|
||||
actionBtns.push({ btn, action: a });
|
||||
if (a.handler) {
|
||||
const origHandler = a.handler;
|
||||
btn.addEventListener('click', () => {
|
||||
refreshModals();
|
||||
origHandler();
|
||||
});
|
||||
}
|
||||
actionsBar.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Uses the toast/dismissToast state from api.js.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=8';
|
||||
import { _toasts, dismissToast } from '../api.js?v=8';
|
||||
import { h } from '../vdom.js?v=9';
|
||||
import { _toasts, dismissToast } from '../api.js?v=9';
|
||||
|
||||
/**
|
||||
* Render all pending toast notifications.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import htm from '../../vendor/htm.js';
|
||||
import { htmAdapter } from './vdom.js?v=8';
|
||||
import { htmAdapter } from './vdom.js?v=9';
|
||||
|
||||
export const html = htm.bind(htmAdapter);
|
||||
|
||||
@@ -5,46 +5,46 @@
|
||||
*/
|
||||
|
||||
/* ── Reactivity ──────────────────────────────────────────────── */
|
||||
export { reactive, requestUpdate } from './reactivity.js?v=8';
|
||||
export { reactive, requestUpdate } from './reactivity.js?v=9';
|
||||
|
||||
/* ── VDOM ────────────────────────────────────────────────────── */
|
||||
export { h } from './vdom.js?v=8';
|
||||
export { h } from './vdom.js?v=9';
|
||||
|
||||
/* ── HTM ──────────────────────────────────────────────────────── */
|
||||
export { html } from './html.js?v=8';
|
||||
export { html } from './html.js?v=9';
|
||||
|
||||
/* ── Render ──────────────────────────────────────────────────── */
|
||||
export { render } from './render.js?v=8';
|
||||
export { render } from './render.js?v=9';
|
||||
|
||||
/* ── Component ───────────────────────────────────────────────── */
|
||||
export { definePage, hComp } from './component.js?v=8';
|
||||
export { definePage, hComp } from './component.js?v=9';
|
||||
|
||||
/* ── Router ──────────────────────────────────────────────────── */
|
||||
export { createRouter, Link } from './router.js?v=8';
|
||||
export { createRouter, Link } from './router.js?v=9';
|
||||
|
||||
/* ── WebSocket ───────────────────────────────────────────────── */
|
||||
export { connect, onMessage } from './websocket.js?v=8';
|
||||
export { connect, onMessage } from './websocket.js?v=9';
|
||||
|
||||
/* ── API & Toast ─────────────────────────────────────────────── */
|
||||
export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad } from './api.js?v=8';
|
||||
export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad, formAction } from './api.js?v=9';
|
||||
|
||||
/* ── Model ───────────────────────────────────────────────────── */
|
||||
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js?v=8';
|
||||
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js?v=9';
|
||||
|
||||
/* ── Helpers ─────────────────────────────────────────────────── */
|
||||
export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js?v=8';
|
||||
export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js?v=9';
|
||||
|
||||
/* ── UI Components: Layout ───────────────────────────────────── */
|
||||
export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js?v=8';
|
||||
export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js?v=9';
|
||||
|
||||
/* ── UI Components: Data ─────────────────────────────────────── */
|
||||
export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js?v=8';
|
||||
export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js?v=9';
|
||||
|
||||
/* ── UI Components: Modal ────────────────────────────────────── */
|
||||
export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal } from './components/modal.js?v=8';
|
||||
export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal, isModalProcessing, setModalProcessing } from './components/modal.js?v=9';
|
||||
|
||||
/* ── UI Components: Apply ────────────────────────────────────── */
|
||||
export { ApplyConfirm } from './components/applyconfirm.js?v=8';
|
||||
export { ApplyConfirm } from './components/applyconfirm.js?v=9';
|
||||
|
||||
/* ── UI Components: Toast ────────────────────────────────────── */
|
||||
export { ToastContainer } from './components/toast.js?v=8';
|
||||
export { ToastContainer } from './components/toast.js?v=9';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* collectLoadingModels(...models) — combine loading/refreshing/error
|
||||
*/
|
||||
|
||||
import { reactive } from './reactivity.js?v=8';
|
||||
import { reactive } from './reactivity.js?v=9';
|
||||
|
||||
/** Registered models: name → { model, subsystem, fetch } */
|
||||
const _models = new Map();
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
* batched re-render loop integration with reactivity.js.
|
||||
*/
|
||||
|
||||
import { requestUpdate, setCommitFn } from './reactivity.js?v=8';
|
||||
import { requestUpdate, setCommitFn } from './reactivity.js?v=9';
|
||||
import {
|
||||
_vnodeDom, createDom, getDom, patchNode, sweepDom,
|
||||
setMountFn, setUnmountFn,
|
||||
} from './vdom.js?v=8';
|
||||
import { mountComponent, unmountComponent } from './component.js?v=8';
|
||||
} from './vdom.js?v=9';
|
||||
import { mountComponent, unmountComponent } from './component.js?v=9';
|
||||
|
||||
/** Container → previous root vnodes */
|
||||
export const _renderSlots = new Map();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* navigation). Link component for client-side navigation.
|
||||
*/
|
||||
|
||||
import { reactive } from './reactivity.js?v=8';
|
||||
import { h } from './vdom.js?v=8';
|
||||
import { reactive } from './reactivity.js?v=9';
|
||||
import { h } from './vdom.js?v=9';
|
||||
|
||||
/**
|
||||
* Hash-based router.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Page-level subscribe/unsubscribe is replaced by the model layer.
|
||||
*/
|
||||
|
||||
import { refreshByTopic } from './model.js?v=8';
|
||||
import { refreshByTopic } from './model.js?v=9';
|
||||
|
||||
let _wsConn = null;
|
||||
let _wsReconnectMs = 0;
|
||||
|
||||
Reference in New Issue
Block a user