diff --git a/webui/static/app.js b/webui/static/app.js index 88bd032..2a18bbe 100644 --- a/webui/static/app.js +++ b/webui/static/app.js @@ -1,4 +1,4 @@ -import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js?v=10'; +import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js?v=11'; import DashboardPage from '/static/pages/dashboard.js?v=11'; import InterfacesPage from '/static/pages/interfaces.js?v=9'; diff --git a/webui/static/hoover/component.js b/webui/static/hoover/component.js index 1631347..954eadf 100644 --- a/webui/static/hoover/component.js +++ b/webui/static/hoover/component.js @@ -70,17 +70,19 @@ export function mountComponent(key, renderer) { if (entry) { // Re-mount: component already exists with its state. - // Don't re-run load — that re-render was triggered by a reactive update. - return; + // Abort previous in-flight load and re-run. + if (entry.abortController) entry.abortController.abort(); + entry.abortController = null; + } else { + entry = { state: pd.state }; + _mounted.set(key, entry); + pd.state.error = null; } - entry = { state: pd.state }; - _mounted.set(key, entry); - - pd.state.error = null; - if (pd.load) { - Promise.resolve().then(() => pd.load(pd.state)); + const abortController = new AbortController(); + entry.abortController = abortController; + Promise.resolve().then(() => pd.load(pd.state, abortController)); } } @@ -94,6 +96,9 @@ export function unmountComponent(key, renderer) { const pd = renderer._pageDef; + // Abort in-flight load requests so they don't mutate unmounted state + if (entry.abortController) entry.abortController.abort(); + if (pd.onUnmount) { try { pd.onUnmount(entry.state); } catch (_) {} } diff --git a/webui/static/hoover/index.js b/webui/static/hoover/index.js index 2298c40..58243fa 100644 --- a/webui/static/hoover/index.js +++ b/webui/static/hoover/index.js @@ -17,7 +17,7 @@ export { html } from './html.js?v=9'; export { render } from './render.js?v=9'; /* ── Component ───────────────────────────────────────────────── */ -export { definePage, hComp } from './component.js?v=9'; +export { definePage, hComp } from './component.js?v=10'; /* ── Router ──────────────────────────────────────────────────── */ export { createRouter, Link } from './router.js?v=9'; diff --git a/webui/static/hoover/render.js b/webui/static/hoover/render.js index 4c1e849..2368792 100644 --- a/webui/static/hoover/render.js +++ b/webui/static/hoover/render.js @@ -10,7 +10,7 @@ import { _vnodeDom, createDom, getDom, patchNode, sweepDom, setMountFn, setUnmountFn, } from './vdom.js?v=9'; -import { mountComponent, unmountComponent } from './component.js?v=9'; +import { mountComponent, unmountComponent } from './component.js?v=10'; /** Container → previous root vnodes */ export const _renderSlots = new Map(); diff --git a/webui/static/pages/backends.js b/webui/static/pages/backends.js index d9a5206..63e571a 100644 --- a/webui/static/pages/backends.js +++ b/webui/static/pages/backends.js @@ -1,4 +1,4 @@ -import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup } from '/static/hoover/index.js?v=9'; +import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup } from '/static/hoover/index.js?v=10'; import { openModal, closeModal, isModalProcessing, setModalProcessing, refreshModals } from '/static/hoover/components/modal.js?v=9'; import { _deleting } from '/static/hoover/components/data.js?v=9'; diff --git a/webui/static/pages/certs.js b/webui/static/pages/certs.js index 9516687..e37d0de 100644 --- a/webui/static/pages/certs.js +++ b/webui/static/pages/certs.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, modalVNodes, refreshModals, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll, formAction } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, modalVNodes, refreshModals, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll, formAction } from '/static/hoover/index.js?v=10'; import { isModalProcessing, setModalProcessing } from '/static/hoover/components/modal.js?v=9'; function _accountCard(account) { diff --git a/webui/static/pages/dashboard.js b/webui/static/pages/dashboard.js index 1179686..2ae9fad 100644 --- a/webui/static/pages/dashboard.js +++ b/webui/static/pages/dashboard.js @@ -1,4 +1,4 @@ -import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=10'; +import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=11'; function fmtBytes(bytes) { if (bytes === 0) return '0 B'; diff --git a/webui/static/pages/dhcp.js b/webui/static/pages/dhcp.js index abe9249..a6e0c23 100644 --- a/webui/static/pages/dhcp.js +++ b/webui/static/pages/dhcp.js @@ -1,4 +1,4 @@ -import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=9'; +import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=10'; function makeAddRange(activeZones, interfaces) { const opts = [ diff --git a/webui/static/pages/interfaces.js b/webui/static/pages/interfaces.js index 3cc8736..f5c1519 100644 --- a/webui/static/pages/interfaces.js +++ b/webui/static/pages/interfaces.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=10'; async function changeZone(name, zone, state) { const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', { diff --git a/webui/static/pages/login.js b/webui/static/pages/login.js index e5fadc9..07b2d17 100644 --- a/webui/static/pages/login.js +++ b/webui/static/pages/login.js @@ -16,7 +16,7 @@ import { handleLoginSuccess, webauthnSupported, startAuthentication, -} from '/static/hoover/index.js?v=12'; +} from '/static/hoover/index.js?v=14'; function LoginPage() { const hasWebAuthn = webauthnSupported(); @@ -214,7 +214,7 @@ const Page = definePage({ if (getAuthToken()) { try { - const res = await apiFetch('/api/auth/session'); + const res = await apiFetch('/api/auth/session', { signal: abortController?.signal }); if (res.ok) { window.location.hash = '/dashboard'; return; diff --git a/webui/static/pages/logs.js b/webui/static/pages/logs.js index ab11dd3..96e97d4 100644 --- a/webui/static/pages/logs.js +++ b/webui/static/pages/logs.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=10'; const logTabs = [ { key: 'journal', label: 'Journal' }, diff --git a/webui/static/pages/nat.js b/webui/static/pages/nat.js index caf37ab..d4b66f0 100644 --- a/webui/static/pages/nat.js +++ b/webui/static/pages/nat.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=10'; const addFwd = QuickModal({ title: 'Add Port Forward', diff --git a/webui/static/pages/notfound.js b/webui/static/pages/notfound.js index 87c8127..3846bb6 100644 --- a/webui/static/pages/notfound.js +++ b/webui/static/pages/notfound.js @@ -1,4 +1,4 @@ -import { html, PageHeader, definePage } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, definePage } from '/static/hoover/index.js?v=10'; export default definePage({ init() { diff --git a/webui/static/pages/passkeys.js b/webui/static/pages/passkeys.js index f7701e5..ce943ab 100644 --- a/webui/static/pages/passkeys.js +++ b/webui/static/pages/passkeys.js @@ -25,7 +25,7 @@ import { webauthnSupported, isModalProcessing, setModalProcessing, -} from '/static/hoover/index.js?v=12'; +} from '/static/hoover/index.js?v=14'; const state = reactive({ credentials: [], loading: true, refreshing: false, error: null }); diff --git a/webui/static/pages/proxy.js b/webui/static/pages/proxy.js index bf88f9b..942e94e 100644 --- a/webui/static/pages/proxy.js +++ b/webui/static/pages/proxy.js @@ -1,4 +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?v=9'; +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?v=10'; import { openModal, formModal, closeModal } from '/static/hoover/components/modal.js?v=9'; import { openBackendModal } from '/static/pages/backends.js?v=11'; diff --git a/webui/static/pages/rules.js b/webui/static/pages/rules.js index b771d75..8c9c15a 100644 --- a/webui/static/pages/rules.js +++ b/webui/static/pages/rules.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=10'; const addRule = QuickModal({ title: 'Add Rich Rule', diff --git a/webui/static/pages/users.js b/webui/static/pages/users.js index 3af323c..fa92e66 100644 --- a/webui/static/pages/users.js +++ b/webui/static/pages/users.js @@ -5,8 +5,8 @@ * Requires auth: rw permission. */ -import { h, definePage, reactive } from '/static/hoover/index.js?v=11'; -import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=11'; +import { h, definePage, reactive } from '/static/hoover/index.js?v=13'; +import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=13'; const SUBSYSTEMS = [ { key: 'firewall', label: 'Firewall' }, @@ -40,8 +40,8 @@ async function loadUsers(abortController) { try { const [usersRes, countsRes] = await Promise.all([ - apiFetch('/api/auth/users'), - apiFetch('/api/auth/webauthn/credential-counts'), + apiFetch('/api/auth/users', { signal: abortController?.signal }), + apiFetch('/api/auth/webauthn/credential-counts', { signal: abortController?.signal }), ]); if (abortController?.signal?.aborted) return; @@ -55,11 +55,15 @@ async function loadUsers(abortController) { state.error = usersRes.error || 'Failed to load users'; } } catch (e) { - state.error = 'Failed to load users'; + if (!abortController?.signal?.aborted) { + state.error = 'Failed to load users'; + } + } finally { + if (!abortController?.signal?.aborted) { + state.loading = false; + state.refreshing = false; + } } - state.loading = false; - state.refreshing = false; -} function permissionLevel(perms, subsystem) { return perms[subsystem] || '—'; diff --git a/webui/static/pages/wireguard.js b/webui/static/pages/wireguard.js index 636d6fc..6ab09ff 100644 --- a/webui/static/pages/wireguard.js +++ b/webui/static/pages/wireguard.js @@ -1,5 +1,5 @@ /** WireGuard page — tunnel & peer management. */ -import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG } from '/static/hoover/index.js?v=10'; /* ── LAN detection helper ────────────────────────────────────── */ function getLanSubnets() { diff --git a/webui/static/pages/zones.js b/webui/static/pages/zones.js index 870746c..5685acf 100644 --- a/webui/static/pages/zones.js +++ b/webui/static/pages/zones.js @@ -1,4 +1,4 @@ -import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=9'; +import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=10'; const addZone = QuickModal({ title: 'Add Zone',