Docs: multi-pattern presence, Sunday-first week and the migration-9 rollback (#233)
This commit was merged in pull request #233.
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` |
|
| 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`) |
|
| 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` |
|
| 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
|
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
|
`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
|
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)
|
### 2.4 Roomy variant (1h)
|
||||||
Header becomes a hero: kicker 11px/700 uppercase, `8 of 12 done` 32px/800, 10px gradient
|
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)
|
## 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
|
- **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 —
|
10px/700 `--nq-a-ink-tertiary`, **Sunday first** (`S M T W T F S`); day cells
|
||||||
rotation-home days `--nq-a-selected` with `#201F7A` numerals, away days
|
`aspect-ratio: 1`, radius 6px — home days `--nq-a-selected` with `#201F7A` numerals,
|
||||||
`--nq-a-page` with `--nq-a-ink-secondary`, override days `--nq-a-warning-bg` with
|
away days `--nq-a-page` with `--nq-a-ink-secondary`, override days
|
||||||
`--nq-a-warning-ink` and a 1px warning border, today filled with the brand gradient in
|
`--nq-a-warning-bg` with `--nq-a-warning-ink` and a 1px warning border, today filled
|
||||||
white 800. Legend row beneath, 10.5px/600, one swatch per state.
|
with the brand gradient in white 800. Legend row beneath, 10.5px/600, one swatch per
|
||||||
- **Presence pattern** card: one row per child, name 13px/700 + rule 12px/600
|
state (`Home`, `Away`, `Override · away`, `Override · home`, `Today`), then the note
|
||||||
(`Weeks 1 & 3 of cycle`, `Every day`).
|
"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`,
|
- **Overrides** card: `Add` text action; rows with `calendar-x` / `calendar-check`,
|
||||||
title `<Child> away · <Mon D – D>`, meta reason, trailing `trash-2`.
|
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
|
- Cycle position is always computed from each pattern's own anchor date. **Never** from
|
||||||
or week parity (D-004).
|
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)
|
## 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 |
|
| `sensor.nestquest_cycle_day` | int | 1d sub-line, 1f header |
|
||||||
| *configured* `weather.<entity>` | HA core | panel dock |
|
| *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
|
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 —
|
from an attribute payload on the per-child sensor (or a websocket command —
|
||||||
implementer's call), shaped:
|
implementer's call), shaped:
|
||||||
@@ -45,12 +53,17 @@ implementer's call), shaped:
|
|||||||
| `nestquest.create_quest_definition` | **yes** | 1e `New` |
|
| `nestquest.create_quest_definition` | **yes** | 1e `New` |
|
||||||
| `nestquest.update_quest_definition` | **yes** | 1e sheet `Save changes` |
|
| `nestquest.update_quest_definition` | **yes** | 1e sheet `Save changes` |
|
||||||
| `nestquest.set_quest_definition_active` | **yes** | 1e row action |
|
| `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.create_presence_override` | **yes** | 1i `Save override` |
|
||||||
| `nestquest.delete_presence_override` | **yes** | 1f override row `trash-2` |
|
| `nestquest.delete_presence_override` | **yes** | 1f override row `trash-2` |
|
||||||
| `nestquest.export_history_csv` | **yes** | 1g `CSV` |
|
| `nestquest.export_history_csv` | **yes** | 1g `CSV` |
|
||||||
| `nestquest.manage_child` | **yes** | settings |
|
| `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:
|
`complete_quest` payload from the panel:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ rather than aliasing.
|
|||||||
|
|
||||||
- `schema.py`: `task_definitions` → `quest_definitions`, `task_instances` →
|
- `schema.py`: `task_definitions` → `quest_definitions`, `task_instances` →
|
||||||
`quest_instances`. Keep `completion_events`, `children`, `admin_users`,
|
`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.
|
- `db.py` / `store.py`: rename DAO methods and any SQL literals.
|
||||||
- `const.py`: no table names live there today; keep it that way.
|
- `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.
|
- `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
|
## 2. Engines and materialisation
|
||||||
Unchanged in intent: pure recurrence engine, pure presence engine with anchor-date
|
Unchanged in intent: pure recurrence engine, pure presence engine with anchor-date
|
||||||
arithmetic, daily materialiser over `horizon_days` at `day_rollover_time`. Only the
|
arithmetic (per pattern, each with its own anchor; since schema 9 it combines a child's
|
||||||
instance fan-out changes (assignees × windows).
|
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
|
## 3. Services, permissions, entities
|
||||||
Implement the service table in `ENTITIES-AND-SERVICES.md` §2 behind the permission
|
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.
|
- **Backups:** continuous replication (e.g. Litestream) plus a proven restore.
|
||||||
A configured-but-untested backup is not a backup.
|
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
|
## 4. Authentication: two planes
|
||||||
|
|
||||||
The kid panel and the admin PWA have opposite trust levels, so they use
|
The kid panel and the admin PWA have opposite trust levels, so they use
|
||||||
|
|||||||
Reference in New Issue
Block a user