* fix(web-ui): load v3 tab content deterministically
The v3 dashboard tab panels loaded content via hx-trigger="revealed",
but the panels are shown/hidden with Alpine x-show (display toggling),
which never produces the scroll event htmx's "revealed" handler waits
for. loadTabContent tried to force it with htmx.trigger(el, 'revealed'),
but "revealed" is a synthetic scroll/observer trigger, not a dispatchable
event, so that call is a no-op. The result was an intermittently blank
panel - content appeared only when htmx's native reveal scan happened to
fire on its own.
- Replace the trigger with a custom "loadtab" event that nothing fires
spontaneously (0% native firing).
- Load panels via htmx.ajax, which issues the request directly and works
even before htmx has processed the element's triggers - unlike
htmx.trigger, which is lost if dispatched before processing.
- Poll for htmx when it hasn't finished loading from the CDN instead of
relying on a one-shot htmx:ready event that can be missed.
- Stamp data-loaded on the request promise so each panel loads once.
Verified in the emulator web UI: overview loads on every reload, tabs
lazy-load on demand, and revisiting a tab does not refetch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(web-ui): guard tab loads against stale pollers and re-entry
Address review feedback. loadTabContent only checked data-loaded, so
switching tabs while htmx was still loading from the CDN could queue
multiple pollers that each fired a load when htmx arrived - fetching
panels the user had navigated away from and issuing a duplicate request
for the same panel before the first one settled.
Add a data-loading flag (set on entry, cleared when the request settles
or the poll times out) so re-entry is a no-op, and skip the load when the
target is no longer the active tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two issues left the v3 web UI's Overview (and other Alpine-driven tabs)
blank:
1. array-table.js had two safeSetHTML(target, `...`) calls that closed the
template-literal argument with `; instead of `); — a SyntaxError that
aborts the script and halts widget registration / Alpine initialization.
2. Static assets are served `Cache-Control: public, max-age=31536000,
immutable` but were referenced without a cache-busting version (the header
comment assumed "versioning via query params", which was only ever applied
by hand to app.css). So edited JS/CSS never reached browsers — including
fix#1.
Add a Flask url_defaults hook that appends each static file's mtime as a ?v=
param to every url_for('static', ...), so changed files get a new URL and are
refetched while unchanged files keep the long immutable cache. Drop the now
redundant manual ?v= on app.css.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>