# NestQuest Architecture Status: approved direction (2026-09-21). This document supersedes the single-process design implied by Features 01–14, in which the Home Assistant integration owned the database and the parent admin surface was planned as a Lovelace card inside Home Assistant. ## 1. Why this changed The original design put everything inside the Home Assistant integration: the SQLite database, the business logic, the kid panel cards, and (planned) the parent admin card. Three problems surfaced: 1. **The panel needed hand configuration.** Standing up the kid panel required hand-writing card YAML (`child_order`, `quest_log_path`, `board_path`) and hand-creating a Lovelace dashboard. A fresh install rendered nothing. 2. **The admin surface fought Home Assistant.** The admin design (`design/ADMIN-SPEC.md`) is drawn at 390×844 — it is a phone app. Trapping it inside a Lovelace card meant fighting HA's UI constraints for a mobile-first experience. 3. **Data was locked to one process.** A standalone admin web app cannot reach a SQLite file inside Home Assistant's config directory. The re-imagined design keeps the panel in Home Assistant (TouchHub loads an HA dashboard), moves data ownership to a standalone API service, and promotes the admin surface to a standalone mobile-first PWA. ## 2. Target architecture ``` DAKboard TouchHub │ (loads an HA dashboard URL) ▼ HA box API box ┌───────────────────────────────┐ ┌───────────────────────────────────┐ │ Lovelace panel dashboard │ │ reverse proxy (TLS) │ │ ├ nestquest-party-board-card │ service │ ├ /api/v1/panel/* LAN-only │ │ └ nestquest-quest-log-card │ token │ └ /api/v1/admin/* public │ │ │ ─────────▶ │ FastAPI (imports core/) │ │ NestQuest integration │ │ └ SQLite (owned here) │ │ (thin client, no database) │ ◀───────── │ Authentik (OIDC) │ │ sensors + HA bus events │ SSE └───────────────────────────────────┘ └───────────────────────────────┘ ▲ PWA ───────────┘ https://nestquest.cubecraftlabs.com ``` One owner of the data (the API). Two clients (the HA integration, the PWA). ## 3. Data ownership **The API service is the only writer.** The HA integration never opens the database; it reads a snapshot and posts completions. This removes the two-writer problem entirely. - **Storage:** SQLite on the API box. Single household, trivial concurrency, and the existing schema, migrations, and DAO tests are already SQLite. Postgres buys nothing at this scale. - **Schema:** unchanged. The cutover from the current HA-hosted database is a file copy — same schema, same migration version. - **Backups:** continuous replication (e.g. Litestream) plus a proven restore. A configured-but-untested backup is not a backup. ### Presence model (schema 9) A child has **zero or more** presence patterns, stored one row each in `presence_patterns` (`child_id` is not unique): | Column | Meaning | |---|---| | `name` | Label shown in the admin PWA, not blank | | `kind` | `home` or `away` — what the pattern claims on the days it covers | | `cycle_length_weeks` | 1–4; the pattern's own repeating cycle | | `anchor_date` | ISO date that starts week 1 of the pattern's cycle | | `pattern` | Pipe-separated weekday sets, one CSV segment per cycle week, weekdays **Monday=0 .. Sunday=6** (e.g. `0,2,4\|1,3`); an empty segment covers no day that week | Cycle position is anchor-date arithmetic per pattern — **never** ISO week numbers or week parity (D-004). A child's presence on a date is decided first match wins: 1. A presence override covering the date — its flag is the answer. 2. Any `away` pattern covers the date — **away**. 3. Any `home` pattern covers the date — **present**. 4. The child has at least one `home` pattern — **away** (a home pattern lists the days the child is home; every other day is away). 5. Otherwise (only `away` patterns, or no patterns) — **present**. The core presence engine, the snapshot's `present_today`, the materialiser's skip-on-away-days rule, and the admin PWA's month grid all apply this order. The admin plane manages patterns with `GET`/`POST /api/v1/admin/children/{child_id}/presence-patterns` and `PATCH`/`DELETE /api/v1/admin/presence-patterns/{pattern_id}`. **Migration 9** replaced the older one-schedule-per-child table: it creates `presence_patterns`, copies every old schedule row into a `home` pattern named `Home schedule` (cycle length, anchor date and weekday sets unchanged, row id kept), and drops the old table, all in one transaction. A lone `home` pattern answers exactly as the old schedule did, so no child's presence changes. The drop is one-way and 0.7.1 refuses a version-9 database; see the rollback step in `deploy/RUNBOOK.md` §11. ### Application timezone (manual, admin-set) The application timezone is set **manually by an admin from the admin app** — the Settings screen's timezone picker, which reads and writes the `timezone` field of `GET`/`PATCH /api/v1/admin/settings`. Nothing sets it automatically: there is no Home Assistant zone adoption, no browser-zone auto-detect, and no auto-save when a screen loads. Only an admin's `PATCH` ever changes the stored value. The stored value is the **single source of local-time truth for the whole application.** The API reads its one clock and re-expresses it in that zone (`NestQuestSettings.household_now`), so every local date and time comes from it: - the panel snapshot's today and overdue state, and both panel cards' visible dates and clocks (the snapshot publishes the zone as `timezone`, which the integration exposes on the household rollup sensor); - completion timestamps (the local completion date and the on-time check); - the missed-sweep scheduler's household rollover; - every admin read, and the admin app's local-time formatting. **Empty means the API host's local zone.** While `timezone` is empty the API reads its clock in the host's local time, and publishes that zone as the effective zone (`api/host_zone.py`: the `effective_timezone` field of the admin settings response and the snapshot's `timezone`) until an admin sets one. No code fills the empty value in. **A save re-plans the sweep.** An admin save that changes `timezone` or `day_rollover_time` notifies the running missed-sweep scheduler (`MissedSweepScheduler.settings_changed`, under its settings lock), which re-plans its next sweep for the new household rollover. ## 4. Authentication: two planes The kid panel and the admin PWA have opposite trust levels, so they use separate credentials and separate network exposure. ### Panel plane (machine) - **Credential:** a static service token. - **Exposure:** `/api/v1/panel/*` is reachable **only from the LAN**. - **Grants:** read the snapshot, and the single open write — complete a quest as `actor: panel` with the tapped `actor_child_id`. - **Refused:** admin routes reject the service token outright, not merely as a non-admin. The internet cannot reach these routes at all. This preserves the original fail-closed boundary: the kiosk can complete a quest and nothing else. ### Admin plane (human) - **Credential:** an Authentik OIDC JWT (authorization code + PKCE). - **Exposure:** `/api/v1/admin/*` is internet-facing behind Authentik. - **Grants:** everything a parent can do — definitions, assignment, presence, children, un-complete, history, export, settings, regenerate. - **Gate:** the API validates the JWT (issuer, audience, JWKS signature, expiry) and requires membership in the `nestquest-admins` group. The Authentik group is the single source of truth for "who is a parent", replacing the integration-side admin allowlist. ## 5. Components ### `core/` — domain library The extracted domain layer: schema, migrations, database access, DAOs, the pure recurrence and presence engines, materialization, completion, event payload construction, and the children/definitions/allowlist business layers. **No Home Assistant imports.** Consumed by both the API and (during transition) the integration. This is where the ~1500 existing tests live after extraction. ### `api/` — NestQuest API A FastAPI service importing `core/`. Owns the database. Serves the panel and admin planes, validates Authentik tokens, and publishes a transition event stream (SSE) that the integration consumes. No business logic in route handlers. ### `custom_components/nestquest/` — HA integration (client) A thin client. Polls the snapshot and builds the existing sensors; subscribes to the SSE stream and re-fires the four HA bus events; proxies the panel completion. Holds a last-good snapshot cache so a brief API outage does not blank the panel. Owns no database and no business logic. ### `panel/` — Lovelace cards and dashboard strategy The two kid-facing Lit cards plus a dashboard strategy that generates the panel views at render time. Cards auto-discover children, so no `child_order` is required; the strategy computes navigation paths, so `quest_log_path` and `board_path` are not required. ### `admin/` — Admin PWA A React progressive web app implementing `design/ADMIN-SPEC.md`, authenticated via Authentik OIDC, installable on a phone. The only admin surface. ## 6. Zero-configuration panel A fresh install should show the panel with no YAML: 1. **Auto-discovery.** The party-board card reads an ordered child list from a household entity attribute instead of a configured `child_order`. 2. **Dashboard strategy.** A Lovelace strategy generates a party-board view and one quest-log view per child, wiring navigation between them, so paths are computed rather than configured. Explicit configuration still wins if a user supplies it. 3. **Auto-registration.** The integration registers the dashboard on first setup, so there is no manual dashboard step. ## 7. Home Assistant surface preserved The re-imagining must not break what already works: - **Sensors:** the per-child and household sensor entity_ids and the per-child instance attribute payload shape are unchanged. The panel cards and the four blueprints depend on them. - **Events:** the four HA bus events (`nestquest_quest_completed`, `nestquest_quest_uncompleted`, `nestquest_quest_missed`, `nestquest_child_day_complete`) keep firing with their documented payloads. They are now driven by the API's transition stream rather than by local service handlers. - **Blueprints:** the morning summary, afternoon reminder, end-of-day report, and day-complete celebration continue to work with no change. ## 8. Events without an HA token on the API box The API publishes transitions on an SSE stream; the integration subscribes and re-fires the HA bus events locally. This deliberately avoids storing a Home Assistant long-lived token on the API box, which would grant full HA access. ## 9. Migration phases Mapped to the Maestro features: | Phase | Feature | Outcome | |---|---|---| | 1 | 15 — Domain Core Extraction | `core/` runs with no HA imports; one implementation of the rules | | 2 | 16 — NestQuest API Service | Panel and admin planes served with two-plane auth | | 3 | 17 — API Deployment and Identity | TLS, Authentik, backups, network separation | | 4 | 18 — HA Integration as API Client | Integration becomes a client; entity/event surface preserved | | 5 | 19 — Admin PWA | Mobile-first React PWA replaces the admin card | | 6 | 20 — Zero-Config Panel Dashboard | Panel installs itself; navigation and animations match the mockups | Cutover: copy the existing SQLite file to the API box (same schema). ## 10. Locked decisions - **API on its own box** — separate from Home Assistant. - **Authentik** (`https://auth.cubecraftlabs.com`) for admin identity; a `nestquest-admins` group is the admin source of truth. - **Single household** — no tenant column, no multi-household auth. - **React** for the admin PWA. - **Drop the HA admin services** — the PWA is the only admin surface. The panel completion path stays. - **SQLite on the API box** initially; Postgres only if multi-household ever becomes real. ## 11. Preserved vs. changed **Preserved:** the tested Python domain layer, the panel card designs and tokens, the sensor and event contract, the completion model, the append-only event log, the panel's fail-closed "complete only" boundary. **Changed:** data ownership moves to the API; the integration becomes a client; the admin card becomes a PWA; panel cards become config-free with a strategy-generated dashboard. **New:** the API service, Authentik-gated admin auth, the PWA, and a last-good-snapshot cache in the integration. **Cost:** two new deployables (the API and the PWA) and one new box. Accepted knowingly in exchange for a self-installing panel, an admin surface that is not fighting HA's UI, and a data layer with real auth in front of it.