Remove query-string cache-busting from static assets

Drop ?v=N version pins from all JS imports and HTML <link>/<script> tags.
Cache invalidation is now handled solely by server-side cache-control headers.
Update docs and AGENTS.md accordingly.
This commit is contained in:
2026-07-28 13:50:22 +00:00
parent c943d17bb3
commit 3de82e3b9b
35 changed files with 96 additions and 95 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ Conventions:
- `h()` builds VNodes with `on:click` prefix. `html` tag (htm) templates use camelCase `onClick` (adapter translates).
- State always has `loading`, `refreshing`, `error` plus data. `load()` receives `(state, abortController, entry)`.
- `openModal` + `formModal` for dialogs; `apiSubmit()` for form submission.
- No build step — ES modules served raw. Assets versioned via `?v=N` query string.
- No build step — ES modules served raw. Cache controlled via HTTP headers.
### Daemon Endpoints
+1 -1
View File
@@ -348,7 +348,7 @@ Each route is a `definePage()` component with reactive state, async data loading
### No Build Step
All JavaScript is served as ES modules. The `?v=N` query string param version-pins asset imports for cache invalidation. Dev mode (`VACUUM_WALL_DEV`) disables aggressive static asset caching.
All JavaScript is served as ES modules. Cache invalidation is handled via HTTP cache-control headers. Dev mode (`VACUUM_WALL_DEV`) disables aggressive static asset caching.
### WebSocket Broadcast
+4 -3
View File
@@ -55,7 +55,7 @@ The app starts from `webui/static/app.js`:
```javascript
import { h, render, Link, hComp, ToastContainer, connect, apiFetch,
modelRegister, modelFetch, reactive } from '/static/hoover/index.js?v=8';
modelRegister, modelFetch, reactive } from '/static/hoover/index.js';
// 1. Register subsystem models
modelRegister('firewall', {
@@ -1151,9 +1151,10 @@ Render the toast notification container. Include in the main render root. See AP
| `parseZones(data)` | Parse zone data from API responses into a flat string array |
| `downloadBlob(blob, filename)` | Trigger a browser file download from a Blob |
## Versioned Imports
## Static Asset Caching
Static assets in `app.js` are imported with querystring version pins (e.g., `?v=8`) to invalidate browser cache when the framework changes. Page imports also include version pins (e.g., `?v=9`). The server handles caching headers; the version query string ensures browser cache invalidation.
The server handles caching headers for static assets. Browser cache invalidation is managed
through server-side cache-control headers rather than query string version pins.
Dev mode (`VACUUM_WALL_DEV` set) disables aggressive static asset caching.
+13 -13
View File
@@ -1,17 +1,17 @@
import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js?v=11';
import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js';
import DashboardPage from '/static/pages/dashboard.js?v=11';
import InterfacesPage from '/static/pages/interfaces.js?v=9';
import ZonesPage from '/static/pages/zones.js?v=9';
import RulesPage from '/static/pages/rules.js?v=9';
import NatPage from '/static/pages/nat.js?v=9';
import DhcpPage from '/static/pages/dhcp.js?v=9';
import ProxyPage from '/static/pages/proxy.js?v=10';
import BackendsPage from '/static/pages/backends.js?v=10';
import CertsPage from '/static/pages/certs.js?v=9';
import WireguardPage from '/static/pages/wireguard.js?v=9';
import LogsPage from '/static/pages/logs.js?v=9';
import NotFoundPage from '/static/pages/notfound.js?v=9';
import DashboardPage from '/static/pages/dashboard.js';
import InterfacesPage from '/static/pages/interfaces.js';
import ZonesPage from '/static/pages/zones.js';
import RulesPage from '/static/pages/rules.js';
import NatPage from '/static/pages/nat.js';
import DhcpPage from '/static/pages/dhcp.js';
import ProxyPage from '/static/pages/proxy.js';
import BackendsPage from '/static/pages/backends.js';
import CertsPage from '/static/pages/certs.js';
import WireguardPage from '/static/pages/wireguard.js';
import LogsPage from '/static/pages/logs.js';
import NotFoundPage from '/static/pages/notfound.js';
import LoginPage from '/static/pages/login.js';
import PasskeysPage from '/static/pages/passkeys.js';
import UsersPage from '/static/pages/users.js';
+3 -3
View File
@@ -6,9 +6,9 @@
* Modal processing guard for async form submissions.
*/
import { modelFetch } from './model.js?v=10';
import { requestUpdate } from './reactivity.js?v=9';
import { isModalProcessing, setModalProcessing, refreshModals } from './components/modal.js?v=9';
import { modelFetch } from './model.js';
import { requestUpdate } from './reactivity.js';
import { isModalProcessing, setModalProcessing, refreshModals } from './components/modal.js';
/**
* Global state — shared with auth.js component.
+3 -3
View File
@@ -15,9 +15,9 @@
* });
*/
import { reactive } from './reactivity.js?v=9';
import { h } from './vdom.js?v=9';
import { _compExpandedCache } from './render.js?v=9';
import { reactive } from './reactivity.js';
import { h } from './vdom.js';
import { _compExpandedCache } from './render.js';
/** 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=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';
import { h } from '../vdom.js';
import { html } from '../html.js';
import { reactive } from '../reactivity.js';
import { apiFetch, toast } from '../api.js';
import { modelFetch } from '../model.js';
import { openModal, closeModal, modalVNodes, isModalProcessing, setModalProcessing, refreshModals } from './modal.js';
export const SUBSYSTEM_LIST = [
{ key: 'firewall', label: 'Firewall' },
+1 -1
View File
@@ -4,7 +4,7 @@
* Token refresh scheduler, session check, logout.
*/
import { apiFetch, setAuthToken, clearAuthTokens, redirectLogin, getAuthToken, tryRefreshToken, toast } from '../api.js?v=12';
import { apiFetch, setAuthToken, clearAuthTokens, redirectLogin, getAuthToken, tryRefreshToken, toast } from '../api.js';
/**
* Schedule a token refresh based on the access token TTL stored in sessionStorage.
+5 -5
View File
@@ -4,11 +4,11 @@
* Data display components: Badge, StatusDot, Empty, Card.
*/
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';
import { h } from '../vdom.js';
import { esc } from '../helpers.js';
import { apiFetch, toast } from '../api.js';
import { modelFetch } from '../model.js';
import { requestUpdate } from '../reactivity.js';
const _actionPending = new Map();
const _confirmPending = new Map();
+3 -3
View File
@@ -4,9 +4,9 @@
* Layout components: PageHeader, Tabs, SectionTitle, DataTableSection, ActionGroup.
*/
import { h } from '../vdom.js?v=9';
import { Table } from './data.js?v=9';
import { collectLoadingModels } from '../model.js?v=9';
import { h } from '../vdom.js';
import { Table } from './data.js';
import { collectLoadingModels } from '../model.js';
/**
* Page header with title, optional subtitle, and action buttons.
+3 -3
View File
@@ -6,9 +6,9 @@
* avoid fighting with the main render cycle.
*/
import { esc, att_esc } from '../helpers.js?v=9';
import { apiSubmit } from '../api.js?v=9';
import { createDom } from '../vdom.js?v=9';
import { esc, att_esc } from '../helpers.js';
import { apiSubmit } from '../api.js';
import { createDom } from '../vdom.js';
/**
* Render Hoover VNodes into a modal content element.
+2 -2
View File
@@ -5,8 +5,8 @@
* Uses qrcode-svg library for generation.
*/
import { h } from '../vdom.js?v=9';
import { esc } from '../helpers.js?v=9';
import { h } from '../vdom.js';
import { esc } from '../helpers.js';
import QRCode from '../../../vendor/qrcode-svg-1.1.0.js';
/**
+2 -2
View File
@@ -5,8 +5,8 @@
* Uses the toast/dismissToast state from api.js.
*/
import { h } from '../vdom.js?v=9';
import { _toasts, dismissToast } from '../api.js?v=9';
import { h } from '../vdom.js';
import { _toasts, dismissToast } from '../api.js';
/**
* Render all pending toast notifications.
+1 -1
View File
@@ -1,4 +1,4 @@
import htm from '../../vendor/htm.js';
import { htmAdapter } from './vdom.js?v=9';
import { htmAdapter } from './vdom.js';
export const html = htm.bind(htmAdapter);
+17 -17
View File
@@ -5,52 +5,52 @@
*/
/* ── Reactivity ──────────────────────────────────────────────── */
export { reactive, requestUpdate } from './reactivity.js?v=9';
export { reactive, requestUpdate } from './reactivity.js';
/* ── VDOM ────────────────────────────────────────────────────── */
export { h } from './vdom.js?v=9';
export { h } from './vdom.js';
/* ── HTM ──────────────────────────────────────────────────────── */
export { html } from './html.js?v=9';
export { html } from './html.js';
/* ── Render ──────────────────────────────────────────────────── */
export { render } from './render.js?v=9';
export { render } from './render.js';
/* ── Component ───────────────────────────────────────────────── */
export { definePage, hComp } from './component.js?v=10';
export { definePage, hComp } from './component.js';
/* ── Router ──────────────────────────────────────────────────── */
export { createRouter, Link } from './router.js?v=9';
export { createRouter, Link } from './router.js';
/* ── WebSocket ───────────────────────────────────────────────── */
export { connect, onMessage } from './websocket.js?v=10';
export { connect, onMessage } from './websocket.js';
/* ── API & Toast ─────────────────────────────────────────────── */
export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad, formAction, setAuthToken, clearAuthTokens, getAuthToken } from './api.js?v=12';
export { apiFetch, toast, dismissToast, apiSubmit, checkAbort, poll, refactorLoad, formAction, setAuthToken, clearAuthTokens, getAuthToken } from './api.js';
/* ── UI Components: Auth ──────────────────────────────────────── */
export { scheduleTokenRefresh, cancelTokenRefresh, checkSession, logout, initAuth, handleLoginSuccess, webauthnSupported, startRegistration, startAuthentication } from './components/auth.js?v=2';
export { scheduleTokenRefresh, cancelTokenRefresh, checkSession, logout, initAuth, handleLoginSuccess, webauthnSupported, startRegistration, startAuthentication } from './components/auth.js';
/* ── Model ───────────────────────────────────────────────────── */
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js?v=9';
export { modelRegister, getModel, modelFetch, collectLoadingModels } from './model.js';
/* ── Helpers ─────────────────────────────────────────────────── */
export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js?v=9';
export { esc, att_esc, enc, $val, parseZones, downloadBlob } from './helpers.js';
/* ── UI Components: Layout ───────────────────────────────────── */
export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js?v=9';
export { PageHeader, renderGuard, renderGuardMulti, Tabs, SectionTitle, ActionGroup, DataTableSection } from './components/layout.js';
/* ── UI Components: Data ─────────────────────────────────────── */
export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js?v=9';
export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, certStatusBadge, serviceStatusBadge, StatCard, StatusText, ServiceStatus, ActionCell, MonoText, ZoneSelect } from './components/data.js';
/* ── UI Components: Modal ────────────────────────────────────── */
export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal, isModalProcessing, setModalProcessing } from './components/modal.js?v=10';
export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal, isModalProcessing, setModalProcessing } from './components/modal.js';
/* ── UI Components: Apply ────────────────────────────────────── */
export { ApplyConfirm } from './components/applyconfirm.js?v=9';
export { ApplyConfirm } from './components/applyconfirm.js';
/* ── UI Components: Toast ────────────────────────────────────── */
export { ToastContainer } from './components/toast.js?v=9';
export { ToastContainer } from './components/toast.js';
/* ── UI Components: QR Code ──────────────────────────────────── */
export { qrSVG, QRCodeVNode, LogoUpload } from './components/qr.js?v=9';
export { qrSVG, QRCodeVNode, LogoUpload } from './components/qr.js';
+1 -1
View File
@@ -13,7 +13,7 @@
* collectLoadingModels(...models) — combine loading/refreshing/error
*/
import { reactive } from './reactivity.js?v=9';
import { reactive } from './reactivity.js';
/** Registered models: name → { model, subsystem, fetch } */
const _models = new Map();
+3 -3
View File
@@ -5,12 +5,12 @@
* batched re-render loop integration with reactivity.js.
*/
import { requestUpdate, setCommitFn } from './reactivity.js?v=9';
import { requestUpdate, setCommitFn } from './reactivity.js';
import {
_vnodeDom, createDom, getDom, patchNode, sweepDom,
setMountFn, setUnmountFn,
} from './vdom.js?v=9';
import { mountComponent, unmountComponent } from './component.js?v=10';
} from './vdom.js';
import { mountComponent, unmountComponent } from './component.js';
/** Container → previous root vnodes */
export const _renderSlots = new Map();
+2 -2
View File
@@ -5,8 +5,8 @@
* navigation). Link component for client-side navigation.
*/
import { reactive } from './reactivity.js?v=9';
import { h } from './vdom.js?v=9';
import { reactive } from './reactivity.js';
import { h } from './vdom.js';
/**
* Hash-based router.
+2 -2
View File
@@ -6,8 +6,8 @@
* Page-level subscribe/unsubscribe is replaced by the model layer.
*/
import { refreshByTopic } from './model.js?v=9';
import { tryRefreshToken, redirectLogin } from './api.js?v=12';
import { refreshByTopic } from './model.js';
import { tryRefreshToken, redirectLogin } from './api.js';
let _wsConn = null;
let _wsReconnectMs = 0;
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vacuum Wall</title>
<link rel="stylesheet" href="/static/style.css?v=9">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div id="app">
@@ -15,6 +15,6 @@
</div>
<div id="modal-root"></div>
<script>window.__WS_URL__ = "__WS_URL_PLACEHOLDER__"</script>
<script type="module" src="/static/app.js?v=11"></script>
<script type="module" src="/static/app.js"></script>
</body>
</html>
+3 -3
View File
@@ -1,6 +1,6 @@
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';
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';
import { openModal, closeModal, isModalProcessing, setModalProcessing, refreshModals } from '/static/hoover/components/modal.js';
import { _deleting } from '/static/hoover/components/data.js';
// ---------------------------------------------------------------------------
// Build a datalist element from DHCP leases
+2 -2
View File
@@ -1,5 +1,5 @@
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';
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';
import { isModalProcessing, setModalProcessing } from '/static/hoover/components/modal.js';
function _accountCard(account) {
if (!account || !account.registered) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=11';
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js';
function fmtBytes(bytes) {
if (bytes === 0) return '0 B';
+1 -1
View File
@@ -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=10';
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';
function makeAddRange(activeZones, interfaces) {
const opts = [
+1 -1
View File
@@ -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=10';
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js';
async function changeZone(name, zone, state) {
const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', {
+1 -1
View File
@@ -16,7 +16,7 @@ import {
handleLoginSuccess,
webauthnSupported,
startAuthentication,
} from '/static/hoover/index.js?v=14';
} from '/static/hoover/index.js';
function LoginPage() {
const hasWebAuthn = webauthnSupported();
+1 -1
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=10';
import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js';
const logTabs = [
{ key: 'journal', label: 'Journal' },
+1 -1
View File
@@ -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=10';
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';
const addFwd = QuickModal({
title: 'Add Port Forward',
+1 -1
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, definePage } from '/static/hoover/index.js?v=10';
import { html, PageHeader, definePage } from '/static/hoover/index.js';
export default definePage({
init() {
+1 -1
View File
@@ -25,7 +25,7 @@ import {
webauthnSupported,
isModalProcessing,
setModalProcessing,
} from '/static/hoover/index.js?v=14';
} from '/static/hoover/index.js';
const state = reactive({ credentials: [], loading: true, refreshing: false, error: null });
+3 -3
View File
@@ -1,6 +1,6 @@
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';
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 { openBackendModal } from '/static/pages/backends.js';
function certLookup(acmeData) {
const m = {};
+1 -1
View File
@@ -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=10';
import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js';
const addRule = QuickModal({
title: 'Add Rich Rule',
+2 -2
View File
@@ -5,8 +5,8 @@
* Requires auth: rw permission.
*/
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';
import { h, definePage, reactive } from '/static/hoover/index.js';
import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js';
const SUBSYSTEMS = [
{ key: 'firewall', label: 'Firewall' },
+1 -1
View File
@@ -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=10';
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';
/* ── LAN detection helper ────────────────────────────────────── */
function getLanSubnets() {
+1 -1
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=10';
import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js';
const addZone = QuickModal({
title: 'Add Zone',