No description
  • Go 95.4%
  • Vue 3.5%
  • TypeScript 0.9%
Find a file
Joeri 4babf90457 feat: tile groups with a second-level grid and mandatory back tile
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.
2026-10-01 23:23:35 +02:00
cmd/server feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
internal feat: tile groups with a second-level grid and mandatory back tile 2026-10-01 23:23:35 +02:00
tools fix: harden full stack after code review 2026-10-01 00:00:19 +02:00
web feat: tile groups with a second-level grid and mandatory back tile 2026-10-01 23:23:35 +02:00
.dockerignore fix: harden full stack after code review 2026-10-01 00:00:19 +02:00
.gitignore feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
AGENTS.md docs: instruct agents to git pull before doing anything 2026-10-01 19:49:28 +02:00
compose.yml feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
Dockerfile feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
go.mod feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
go.sum feat: initial HA Dash — lightweight kiosk dashboard for Home Assistant 2026-09-30 09:18:51 +02:00
Makefile fix: harden full stack after code review 2026-10-01 00:00:19 +02:00
README.md feat: define grid by columns and rows instead of row height 2026-09-30 09:38:05 +02:00

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)

  1. Install Docker Engine with the compose plugin.

  2. 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
    
  3. Build and run:

    docker compose up -d
    
  4. Open http://<host>:8090 and finish the setup (next section).

First dashboard

  1. Create a long-lived access token in Home Assistant: profile (bottom left) → Security → Long-lived access tokens → Create token.
  2. 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.
  3. Create a dashboard: pick a name, number of grid columns and rows.
  4. 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.
  5. 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 ./data on 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, tmpfs on /tmp.
  • Service calls are allow-listed: only light/switch domains with toggle/turn_on/turn_off (plus brightness_pct/transition data).

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 denied writing dash.db — the ./data bind mount is root-owned. Run the chown 10001:10001 step from Quick start.
  • SELinux hosts (Fedora/RHEL) — if SQLite still fails to write despite correct ownership, append :Z to the bind mount: - ./data:/data:Z.
  • HA connection failed on 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 + healthcheck subcommand
  • internal/ha — Home Assistant WebSocket client: auth handshake, subscribe_entities diff parsing (a/c/r compact format), reconnect with backoff, service calls over the REST API
  • internal/ws — hub: fans out snapshots + diffs to browsers, filters to light/switch domains
  • internal/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-plus
  • tools/mockha — mock Home Assistant for development and testing