Files
NestQuest/docs/ARCHITECTURE.md

13 KiB
Raw Permalink Blame History

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.