From 575cf06a4b1c386a81e17988378660965489ce0e Mon Sep 17 00:00:00 2001 From: Mike Teehan Date: Tue, 30 Jun 2026 04:01:41 +0000 Subject: [PATCH] hoover: move ToastContainer to separate module, add reactive re-render, bump module cache v8 --- webui/static/app.js | 2 +- webui/static/hoover/api.js | 27 ++++-------------------- webui/static/hoover/components/data.js | 8 +++---- webui/static/hoover/components/modal.js | 8 +++---- webui/static/hoover/components/toast.js | 20 ++++++++++-------- webui/static/hoover/index.js | 28 ++++++++++++------------- webui/static/index.html | 2 +- webui/static/pages/certs.js | 2 +- webui/static/pages/dashboard.js | 2 +- webui/static/pages/dhcp.js | 2 +- webui/static/pages/interfaces.js | 2 +- webui/static/pages/logs.js | 2 +- webui/static/pages/nat.js | 2 +- webui/static/pages/notfound.js | 2 +- webui/static/pages/proxy.js | 4 ++-- webui/static/pages/rules.js | 2 +- webui/static/pages/wireguard.js | 2 +- webui/static/pages/zones.js | 2 +- 18 files changed, 51 insertions(+), 68 deletions(-) diff --git a/webui/static/app.js b/webui/static/app.js index eec5e4c..67ea9f7 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 } from '/static/hoover/index.js?v=8'; +import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive } from '/static/hoover/index.js?v=9'; import DashboardPage from '/static/pages/dashboard.js?v=8'; import InterfacesPage from '/static/pages/interfaces.js?v=8'; diff --git a/webui/static/hoover/api.js b/webui/static/hoover/api.js index 63493e8..512b07b 100644 --- a/webui/static/hoover/api.js +++ b/webui/static/hoover/api.js @@ -3,11 +3,10 @@ * * JSON-friendly fetch wrapper with automatic header management. * Toast notification system with auto-dismiss. - * ToastContainer component for rendering queued toasts. */ -import { h } from './vdom.js?v=7'; -import { modelFetch } from './model.js?v=7'; +import { modelFetch } from './model.js?v=8'; +import { requestUpdate } from './reactivity.js?v=8'; /** * JSON-friendly fetch wrapper. @@ -65,6 +64,7 @@ const _toastIds = { next: 1 }; export function toast(message, type = 'info', duration = 4000) { const id = _toastIds.next++; _toasts.push({ id, message, type, createdAt: Date.now(), duration }); + requestUpdate(); if (duration > 0) setTimeout(() => dismissToast(id), duration); return id; @@ -76,26 +76,7 @@ export function toast(message, type = 'info', duration = 4000) { export function dismissToast(id) { const idx = _toasts.findIndex(t => t.id === id); if (idx !== -1) _toasts.splice(idx, 1); -} - -/** - * Render the queued toast notifications. - * - * @returns {VNode} – Toast container (empty text node when no toasts) - */ -export function ToastContainer() { - if (!_toasts.length) return h('#text', ''); - - const clsMap = { info: 'toast-info', success: 'toast-success', error: 'toast-error', warning: 'toast-warning' }; - - return h('div', { class: 'toast-container' }, - ..._toasts.map(t => - h('div', { class: `toast ${clsMap[t.type] || clsMap.info}`, 'on:click': () => dismissToast(t.id) }, - h('span', null, t.message), - h('button', { class: 'toast-close', 'on:click': (e) => { e.stopPropagation(); dismissToast(t.id); } }, '\u00d7'), - ), - ), - ); + requestUpdate(); } /** diff --git a/webui/static/hoover/components/data.js b/webui/static/hoover/components/data.js index f9a90cd..22e3b88 100644 --- a/webui/static/hoover/components/data.js +++ b/webui/static/hoover/components/data.js @@ -4,10 +4,10 @@ * Data display components: Badge, StatusDot, Empty, Card. */ -import { h } from '../vdom.js?v=7'; -import { esc } from '../helpers.js?v=7'; -import { apiFetch, toast } from '../api.js?v=7'; -import { modelFetch } from '../model.js?v=7'; +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'; /** * Colored badge/span. diff --git a/webui/static/hoover/components/modal.js b/webui/static/hoover/components/modal.js index 6570d50..a8ba76e 100644 --- a/webui/static/hoover/components/modal.js +++ b/webui/static/hoover/components/modal.js @@ -6,10 +6,10 @@ * avoid fighting with the main render cycle. */ -import { esc } from '../helpers.js?v=7'; -import { att_esc } from '../helpers.js?v=7'; -import { apiSubmit } from '../api.js?v=7'; -import { createDom } from '../vdom.js?v=7'; +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'; /** * Render Hoover VNodes into a modal content element. diff --git a/webui/static/hoover/components/toast.js b/webui/static/hoover/components/toast.js index cdcf741..514a371 100644 --- a/webui/static/hoover/components/toast.js +++ b/webui/static/hoover/components/toast.js @@ -5,8 +5,8 @@ * Uses the toast/dismissToast state from api.js. */ -import { h } from '../vdom.js?v=7'; -import { _toasts, dismissToast } from '../api.js?v=7'; +import { h } from '../vdom.js?v=8'; +import { _toasts, dismissToast } from '../api.js?v=8'; /** * Render all pending toast notifications. @@ -23,17 +23,19 @@ export function ToastContainer() { warning: 'toast-warning', }; - return h('div', { class: 'toast-container' }, + return h('div', { class: 'toast' }, ..._toasts.map(t => h('div', { - class: `toast ${clsMap[t.type] || clsMap.info}`, + class: `toast-message ${clsMap[t.type] || clsMap.info}`, 'on:click': () => dismissToast(t.id), }, - h('span', null, t.message), - h('button', { - class: 'toast-close', - 'on:click': (e) => { e.stopPropagation(); dismissToast(t.id); }, - }, '\u00d7'), + h('span', { class: 'toast-text' }, t.message), + h('div', { class: 'toast-actions' }, + h('button', { + class: 'toast-btn toast-close', + 'on:click': (e) => { e.stopPropagation(); dismissToast(t.id); }, + }, '\u00d7'), + ), ), ), ); diff --git a/webui/static/hoover/index.js b/webui/static/hoover/index.js index e24f2b9..1e5f8d6 100644 --- a/webui/static/hoover/index.js +++ b/webui/static/hoover/index.js @@ -5,43 +5,43 @@ */ /* ── Reactivity ──────────────────────────────────────────────── */ -export { reactive, requestUpdate } from './reactivity.js?v=7'; +export { reactive, requestUpdate } from './reactivity.js?v=8'; /* ── VDOM ────────────────────────────────────────────────────── */ -export { h } from './vdom.js?v=7'; +export { h } from './vdom.js?v=8'; /* ── HTM ──────────────────────────────────────────────────────── */ -export { html } from './html.js?v=7'; +export { html } from './html.js?v=8'; /* ── Render ──────────────────────────────────────────────────── */ -export { render } from './render.js?v=7'; +export { render } from './render.js?v=8'; /* ── Component ───────────────────────────────────────────────── */ -export { definePage, hComp } from './component.js?v=7'; +export { definePage, hComp } from './component.js?v=8'; /* ── Router ──────────────────────────────────────────────────── */ -export { createRouter, Link } from './router.js?v=7'; +export { createRouter, Link } from './router.js?v=8'; /* ── WebSocket ───────────────────────────────────────────────── */ -export { connect, onMessage } from './websocket.js?v=7'; +export { connect, onMessage } from './websocket.js?v=8'; /* ── API & Toast ─────────────────────────────────────────────── */ -export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad } from './api.js?v=7'; +export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad } from './api.js?v=8'; /* ── Model ───────────────────────────────────────────────────── */ -export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js?v=7'; +export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js?v=8'; /* ── Helpers ─────────────────────────────────────────────────── */ -export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js?v=7'; +export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js?v=8'; /* ── UI Components: Layout ───────────────────────────────────── */ -export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js?v=7'; +export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js?v=8'; /* ── UI Components: Data ─────────────────────────────────────── */ -export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js?v=7'; +export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js?v=8'; /* ── UI Components: Modal ────────────────────────────────────── */ -export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal } from './components/modal.js?v=7'; +export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal } from './components/modal.js?v=8'; /* ── UI Components: Toast ────────────────────────────────────── */ -export { ToastContainer } from './components/toast.js?v=7'; +export { ToastContainer } from './components/toast.js?v=8'; diff --git a/webui/static/index.html b/webui/static/index.html index da4d38b..c69eb0e 100644 --- a/webui/static/index.html +++ b/webui/static/index.html @@ -15,6 +15,6 @@ - + diff --git a/webui/static/pages/certs.js b/webui/static/pages/certs.js index ecaf2ad..7bbc9af 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 } from '/static/hoover/index.js?v=7'; +import { html, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, modalVNodes, refreshModals, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll } from '/static/hoover/index.js?v=8'; function _accountCard(account) { if (!account || !account.registered) { diff --git a/webui/static/pages/dashboard.js b/webui/static/pages/dashboard.js index e8d523b..473ed91 100644 --- a/webui/static/pages/dashboard.js +++ b/webui/static/pages/dashboard.js @@ -1,4 +1,4 @@ -import { html, PageHeader, definePage, getModel, renderGuard, ServiceStatus, StatCard } from '/static/hoover/index.js?v=7'; +import { html, PageHeader, definePage, getModel, renderGuard, ServiceStatus, StatCard } from '/static/hoover/index.js?v=8'; export default definePage({ init() { diff --git a/webui/static/pages/dhcp.js b/webui/static/pages/dhcp.js index 0a31672..e3bbc8a 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=7'; +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=8'; function makeAddRange(activeZones, interfaces) { const opts = [ diff --git a/webui/static/pages/interfaces.js b/webui/static/pages/interfaces.js index a7c31e5..702e65d 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=7'; +import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=8'; async function changeZone(name, zone, state) { const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', { diff --git a/webui/static/pages/logs.js b/webui/static/pages/logs.js index d3d9731..7559978 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=7'; +import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=8'; const logTabs = [ { key: 'journal', label: 'Journal' }, diff --git a/webui/static/pages/nat.js b/webui/static/pages/nat.js index 9fcc4ac..7c87d8d 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=7'; +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=8'; const addFwd = QuickModal({ title: 'Add Port Forward', diff --git a/webui/static/pages/notfound.js b/webui/static/pages/notfound.js index 1d43d46..80d677a 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=7'; +import { html, PageHeader, definePage } from '/static/hoover/index.js?v=8'; export default definePage({ init() { diff --git a/webui/static/pages/proxy.js b/webui/static/pages/proxy.js index a8b4860..586b790 100644 --- a/webui/static/pages/proxy.js +++ b/webui/static/pages/proxy.js @@ -1,5 +1,5 @@ -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?v=7'; -import { openModal, formModal, closeModal } from '/static/hoover/components/modal.js?v=7'; +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?v=8'; +import { openModal, formModal, closeModal } from '/static/hoover/components/modal.js?v=8'; // --------------------------------------------------------------------------- // Cert lookup map from ACME state keyed by domain name diff --git a/webui/static/pages/rules.js b/webui/static/pages/rules.js index ef06928..f284fac 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=7'; +import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=8'; const addRule = QuickModal({ title: 'Add Rich Rule', diff --git a/webui/static/pages/wireguard.js b/webui/static/pages/wireguard.js index 6b6191d..3faa291 100644 --- a/webui/static/pages/wireguard.js +++ b/webui/static/pages/wireguard.js @@ -1,4 +1,4 @@ -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 } from '/static/hoover/index.js?v=7'; +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 } from '/static/hoover/index.js?v=8'; const addPeer = QuickModal({ title: 'Add WireGuard Peer', diff --git a/webui/static/pages/zones.js b/webui/static/pages/zones.js index 4ce033d..8626d49 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=7'; +import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=8'; const addZone = QuickModal({ title: 'Add Zone',