Development¶
Requires Bun 1.4 or newer.
git clone https://github.com/firsttris/funkloch && cd funkloch
bun install
bun run demo # http://localhost:8090 with the demo network, no login
With your own network: ZIGBEE2MQTT_MQTT_URL=… OTBR_URL=… bun run dev.
Stack¶
Bun, TanStack Start (React) with Router, Query and Table, Zod, SQLite (bun:sqlite) with Drizzle,
mqtt.js, Tailwind 4 with Radix and lucide, uPlot, Paraglide (German
and English), Biome, Vitest, Playwright. bun run build bundles everything into dist/; serve.ts
serves it with Bun.serve, so the image needs no node_modules. On SIGTERM or SIGINT it logs
shutting down, closes the HTTP server, stops the adapters and jobs, writes what is still buffered
and closes the database, then exits with 0; as PID 1 in a container it would otherwise ignore SIGTERM
and be killed after the grace period. A cycle against the border router that is still waiting is not
waited for.
Architecture¶
src/adapters/ zigbee2mqtt (MQTT), otbr (REST), matter (WebSocket to matterjs-server: Thread and
Wi-Fi): protocol → observations, score formula per protocol
src/core/ pure functions: score, reachability, rollups, trend, events, patterns, notify, layout
src/db/ Drizzle schema, migrations, storing observations, queries, jobs' database side
src/jobs/ the scheduler: reachability and signal every minute, rollup and trend hourly,
retention and patterns at 3 a.m.
src/server/ runtime (adapters → ingest → database), auth, live updates (SSE), notifications,
views (shape the pages' data), server functions
src/routes/ the pages and /healthz, /api/live, /api/events.csv
src/demo/ the demo network
The core knows no protocols: each adapter delivers devices, readings, availability, topologies and
channel scans as Observations. A new network is a new adapter with its own ProtocolMeta (labels,
raw unit, score formula); nothing in the core changes. An adapter can deliver several protocols
(Matter: Thread and Wi-Fi), and two adapters can describe the same devices (the OTBR and Matter
for Thread): their device lists carry a source with the fields each knows best, and the ingest
merges them field by field (mergeDescribed in src/core/devices.ts). More in CLAUDE.md.
Checks¶
bun run typecheck # Paraglide compile + tsc --noEmit
bunx biome ci .
bun run test # unit, database, API and contract tests
bun run build
bun run e2e # Playwright against the demo and a server with login
MQTT_TEST_URL=mqtt://localhost:1883 bun run test:integration # needs a broker
mkdocs build --strict # when docs/ or mkdocs.yml changed
| Layer | Where | What |
|---|---|---|
| Unit | next to the code in src/core, src/adapters |
formulas, parsing, decisions; property tests with fast-check |
| Database | src/db/*.test.ts |
real SQLite files |
| API | tests/api |
the pages' data from the seeded demo at a fixed time |
| Contract | tests/contract |
every recorded Zigbee2MQTT message, OTBR answer and matterjs-server message passes the schemas |
| Integration | tests/integration |
the Zigbee adapter and MQTT notifications against Mosquitto |
| E2E | tests/e2e |
Chromium, Firefox and WebKit against the demo; login and settings; in CI in two halves at once |
| Container | CI, when docker/, serve.ts, the package files, vite.config.ts or the migrations change |
the image starts on amd64 and arm64, each on its own runner, and /healthz turns green |
Fixtures¶
Recordings from real networks keep the schemas honest: see Recording fixtures.
Releases¶
Actions → Bump version → Run workflow raises the version and pushes the tag; the tag starts
Release: all checks (unless main's CI already ran them for the code: the release commit only
raises the version), the image tristanteu/funkloch for amd64 and arm64 on Docker Hub, and the
GitHub release. The image is started in CI whenever what it is built from changes; its build
cache is the release's, so the release builds on layers that are already there. Update screenshots renews the pictures in docs/screenshots from the demo after every push
to main that touches the app (src/, public/, the texts, the screenshot test or the packages),
and by hand under Actions; the commit it makes starts no other workflow. bun run screenshots
runs the demo standing still at one moment (FUNKLOCH_DEMO_NOW, and the browser's clock with it),
so a picture changes only when the look does and the workflow commits nothing otherwise. The pictures
show the English interface (the browser's locale in the screenshots project).