# 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.