Web UI development
July 8, 2026 · View on GitHub
The default web UI is gui-nightshift — a Svelte 5 + Vite + Tailwind app in
the gui-nightshift/ git submodule (repository).
It is a pure client of the device's HTTP + WebSocket API; the production build
is a small gzipped static bundle embedded into the firmware image.
gui-v2 is the previous UI, kept as a submodule for reference; select it with
GUI_NAME=gui-v2 at firmware build time.
Development workflow
cd gui-nightshift
npm install
npm run dev:mock # offline against built-in mock data — no hardware needed
npm run dev # against a real charger (set VITE_OPENEVSEHOST in .env)
npm test # vitest unit tests
npm run build # production build → dist/
Mock mode serves canned fixtures for every API endpoint and a fake WebSocket,
including runtime switchers for EVSE state and named fixture scenarios — see
the gui-nightshift README for details. A
docker-compose.yml in the submodule spins up the UI against the native
firmware build and a device emulator for full end-to-end development.
Screenshots
All UI screenshots in the documentation are generated automatically:
cd gui-nightshift
npm run screenshots # regenerates docs/screenshots/*.png deterministically
The capture manifest is gui-nightshift/scripts/screenshots.config.js. When a
UI change alters a screen's appearance, regenerate and commit the images in the
same change.
Embedding into the firmware — the submodule rules
The firmware build embeds gui-nightshift/dist as C headers into
src/web_static/ (via scripts/extra_script.py). Those generated headers are
tracked and must be committed whenever the GUI changes.
The full cycle for a GUI change:
cd gui-nightshift
# ...edit src/..., then:
npm run build # produces dist/
git add -A && git commit -m "feat(...): ..."
git push # push the submodule FIRST
cd ..
pio run -e openevse_wifi_v1 # regenerates src/web_static/ from dist/
git add gui-nightshift src/web_static
git commit -m "chore: bump gui-nightshift" # pointer bump + regenerated assets
git push
Two rules that must never be broken:
- Never commit a firmware pointer bump for a submodule commit that hasn't been pushed — anyone cloning the firmware would get an unresolvable submodule reference. Push order is always: submodule first, firmware second.
src/web_static/must be regenerated and committed together with the pointer bump, so the embedded UI always matches the recorded submodule commit.