- Go 64.8%
- TypeScript 32.9%
- CSS 1.1%
- Dockerfile 0.6%
- JavaScript 0.3%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| docker | ||
| frontend | ||
| .gitignore | ||
| .mise.toml | ||
| README.md | ||
control-center
Home control center for the Z-Wave network: a live dashboard of all nodes (currently: the door sensor with open/closed state, node status, last activity, and temperature).
Public: https://control-center.barbara8.de (auth required; default
accounts admin@barbara8.de / user@barbara8.de, passwords seeded as
Dockhand stack secrets)
Architecture
zwave-js-ui (narrow) ──publish──▶ mosquitto (narrow, tcp://100.113.11.77:1883, topic zwave/#)
│ (Tailscale)
▼
control-center (UM890, one container)
├─ Go + embedded PocketBase
│ ├─ MQTT bridge: parses zwave-js-ui topics,
│ │ learns topic-ref → node-id from `status`,
│ │ upserts `zwave_nodes` records
│ ├─ REST API: /api/nodes, /api/nodes/{id}, /api/me,
│ │ /api/system (auth via PocketBase tokens)
│ └─ serves the React SPA statically
└─ React 19 + Vite + Tailwind v4 + shadcn/ui
(bun toolchain, 5 s polling)
- Topic model (zwave-js-ui):
zwave/<loc>/<name>/…,zwave/<name>/…orzwave/nodeID_<id>/…. Only the…/statustopic carries the node id, so the bridge learns a ref→node-id map and resolves everything else against it; value topics that arrive before their node's status (initial retained burst) are buffered per ref and flushed on learn. - Door state: Access Control CC (113)
Door_state_simple/Door_state;22 (0x16)= open,23 (0x17)= closed (zwave-js enumDoorState). - Data: PocketBase collections are rule-denied; access is exclusively
through the authenticated custom API. Migrations are Go code
(
backend/migrations/, run on boot). - Secrets:
ADMIN_PASSWORD/USER_PASSWORDare Dockhand stack secrets (isSecret:true, seeded once out-of-band). CI only ever sends the non-secret image ref.
Layout
backend/ Go service (DDD layout: domain / application / infrastructure)
frontend/ React SPA (bun, Vite, Tailwind v4, shadcn/ui in src/components/ui)
docker/ Dockerfile (multi-stage), docker-compose.yml (stack source of
truth), deploy.ts (two-step Dockhand deployer), entrypoint.sh
docs/ (API notes)
Local development
# backend (needs a reachable MQTT broker)
cd backend
go test ./...
ADMIN_PASSWORD=x USER_PASSWORD=y MQTT_URL=tcp://100.113.11.77:1883 \
go run ./cmd/server serve --http 127.0.0.1:8080
# frontend (proxies /api to :8080)
cd frontend
bun install
bun run dev
Deployment
Pattern D (see ../AGENTS.md): CI builds the single image
code.schwanke.it/home/control-center/control-center:<sha> and deploys the
control-center Dockhand stack on UM890 (env 3) via docker/deploy.ts.
Ingress: control-center.barbara8.de (Caddy on narrow via the
caddy-ingress-controller labels in the compose).
First-time bootstrap (done): the stack + its two secret env vars were seeded out-of-band via the Dockhand API before the first CI deploy.