Lazy-loaded JS/CSS assets and their ?v= versioning
The dashboard embeds its entire frontend locally (go:embed in
webui.go, no CDN dependencies) and
serves every file under /static/ with
w.Header().Set("Cache-Control", "public, max-age=86400")
— see Static() in
webui.go:572. There is no
content hashing in the filename and no immutable. A browser (and any proxy
in front of it, e.g. Caddy) therefore treats a loaded history.js as valid for
up to a day, even though the deployment replaced the file long ago.
The only lever against this is a manually maintained query string ?v=N on
the src/href. When N changes it is a new URL as far as the cache is
concerned and gets reloaded. Almost all of these query strings live centrally in
base.html. Exception:
Gridstack is no longer part of the editor assets since the move to the real CSS
grid — edit mode moves and scales tiles itself so that the view and the editor
show the same geometry.
The overview’s editor assets (js/layout-editor.js, css/layout-editor.css)
are declared in data-editor-script / data-editor-css on #overview-panel in
overview.html and carry
their ?v= there. The remaining panel fragments reference no versioned assets.
For the lazy-loaded panel assets there is a second reason:
loadSingleAsset() in
dashboard.js
deduplicates via this.loadedScripts / this.loadedStyles — sets keyed on the
exact URL string (including ?v=). As long as the URL stays the same, the
asset counts as “already loaded” within a session.
The three loading channels in base.html
<script defer>at the end of<body>(lines 154–181). Runs on every full page build after parsing, in document order. Not “lazy per panel”, but deferred. Some carry?v=.data-panel-script/data-panel-csson the<section class="panel">(lines 103–114). Truly lazy:dashboard.jsloadPanel()fetches them only when the panel is activated for the first time. Comma-separated list, loaded sequentially (dependent scripts such as Popper before Tippy).EnergyCardScripts— the seven energy-card scripts (the six energy-chart variants plusbattery-status.js) plusenergy-model.jsandbattery-card-core.js, whichwebui.go(energyCardScripts, webui.go:361) injects conditionally as<script defer>depending on the configured card types.battery-card-core.jssits inbase.htmlimmediately before theloop, in the sameblock asenergy-model.js— deliberately: it is the host-independent core of the battery card and would otherwise load on every page, even without abattery_statustile in the layout.
Current version state
As of 2026-09-16, read from base.html and overview.html. “–” means: no
?v=, relies solely on the 1-day cache.
Global
| Asset | ?v= |
|---|---|
css/base.css |
21 |
Deferred <script> block (channel 1)
| Asset | ?v= |
|---|---|
js/notify.js |
– |
js/theme.js |
– |
js/layout-fill.js |
– |
js/device-tile.js |
– |
js/entity-values.js |
– |
js/overview-values.js |
1 |
js/device-tile-values.js |
– |
js/compact-card-values.js |
– |
js/energy-presentation.js |
– |
js/energy-flow.js |
– |
js/history-rollup.js |
1 |
js/history-store.js |
2 |
js/history-coverage.js |
1 |
js/history-exchange.js |
1 |
js/history-maintenance.js |
1 |
js/history-recorder.js |
8 |
js/notifications.js |
1 |
js/dashboard.js |
14 |
js/overview.page.js |
5 |
js-deps/htmx.min.js |
– |
js-deps/alpine-collapse.min.js |
– |
js-deps/alpine.min.js |
– |
Conditional energy-card scripts (channel 3)
| Asset | ?v= |
|---|---|
js/energy-model.js |
– |
js/battery-card-core.js |
2 |
js/battery-status.js |
1 |
js/energy-band.js, js/energy-ring.js, js/energy-board.js, js/energy-day.js, js/energy-schema.js, js/energy-status.js |
– |
Lazy panel assets (channel 2)
| Panel | Scripts (?v=) |
CSS (?v=) |
|---|---|---|
devices-panel |
js-deps/popper.min.js –, js-deps/tippy.umd.min.js – |
css/tippy.css – |
history-panel |
js-deps/apexcharts.min.js –, js-deps/flatpickr.min.js 1, js-deps/flatpickr-l10n-de.js 1, js/history-export.js 1, js/energy-model.js –, js/history.js 9 |
css/flatpickr.min.css 1, css/flatpickr.css 1, css/history.css 3 |
diagnostics-panel |
– | css/diagnostics.css 1 |
config-panel |
js/revisions.js –, js/schema-form.js –, js/config.page.js 2 |
css/manager.css 22 |
energy-panel |
js/revisions.js –, js/energy.page.js 2 |
css/manager.css 22 |
devicemap-panel |
js-deps/cytoscape.min.js –, js/revisions.js –, js/devicemap.page.js 7 |
css/manager.css 22 |
settings-panel |
js-deps/choices.min.js –, js/revisions.js –, js/schema-form.js –, js/settings.page.js 7, js/mqtt.page.js 3, js/tailscale.page.js 1, js/systemconfig.page.js 2 |
css/choices.min.css –, css/choices.css 2, css/manager.css 22, css/settings-controls.css 5 |
automations-panel |
js/automations.page.js 2 |
css/manager.css 22, css/automations.css 3 |
The former layout-panel is gone (the “layout edit mode” work): the layout
editor is now an edit mode of the overview, and its assets load through their own
channel (see below).
Overview editor assets (data-editor-* on #overview-panel)
Loaded by overview.page.js editorAssetsReady() on the first switch into edit
mode. Declared in data-editor-script / data-editor-css on #overview-panel
in overview.html — the
only place outside base.html with versioned assets.
| Channel | Assets (?v=) |
|---|---|
data-editor-script |
js-deps/choices.min.js –, js/revisions.js –, js/layout-editor.js 11 |
data-editor-css |
css/choices.min.css –, css/choices.css 1, css/layout-editor.css 9 |
Assets referenced from multiple places
On a bump these must be raised in every reference at once, otherwise one place pulls the old file from the cache:
css/manager.css– 5 panels (config,energy,devicemap,settings,automations)js/energy-model.js– channel 2 (history-panel) and channel 3 (energy cards)js/revisions.js,js/schema-form.js– several manager panels and the overview editor assetsjs/choices.min.js– overview editor assets andsettings-panelcss/choices.css– overview editor assets andsettings-panel(reconciled 2026-09-11: both now carry a?v=, previously onlysettings-paneldid)
Conventions
- The counter is an integer and is incremented by exactly 1.
js/dashboard.js?v=v4(cleaned up to5on 2026-09-03) is a leftover; switch it to a plain number (e.g.?v=5) on the next bump. - File had no
?v=yet → append?v=1on the first substantive change. - Vendored
js-deps/*get a?v=only when the bundled library version is swapped. The upstream version of each library is recorded next to it injs-deps/and in the dashboard’s internal design notes; here it is only the cache-bust. - The
?v=query is independent ofdashboard/VERSION.VERSIONis the release triple on the settings page, whose patch position thepre-commithook auto-increments on every commit tomain(AGENTS.md). Nobody maintains the?v=tags automatically — only this file andbase.html, by hand.
The installer/webui/ module: one central mark instead of many hand-kept ones
The installer’s UI lives in its own Go module, installer/webui/, and both
hosts (the installer binary and — from Plan D on — the dashboard) embed it.
Its assets do not follow the per-file ?v=N convention above. Instead
webui.AssetVersion() reads installer/webui/VERSION and the shell template
appends ?v=<version> to every JS and CSS URL.
Why the difference: installer/webui/VERSION is patch-bumped by CI on every
PR that touches installer/webui/ (scripts/version/components.sh), so a
changed asset busts its cache without anyone remembering to. The dashboard’s
own assets keep their hand-kept marks — retrofitting them would mean
invalidating every asset on every dashboard release, which the lazy-panel
loading above makes expensive.
The vendored installer/webui/static/js-deps/alpine.min.js is the same build
as the dashboard’s. Update both in the same PR: from Plan D on they share a
binary.
The installer UI’s look is pinned to the drafts copied into
installer/webui/test/reference/. installer.css and screens.css contain the draft
CSS verbatim inside /* == Vorlage: … == */ blocks; npm test fails when a
rule inside a block drifts from its draft or when an addition after a block
overrides a property the draft sets.
Mandatory: bump at the end of every development branch
At the end of every branch, every worktree and every small patch straight onto
main:
- Check which files under
dashboard/internal/webui/static/{js,js-deps,css}/the branch changed substantively (git diff --stat main -- dashboard/internal/webui/static/). - For each changed file, raise the
?v=counter in base.html by 1 — append?v=1if there is no tag yet. Files referenced from multiple places (see the table above) in all references at once. - Add a row to the change history table below: the
dashboard/VERSIONtriple, the files touched, their new?v=values (same order across both columns), and the date. - Bring the current-version-state section above up to date.
cd dashboard && go test ./...andnpm test— the template tests in webui_test.go check somedata-panel-*strings verbatim and must be updated along with them.
A branch that touches frontend assets is not done until this bump and its entry here are in place.
Change history
Newest first. Each row is one release that moved one or more ?v= values: the
files and the value they moved to, in the same order across the two columns.
The dashboard version is dashboard/VERSION at the commit that carried the
bump, taken from the predecessor repo’s (werkstatt-IoT) history — that history
was not brought into this fork, so the mapping is fixed here and not
recomputable.
| Dashboard version | Files | New ?v= |
Date |
|---|---|---|---|
| v0.7.0 | css/manager.css (5 panels) |
22 |
2026-09-16 |
| v0.7.0 | css/base.css · css/manager.css (5 panels) |
21 · 21 |
2026-09-16 |
| v0.6.7 | css/base.css · js/dashboard.js · js/settings.page.js |
20 · 14 · 7 |
2026-09-16 |
| v0.6.2 | css/choices.css (settings-panel + overview editor assets) · css/settings-controls.css |
2 · 1 (was unversioned) · 3 |
2026-09-11 |
| v0.6.0 | js/mqtt.page.js · js/dashboard.js · js/settings.page.js · js/energy.page.js · css/manager.css (5 panels) |
2 · 13 · 6 · 2 · 19 |
2026-09-10 |
| v0.5.25 | js/settings.page.js · css/settings-controls.css · js/dashboard.js · css/base.css |
5 · 2 · 12 · 19 |
2026-09-10 |
| v0.5.20 | css/manager.css (5 panels) · js/systemconfig.page.js |
18 · 2 |
2026-09-08 |
| v0.5.19 | js/notifications.js |
1 |
2026-09-08 |
| v0.5.18 | css/base.css |
18 |
2026-09-07 |
| v0.5.17 | js/dashboard.js · js/layout-editor.js |
11 · 11 |
2026-09-07 |
| v0.5.15 | js/config.page.js · css/manager.css (5 panels) · js/history-recorder.js |
2 · 17 · 8 |
2026-09-06 |
| v0.5.11 | js/battery-card-core.js · js/battery-status.js · js/layout-editor.js |
2 · 1 · 10 |
2026-09-05 |
| v0.5.10 | css/manager.css (5 panels) |
16 |
2026-09-05 |
| v0.5.9 | js/settings.page.js |
2 |
2026-09-05 |
| v0.5.8 | js/battery-card-core.js · js/dashboard.js |
1 · 10 |
2026-09-05 |
| v0.5.7 | css/base.css · js/dashboard.js · js/layout-editor.js · css/layout-editor.css |
17 · 9 · 9 · 9 |
2026-09-05 |
| v0.5.6 | css/manager.css (5 panels) |
15 |
2026-09-05 |
| v0.5.5 | css/manager.css (5 panels) · js/settings.page.js · js/tailscale.page.js |
14 · 1 · 1 |
2026-09-05 |
| v0.5.4 | js/layout-editor.js |
7 |
2026-09-04 |
| v0.5.0 | js/history-store.js · js/history-recorder.js · js/history-export.js · js/systemconfig.page.js |
2 · 7 · 1 · 1 |
2026-09-04 |
| v0.3.19 | css/base.css · js/dashboard.js · css/manager.css (5 panels) · css/history.css · css/automations.css · js/overview.page.js · js/layout-editor.js · css/layout-editor.css |
16 · 8 · 12 · 3 · 3 · 5 · 6 · 6 |
2026-09-04 |
| v0.3.15 | css/base.css · css/manager.css (5 panels) |
13 · 10 |
2026-09-03 |
| v0.3.14 | baseline — base.html state at commit d6cc3e0, no bump |
— | 2026-09-02 |
The v0.6.2 row: choices.css gets box-sizing: border-box on
.choices/.choices__inner/.choices__list--dropdown/.choices__input —
without it, the open (.is-open { overflow: visible }) multiselect’s inner
box and dropdown list were each ~17px wider than their container (padding +
border added on top of width: 100% under the browser’s content-box
default), spilling past the settings card’s right edge. Bumped in both places
that load choices.css (settings-panel and the overview’s layout-editor
assets), reconciling the pre-existing gap where only settings-panel carried
a ?v=. settings-controls.css gains a four-option variant of .segmented
(.segmented--4, used by the new “Farbschema” theme picker replacing the
<select> on the settings display tab).
The v0.6.0 row is the feat: fold node telemetry into the dashboard
nodeagent squash merge. On the branch the bumps landed in steps:
mqtt.page.js2— MQTT tab gains a simulation toggle and per-metric publish switches; the file grows themetricKeyslist,saveNodeSettings()and thesimulation_active/metricsform fields.dashboard.js13,settings.page.js6— three new opt-in status-bar items (cpu_temp,ram,undervoltage) rendered from/api/v1/healthnode.telemetry;dashboard.jsaddsformatTemp()/formatPct(),settings.page.jsexpands the status-bar-item options list.energy.page.js2— the own-device filter follows theenergy_nodeidentity move:OWN_ENERGY_DEVICE_IDchanges fromenergy-node-dashboard-energytoenergy_nodeto match the renamedenergydiscovery.DeviceIdentifier, so the dashboard’s own seven energy sensors stay filtered out of the energy-role editor.css/manager.css18 → 19(5 panels) — the<fieldset class="mqtt-metric-toggles">added with the per-metric publish switches was rendering with raw browser fieldset/legend chrome;manager.cssgains a.mqtt-metric-togglesrule matching the surrounding.mqtt-source-toggleblock.
The v0.3.19 row is the feat(layout): overview layout-editing mode squash
merge — on the branch the same files were bumped in steps (layout-editor.* and
overview.page.js 1 → 6, base.css 13 → 16, etc.), but only the merged
result reached main.