- Go 95.4%
- Vue 3.5%
- TypeScript 0.9%
Groups are one layer deep and carry their own grid size and layout inside the dashboard's layout_json. Each group always contains exactly one back tile (movable, resizable, but not removable); group and back tiles are validated server-side (no nesting, no entity_id, ids unique across the document). The editor adds groups by click or drag-and-drop (no auto drill-in), and the kiosk serves them at /d/:id/g/:gid with a live on-count summary on the group tile. |
||
|---|---|---|
| cmd/server | ||
| internal | ||
| tools | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
HA Dash
A lightweight Home Assistant dashboard designed for kiosk browsers on low-end devices. The stock Home Assistant frontend is far too heavy for old tablets and thin clients — HA Dash ships a single static page (~120 KB gzipped JS), renders only what you placed on a grid, and receives entity diffs over one WebSocket so updates cost almost nothing.
┌─ kiosk browser ─┐ WS: snapshot + diffs ┌── Go server ──┐ 1 WS conn ┌── HA ──┐
│ Vue SPA │ ◄──────────────────────► │ API + hub │ ◄─────────► │ /api/websocket
│ (embedded in │ POST /api/services/… │ SQLite │ subscribe_entities
│ Go binary) │ └───────────────┘ └────────┘
- One Go binary serves the embedded Vue app, the REST API and the browser WebSocket
- The Go server holds a single WebSocket connection to Home Assistant (
subscribe_entities) and fans out only changed states — the HA token never leaves the server - Multiple dashboards, free grid with draggable/resizable tiles (WYSIWYG editor)
- v1 tile types: lights (toggle + brightness) and switches (toggle)
- Optimistic updates: taps feel instant, the authoritative diff follows over the socket
- Runs as non-root (uid/gid 10001) in a read-only distroless container
Quick start (Linux)
-
Install Docker Engine with the compose plugin.
-
Create the data directory owned by the container user (Docker creates missing bind-mount paths as root, which would make SQLite fail):
mkdir -p ./data && sudo chown 10001:10001 ./data -
Build and run:
docker compose up -d -
Open
http://<host>:8090and finish the setup (next section).
First dashboard
- Create a long-lived access token in Home Assistant: profile (bottom left) → Security → Long-lived access tokens → Create token.
- In HA Dash, fill in your Home Assistant URL (e.g.
http://homeassistant.local:8123), paste the token, Test connection (should show your HA version), then Save. - Create a dashboard: pick a name, number of grid columns and rows.
- In the editor, drag lights/switches from the left palette onto the grid (or click to place). Move and resize tiles freely; the layout autosaves.
- Open the dashboard and point your kiosk browser at its URL — e.g.
http://<host>:8090/d/<dashboard-id>. Use Copy kiosk URL on the dashboard card.
Kiosk tips
-
Chromium in kiosk mode:
chromium --kiosk --noerrdialogs --disable-session-crashed-bubble \ --incognito http://<host>:8090/d/<id> -
The kiosk view hides all chrome. A faint grid button in the top-right corner opens a switcher between dashboards, the editor and the manage page.
-
If Home Assistant goes down, the kiosk shows a small reconnecting pill and heals itself — no page reload needed. The browser WebSocket also auto-reconnects with backoff.
-
Optional: disable screen blanking (
xset s off -dpms) and hide the cursor in your kiosk session manager.
Security notes
- The long-lived HA token is stored only in the SQLite DB under
./dataon the server and is never sent to any browser. - The admin UI has no authentication (LAN-trusted model, like most HA add-ons). If you expose the port beyond your trusted network, put it behind a reverse proxy with auth.
- The container: distroless (no shell, no package manager), non-root
10001:10001, read-only rootfs, all capabilities dropped,no-new-privileges,tmpfson/tmp. - Service calls are allow-listed: only
light/switchdomains withtoggle/turn_on/turn_off(plusbrightness_pct/transitiondata).
Configuration
The compose file hardening flags at a glance:
| flag | purpose |
|---|---|
user: "10001:10001" |
never runs as root |
read_only: true + tmpfs /tmp |
immutable root filesystem |
cap_drop: [ALL] |
no Linux capabilities |
security_opt: no-new-privileges |
blocks privilege escalation |
volumes: ./data:/data |
SQLite + settings live here |
healthcheck |
uses /server healthcheck (distroless has no curl/wget) |
Environment variables: PORT (default 8080), DATA_DIR (default /data in the container, ./data for local dev).
Development
make test # go test + web typecheck
make lint # go vet, gofmt check, web typecheck
make mockha # fake Home Assistant on :8123 (token: mock-token)
make dev # build web UI + run Go server on :8080
make web-dev # Vite dev server on :5173 (proxies /api to :8080)
The mock HA implements the real WebSocket protocol (auth handshake, subscribe_entities, state diffs, service calls) with five entities and random state changes every few seconds — ideal for developing without a real instance.
Troubleshooting
permission deniedwritingdash.db— the./databind mount is root-owned. Run thechown 10001:10001step from Quick start.- SELinux hosts (Fedora/RHEL) — if SQLite still fails to write despite correct ownership, append
:Zto the bind mount:- ./data:/data:Z. HA connection failedon the setup page — check the URL scheme and port (must be the HA web URL, not the add-on internal one), and that the token was not revoked.- Dashboard unreachable from the kiosk — check the host firewall (
ufw allow 8090). Note that Docker-published ports bypass ufw INPUT rules, so also verify the port is bound to the right interface. - States are stale — the pill in the top bar shows the connection status; hover it for the last error. The server keeps retrying with exponential backoff.
Architecture
cmd/server— entrypoint +healthchecksubcommandinternal/ha— Home Assistant WebSocket client: auth handshake,subscribe_entitiesdiff parsing (a/c/rcompact format), reconnect with backoff, service calls over the REST APIinternal/ws— hub: fans out snapshots + diffs to browsers, filters to light/switch domainsinternal/api— REST API + embedded SPA serving (Go 1.22+ mux patterns)internal/store— SQLite (pure-Go driver, WAL mode)web— Vue 3 + TypeScript + Tailwind CSS 4 + shadcn-vue (reka-ui) +grid-layout-plustools/mockha— mock Home Assistant for development and testing