Files
felhom.eu/documentation/audits/day-2026-10-08/design-R-35.md
T

46 lines
3.9 KiB
Markdown

# R-35 — a config apply ends the household's dashboard session: a one-page design (2026-10-08)
**Status:** design only. Read in source today (controller `main` a0370b4ed8ef). Architecture documents:
`architecture/02-controller-module-map.md` (config refresh), `07-backup-architecture.md` §5 (the whole-guest archive is
plaintext on the household's premises), `09-update-architecture.md` §3 decisions 63-64 (the family gate).
## The problem, as measured
2026-07-21: the hub raised `config_version` 10→11 at 16:54:58; the controller self-restarted (StartedAt 16:54:59Z, up
16:55:02); the household was logged out mid-flow (row evidence `controller-log-full.txt`). Still true in source:
- `internal/report/config_refresh.go:33-71` — any `config_version` change: re-pull `controller.yaml`, record, then
`Restart` = `api.GracefulSelfRestart` (`cmd/controller/main.go:1100-1109`). No per-setting choice.
- `internal/web/auth.go:15-18, 250-270` — dashboard sessions live only in `s.sessions map[string]*session` (token,
expiry, CSRF token). Any process exit ends every session. **This is not only a config-apply problem:** every
controller update (the floor, the household's own update press, a crash restart) logs the household out too.
**New since the row's last note (2026-10-05):** the family gate (decisions 63-64, controller v0.287.0) already persists
its sessions to `family.json` (0600, tmp+fsync+rename) in the data dir „so a restart keeps every session"
(`internal/family/family.go:11-12, 45-50, 267-290`). It stores the session ID itself. So „login tokens on disk" is
already the product's state for family sign-ins; the dashboard is the odd one out.
## Options
| | What | Costs | Risk |
|---|---|---|---|
| **A — hot-apply** | Adopt changed `controller.yaml` fields in the running process; restart only for fields that need it. | A per-field ruling over the whole config (hub, offbox, cloudflare, paths…), a reload path per module. ~2-3 sessions. | A field applied half-way; fixes config apply only, not updates. |
| **B — persist sessions, token on disk** | Write the session map to `sessions.json` (0600) like `family.json`. | ~½ session. | A copy of the archive (plaintext by design, `07` §5) holds live 7-day dashboard tokens. |
| **C — persist sessions, fingerprint on disk** | As B, but the file holds `sha256(token)` → {expiry, CSRF token}; lookup hashes the cookie. Cleared by `invalidateAllSessions` (password change) and logout as today; expired rows dropped at load and at the 15-min cleanup. | ~½ session + tests. | A stolen archive gives a fingerprint that cannot be turned back into a cookie. A box restored from an archive keeps that day's sessions valid until they expire (≤7 days) — the same as family sessions today. |
**Pick: C.** It ends the logout for every restart, not just config apply, at the smallest cost, and it removes the one
cost the row named (tokens in the archive). A stays a later refinement if restarts themselves become a problem.
**First slice, with its red test:** `internal/web/session_store.go` (load at `NewServer`, save under `sessionsMu` on
create/delete/invalidate, atomic write as in `family.saveLocked`). Tests: (1) create a session, build a NEW `Server` on
the same data dir, the cookie is still valid and returns the same CSRF token — red today (map is fresh); (2) the file
contains no token bytes (grep the file for the token: must be absent); (3) after `invalidateAllSessions`, a new
`Server` rejects the old cookie; (4) an expired row is not loaded. Live check on scratch 9202: sign in, trigger a
controller restart, the next page load needs no login.
## One question for the operator
**May the box remember a dashboard sign-in across its own restarts, keeping on its disk only a fingerprint that cannot
be used to sign in?** *If you do nothing:* the household is logged out at every settings push and every controller
update, as today.