Document the multi-pattern presence model and migration 9
Describe presence_patterns (zero or more home/away patterns per child, each with its own cycle length and anchor date), the first-match-wins evaluation order, and migration 9 replacing the one-per-child schedule table. Update the Admin Schedule spec for the patterns list, pattern editor, combined month grid and Sunday-first week; replace the set_presence_pattern service row with the admin pattern routes; and add the pre-upgrade backup and rollback step for migration 9 to the runbook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -551,3 +551,72 @@ The file provider reloads the dynamic file on save.
|
||||
| 5 | Panel refused externally | section 9 check 4 | `403` |
|
||||
| 6 | Non-Traefik source refused | from a LAN machine other than `10.60.1.33`/`10.60.1.14`: `curl -s -o /dev/null -w '%{http_code}\n' http://10.60.1.14:8081/` | `403` (nginx `deny all`) |
|
||||
| 7 | Proxy source allowed | on `10.60.1.14`: `curl -s -o /dev/null -w '%{http_code}\n' http://10.60.1.14:8081/` | `200` |
|
||||
|
||||
## 11. Upgrades and rollback
|
||||
|
||||
The API applies pending schema migrations when it starts (FastAPI
|
||||
lifespan, before `/health` answers), so **the first start of a new
|
||||
version is the point of no return** for any migration it carries. A
|
||||
build refuses a database whose schema version is newer than it
|
||||
understands ("newer than this integration understands … downgrades are
|
||||
not supported") and does not start.
|
||||
|
||||
### Migration 9 (multiple presence patterns)
|
||||
|
||||
Migration 9 creates `presence_patterns`, copies every row of the old
|
||||
one-per-child `presence_schedules` table into it as a `home` pattern
|
||||
named `Home schedule`, and **drops `presence_schedules`**. Version 0.7.1
|
||||
refuses the resulting version-9 database, and could not read
|
||||
`presence_patterns` anyway, so the only way back to 0.7.1 is a copy of
|
||||
the database taken **before** the first start on the new version.
|
||||
|
||||
**Before the first start on the new version**, with the old version
|
||||
still deployed:
|
||||
|
||||
1. Stop writers (as section 8 step 1):
|
||||
|
||||
dc stop api litestream
|
||||
|
||||
2. Copy the database file beside the live one with SQLite's online
|
||||
backup (includes anything still in the WAL), then check it:
|
||||
|
||||
ts=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
dc run --rm --no-deps --entrypoint sqlite3 litestream \
|
||||
/var/lib/nestquest/nestquest.db \
|
||||
".backup /var/lib/nestquest/pre-v9-$ts.db"
|
||||
dc run --rm --no-deps --entrypoint sqlite3 litestream \
|
||||
/var/lib/nestquest/pre-v9-$ts.db \
|
||||
'PRAGMA integrity_check;' \
|
||||
'SELECT version FROM nestquest_schema_version;' \
|
||||
'SELECT count(*) FROM presence_schedules;'
|
||||
|
||||
Expect `ok`, schema version `8`, and the number of presence
|
||||
schedules. Write down `$ts`.
|
||||
|
||||
3. Also record the Litestream replica position and the UTC time, so a
|
||||
point-in-time restore (section 8, `-timestamp`) can reach the
|
||||
pre-upgrade state even if the local copy is lost:
|
||||
|
||||
dc up -d litestream
|
||||
dc exec litestream litestream status # note the txid
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ # note the time
|
||||
dc stop litestream
|
||||
|
||||
The replica only covers the 7-day retention window (section 7).
|
||||
|
||||
4. Deploy the new version (sections 1–2) and run section 9. Confirm the
|
||||
live database is at version `9` (the section 8 step 3 `sqlite3`
|
||||
command against `nestquest.db`, with
|
||||
`'SELECT version FROM nestquest_schema_version;'`) and that the
|
||||
Schedule tab lists each child's old schedule as a `Home schedule`
|
||||
pattern.
|
||||
|
||||
**Rollback to 0.7.1:** check out and build 0.7.1 (section 1), then put
|
||||
the pre-upgrade database back with section 8: either steps 1 and 4–6
|
||||
with `pre-v9-<ts>.db` in place of `nestquest.db.restored`, or the full
|
||||
restore with `-timestamp` set to the time noted in step 3. Presence
|
||||
pattern changes made after the upgrade are not in that copy and are
|
||||
lost. Once the new version has proven itself, remove the copy:
|
||||
`dc run --rm --no-deps --entrypoint rm litestream -f /var/lib/nestquest/pre-v9-<ts>.db`.
|
||||
|
||||
This rollback has not yet been rehearsed.
|
||||
|
||||
+46
-11
@@ -37,7 +37,7 @@ One card, rows divided by `--nq-a-divider`, row padding 11px 13px:
|
||||
meta 11px/600 (`3 of 7 · 1 overdue`; all-done in `--nq-a-success`; away reads
|
||||
`Away · returns <Mon D>`) · 82×6px progress track with gradient fill. An away child's
|
||||
avatar is flat `#9898A8` and the row carries a lowercase `override` pill when the absence
|
||||
comes from a presence override rather than the pattern.
|
||||
comes from a presence override rather than a presence pattern.
|
||||
|
||||
### 2.4 Roomy variant (1h)
|
||||
Header becomes a hero: kicker 11px/700 uppercase, `8 of 12 done` 32px/800, 10px gradient
|
||||
@@ -89,19 +89,54 @@ yet defined** — pick 24–40 household-relevant glyphs and ship them as a grid
|
||||
|
||||
## 4. Schedule (1f)
|
||||
|
||||
- Header `Schedule` / `2-week cycle · anchor <weekday>, <Mon D>`.
|
||||
A child has **zero or more** presence patterns (schema 9). Each pattern has a name, a
|
||||
kind (`Home` or `Away`), its own cycle length (1–4 weeks), its own anchor date, and a
|
||||
weekday set per cycle week. The screen shows one child at a time.
|
||||
|
||||
- Header `Schedule` / `<Child> · N patterns` (`<Child> · no patterns, home every day`
|
||||
when the child has none).
|
||||
- **Child** chip row: one chip per child; the selected chip drives everything below.
|
||||
- **Month grid** card: month label 11px/700 uppercase + chevron pair; weekday initials
|
||||
10px/700 `--nq-a-ink-tertiary`; day cells `aspect-ratio: 1`, radius 6px —
|
||||
rotation-home days `--nq-a-selected` with `#201F7A` numerals, away days
|
||||
`--nq-a-page` with `--nq-a-ink-secondary`, override days `--nq-a-warning-bg` with
|
||||
`--nq-a-warning-ink` and a 1px warning border, today filled with the brand gradient in
|
||||
white 800. Legend row beneath, 10.5px/600, one swatch per state.
|
||||
- **Presence pattern** card: one row per child, name 13px/700 + rule 12px/600
|
||||
(`Weeks 1 & 3 of cycle`, `Every day`).
|
||||
10px/700 `--nq-a-ink-tertiary`, **Sunday first** (`S M T W T F S`); day cells
|
||||
`aspect-ratio: 1`, radius 6px — home days `--nq-a-selected` with `#201F7A` numerals,
|
||||
away days `--nq-a-page` with `--nq-a-ink-secondary`, override days
|
||||
`--nq-a-warning-bg` with `--nq-a-warning-ink` and a 1px warning border, today filled
|
||||
with the brand gradient in white 800. Legend row beneath, 10.5px/600, one swatch per
|
||||
state (`Home`, `Away`, `Override · away`, `Override · home`, `Today`), then the note
|
||||
"Combined from every pattern: away beats home; overrides beat both."
|
||||
- The grid shows the child's **combined** presence, first match wins: an override
|
||||
covering the date; else away if any Away pattern covers it; else home if any Home
|
||||
pattern covers it; else away when the child has at least one Home pattern (a Home
|
||||
pattern lists the days the child is here); else home (only Away patterns, or none).
|
||||
- **Presence patterns** card: `Add` text action; one row per pattern with
|
||||
`calendar-check` (Home) / `calendar-x` (Away), title = pattern name 13px/700, meta
|
||||
`Home · Every 2 weeks` 12px/600, and the weekday set (`Thu, Fri` for a 1-week cycle,
|
||||
otherwise `Week 1: Sat, Sun · Week 2: No days`). Trailing `pencil` opens the pattern
|
||||
editor (§4.1); trailing `trash-2` asks `Delete pattern?` inline before deleting. With
|
||||
no patterns the card reads "No patterns for <Child> — home every day."
|
||||
- **Overrides** card: `Add` text action; rows with `calendar-x` / `calendar-check`,
|
||||
title `<Child> away · <Mon D – D>`, meta reason, trailing `trash-2`.
|
||||
- Cycle position is always computed from the anchor date. **Never** from ISO week numbers
|
||||
or week parity (D-004).
|
||||
- Cycle position is always computed from each pattern's own anchor date. **Never** from
|
||||
ISO week numbers or week parity (D-004).
|
||||
- Weekday numbering on the wire stays **Monday=0 .. Sunday=6**; Sunday-first is display
|
||||
order only.
|
||||
|
||||
### 4.1 Pattern editor
|
||||
|
||||
Pushed screen with a back chevron; title `New pattern` / `Edit pattern`, sub "Repeating
|
||||
presence for <Child>". Form card fields:
|
||||
|
||||
- **Name** text field (required, not blank).
|
||||
- **Covered days are** 2-up segmented (`Away` / `Home`), selected = `#18181D`.
|
||||
- **Cycle length** select (`1 week` … `4 weeks`) and **Anchor date** side by side.
|
||||
Changing the cycle length keeps existing weeks and adds new weeks covering no day.
|
||||
- One **weekday picker** per cycle week, labelled `Week N · from <Mon D>` (the date is
|
||||
anchor + (N−1) weeks): seven toggle cells in **Sunday-first** order
|
||||
(`Su Mo Tu We Th Fr Sa`). A week may cover no day.
|
||||
|
||||
Footer: `Cancel` (flex 1, white, bordered) + `Save pattern` (flex 2, gradient), as the
|
||||
override editor (§6). Save stays disabled until the name is set, the anchor is a valid
|
||||
date, and something changed.
|
||||
|
||||
## 5. History (1g)
|
||||
|
||||
|
||||
@@ -19,6 +19,14 @@ Planned surface from CTXD CONTEXT.MD, adjusted for the design decisions in
|
||||
| `sensor.nestquest_cycle_day` | int | 1d sub-line, 1f header |
|
||||
| *configured* `weather.<entity>` | HA core | panel dock |
|
||||
|
||||
`present_today` is the child's **combined** presence across all of their presence patterns
|
||||
(a child has zero or more, each `home` or `away`) and overrides, first match wins: an
|
||||
override covering the date; else absent if any `away` pattern covers it; else present if
|
||||
any `home` pattern covers it; else absent when the child has at least one `home` pattern;
|
||||
else present. `cycle_day` is the 1-based day in the first patterned child's lowest-id
|
||||
pattern, from that pattern's anchor date (never ISO week numbers or parity); `0` when no
|
||||
child has a pattern.
|
||||
|
||||
Per-instance data is too granular for entity state. The cards read the day's instance list
|
||||
from an attribute payload on the per-child sensor (or a websocket command —
|
||||
implementer's call), shaped:
|
||||
@@ -45,12 +53,17 @@ implementer's call), shaped:
|
||||
| `nestquest.create_quest_definition` | **yes** | 1e `New` |
|
||||
| `nestquest.update_quest_definition` | **yes** | 1e sheet `Save changes` |
|
||||
| `nestquest.set_quest_definition_active` | **yes** | 1e row action |
|
||||
| `nestquest.set_presence_pattern` | **yes** | 1f presence card |
|
||||
| `nestquest.create_presence_override` | **yes** | 1i `Save override` |
|
||||
| `nestquest.delete_presence_override` | **yes** | 1f override row `trash-2` |
|
||||
| `nestquest.export_history_csv` | **yes** | 1g `CSV` |
|
||||
| `nestquest.manage_child` | **yes** | settings |
|
||||
|
||||
Presence patterns have no HA service: the admin surface is the PWA (ARCHITECTURE §10),
|
||||
which manages a child's patterns through the admin API —
|
||||
`GET`/`POST /api/v1/admin/children/{child_id}/presence-patterns` to list and add, and
|
||||
`PATCH`/`DELETE /api/v1/admin/presence-patterns/{pattern_id}` to edit and delete one
|
||||
pattern (1f patterns card and pattern editor).
|
||||
|
||||
`complete_quest` payload from the panel:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -11,7 +11,9 @@ rather than aliasing.
|
||||
|
||||
- `schema.py`: `task_definitions` → `quest_definitions`, `task_instances` →
|
||||
`quest_instances`. Keep `completion_events`, `children`, `admin_users`,
|
||||
`schedule_rules`, `presence_schedules`, `presence_overrides`.
|
||||
`schedule_rules`, `presence_patterns`, `presence_overrides`. (`presence_patterns` —
|
||||
zero or more `home`/`away` patterns per child — replaced the retired one-per-child
|
||||
schedule table in schema 9; migration 9 converts its rows into `home` patterns.)
|
||||
- `db.py` / `store.py`: rename DAO methods and any SQL literals.
|
||||
- `const.py`: no table names live there today; keep it that way.
|
||||
- `tests/test_schema.py`, `tests/test_store.py`, `tests/test_db.py`: update expectations.
|
||||
@@ -43,8 +45,9 @@ Nullable int. Set for panel completions, null for admin ones. Still append-only.
|
||||
|
||||
## 2. Engines and materialisation
|
||||
Unchanged in intent: pure recurrence engine, pure presence engine with anchor-date
|
||||
arithmetic, daily materialiser over `horizon_days` at `day_rollover_time`. Only the
|
||||
instance fan-out changes (assignees × windows).
|
||||
arithmetic (per pattern, each with its own anchor; since schema 9 it combines a child's
|
||||
patterns and overrides first-match-wins), daily materialiser over `horizon_days` at
|
||||
`day_rollover_time`. Only the instance fan-out changes (assignees × windows).
|
||||
|
||||
## 3. Services, permissions, entities
|
||||
Implement the service table in `ENTITIES-AND-SERVICES.md` §2 behind the permission
|
||||
|
||||
@@ -61,6 +61,44 @@ two-writer problem entirely.
|
||||
- **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.
|
||||
|
||||
## 4. Authentication: two planes
|
||||
|
||||
The kid panel and the admin PWA have opposite trust levels, so they use
|
||||
|
||||
Reference in New Issue
Block a user