Frontend Build¶
The dashboard is a Vue 3 application in frontend/. It is built by npm into
src/batcontrol/web/static/, from where setuptools picks it up as package data - so the
assets travel inside the wheel.
That last point is the one to remember: the Home Assistant stable add-on installs nothing but the released wheel. A wheel built without the npm step produces an add-on without a dashboard, and nothing fails loudly on the way there.
Building it¶
cd frontend
npm ci
npm run build # -> ../src/batcontrol/web/static/
npm run build also runs scripts/check-offline.mjs, which fails if the built output would
load a font, stylesheet or script from the network. batcontrol runs in homes without internet
egress and behind Home Assistant ingress, where an external asset does not degrade - it never
arrives.
A source checkout without Node is a legitimate state: batcontrol then serves a short hint page
at / instead of the dashboard, and the REST API is unaffected.
Developing¶
Two modes, depending on whether you need live data:
# Against a running batcontrol (api.enabled: true). The dummy inverter in
# config/batcontrol_config_dummy.yaml is enough - no hardware needed.
npm run dev
# Against frozen fixtures, no backend at all
npm run dev:fixtures
VITE_FIXTURE=battery-full npm run dev:fixtures
Fixtures exist for the states a live battery will not produce on request: exactly 100 %, 3 % in
force charge, a reservation larger than the battery, the 503 before the first control cycle, a
stalled control loop, a wrong token. They were captured from a real instance, so their shape is
the API's. See frontend/fixtures/README.md.
npm run dev proxies /api to VITE_BACKEND_URL (default http://127.0.0.1:8080), so the
browser sees one origin and no CORS or private-network prompt.
Dependency pinning¶
package.json holds exact versions, not ranges, and .npmrc sets save-exact=true so a
later npm install <pkg> cannot reintroduce one. package-lock.json pins every transitive
package by its sha512 integrity hash, and npm ci - which is what the Dockerfile and both CI
workflows run - verifies each hash and fails on a mismatch rather than resolving anything.
Updating a dependency is therefore deliberate: npm install <pkg>@<version>, commit both
package.json and package-lock.json, and check the build output still passes
check-offline.mjs.
Tests¶
npm test # vitest
Covered: normalize() against every fixture, the SOC grid cell states against the table in the
dashboard specification, and the decision trace component against a
captured payload.
How it reaches the user¶
frontend/ npm run build
└─> src/batcontrol/web/static/
└─> wheel (package data)
├─> Docker image (Dockerfile, node stage)
├─> HA stable add-on (installs the released wheel)
└─> HA development add-on (builds the wheel from main)
The Docker build uses a dedicated node:24-alpine stage with
--platform=$BUILDPLATFORM, so a multi-arch build runs npm once, natively, instead of once per
architecture under emulation. The output is static files and architecture independent.
Pitfalls¶
base: './'is required. The same bundle is served at/, under a Home Assistant ingress token path and under a reverse-proxy subpath. Absolute asset paths break two of the three. The API base URL is derived fromdocument.baseURIfor the same reason.- An asset only ships if something imports it. Vite does not copy unreferenced files under
src/. Anything that has to ship verbatim - the font licenses, for example - belongs infrontend/public/. - Timestamps are epoch milliseconds in the API and epoch seconds in MQTT.
valueis Wh per slot,power_wis W. At 15-minute resolution they differ by a factor of four.