From c65884f9298127ea4c0b47c55443fb1e64152d76 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 12:47:33 -0500 Subject: [PATCH] docs: Phase 5 finalize contract, add release notes --- docs/schedule-api-contract.md | 34 ++++- docs/scheduler-overhaul-release-notes.md | 164 +++++++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 docs/scheduler-overhaul-release-notes.md diff --git a/docs/schedule-api-contract.md b/docs/schedule-api-contract.md index a1b8976..258dce0 100644 --- a/docs/schedule-api-contract.md +++ b/docs/schedule-api-contract.md @@ -497,8 +497,9 @@ and is recorded under [Open items](#open-items). ## Open items -Recorded ambiguities and conservative choices. Each is subject to change in a -later phase. +Recorded ambiguities, conservative choices, and remaining known limitations. +Items 1–21 were resolved or restated during Phases 1–4; items 22–24 are Phase 5 +findings. Everything marked "Resolved" is settled for this overhaul. 1. **Phase numbering.** The brief names Phases 3 (CRUD) and 4 (copy) only; Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here. @@ -564,6 +565,27 @@ later phase. tests that a copied payload (new Block IDs, same type/times/audio/refs) is parsed and evaluated identically to any other; **no Kiosk changes were required**. +22. **Phase 3 migration down is lossy for new-model data.** The `up` migration + (`20260929130000_move_schedule_to_license_key`) is additive and safe. Its + `down` path is **reversible in structure but lossy in data**: the down SQL + drops `Schedule.licenseKeyId`/`type` and `Block.day`, then deletes + `Schedule` rows whose `userId IS NULL`. Any schedule created under the + per-License-Key model (which sets `licenseKeyId` and leaves `userId` NULL) + is therefore **deleted** by a down-migration. Consequences: + - Back up `Schedule`/`Block`/`BlockProclamations` before any down-migration. + - Rolling back past Phase 3 requires restoring that data from backup; it is + **not** a data-preserving rollback. + - Rollback guidance is in + [scheduler-overhaul-release-notes.md](scheduler-overhaul-release-notes.md). +23. **Kiosk UI/E2E on real hardware not run here.** The verification ran the + Portal and the Kiosk schedule logic (engine, client, cache) via the test + harness and real HTTP against a locally running Portal; the SDL Kiosk UI was + not driven interactively because the build environment has no display. See + the manual checklist in the release notes and the Phase 5 report. +24. **Unauthenticated Kiosk endpoints remain a known limitation** (restated from + item 6): the key-only schedule and system-name endpoints authenticate by + License Key alone, mitigated only by the 10/min/IP rate limit. Authentication + is out of scope for this overhaul. --- @@ -587,3 +609,11 @@ later phase. and Proclamation order, scoped to the caller's account, and validated before commit. Endpoint (e) is finalized; open items 20 and 21 record the copy semantics and the Kiosk no-change verification. +- 2026-09-29 — Phase 5 (final). End-to-end verification against a locally + running Portal on a fresh database: schedule create/type/copy services and + the Kiosk-facing HTTP endpoints (validation with/without `systemName`, + system-name PUT, key-only schedule GET with ETag/304/404, legacy endpoint + parity, cross-account rejection) all pass; old-client compatibility confirmed; + migrations verified up/down (the down path is lossy, see open item 22). Adds + open items 22–24 (lossy down-migration, unrun UI-on-hardware checks, and the + restated unauthenticated-endpoint limitation) and the release notes. diff --git a/docs/scheduler-overhaul-release-notes.md b/docs/scheduler-overhaul-release-notes.md new file mode 100644 index 0000000..fa15185 --- /dev/null +++ b/docs/scheduler-overhaul-release-notes.md @@ -0,0 +1,164 @@ +# Scheduler Overhaul — Release Notes + +Status: prepared for rollout. These are **instructions only**; nothing here has +been executed. The contract lives at `docs/schedule-api-contract.md` (kept +byte-identical in both repos). + +This document is kept **byte-identical** in both repositories: + +- `radiantwave` (C++/SDL3 Kiosk app) — `docs/scheduler-overhaul-release-notes.md` +- `portal` (SvelteKit/Node) — `docs/scheduler-overhaul-release-notes.md` + +--- + +## What changed + +### Portal + +- **Schedules are assigned to a License Key**, not a user. `Schedule` gained + `licenseKeyId` (unique, nullable) and `type` (`daily` | `weekly` | null); + `Block` gained `day` (null for daily, `mon`..`sun` for weekly) and its + `dynamicAudio` default became `true`. +- **New Kiosk endpoint** `GET /api/v2/licenses/{licenseKey}/schedule` returns the + target payload (`version`, `type`, `daily`, `weekly`, `blocks`) with an `ETag` + derived from `version`, supports conditional GET (`304`), and is `404` for an + unknown key. +- **License validation** (`POST /api/v2/license`) accepts an optional + `systemName` and returns an additive `systemNameStatus` + (`ok` | `unchanged` | `conflict` | `invalid`). Validation always succeeds; a + non-`ok` name status is advisory. +- **New Kiosk endpoint** `PUT /api/v2/licenses/{licenseKey}/system-name` + (`200` / `400 invalid_name` / `404 unknown_license` / `409 name_taken`, + idempotent). +- **System Name** is a human-readable label for a License Key. It is set only + by the Kiosk and shown read-only in the Portal (dashboard and admin lists), + falling back to displaying the License Key. There is no Portal rename path. + Names are unique per account, compared case-insensitively, trimmed, 1–40 + characters. +- **Schedule editor** (protected user route `(protected)/(user)/schedule`): + a per-system picker labelled by System Name, a Daily/Weekly type selector with + confirmation when switching a populated schedule (daily→weekly prefills all + seven days; weekly→daily and clearing start empty), per-day weekly editing, + AM/PM `TimeInput` (5-minute suggestions plus manual minute entry), midnight + crossing labelled "ends next day", an on-by-default dynamic-audio toggle, gap + helper text, and an informational note that times run in the Kiosk's own + timezone (browser timezone fallback; no external GeoIP service). +- **Copy to…** copies one system's Schedule to one or more other systems in the + same account, by value, in a single database transaction (all or none). Target + Blocks get fresh IDs so target `version`/ETag changes; `dynamicAudio` and + Proclamation order are preserved. Overwriting a target that already has a + schedule requires explicit confirmation. Copies are independent. +- **OpenAPI** (`docs/openapi.yaml`) documents the new endpoints and payloads. + +### Kiosk (`radiantwave`) + +- **12-hour AM/PM** time display everywhere the Kiosk shows a time; storage and + parsing remain 24-hour `HH:MM`. The Date Format setting is unchanged. +- **System Settings** menu contains Email Address, License Key, **System Name**, + **Schedule** (on/off), WiFi Setup, Time Zone Setup, and Date Format. The list + is data-driven. +- **System Name** entry (trimmed, 1–40), sent on license validation and via + `PUT /api/v2/licenses/{key}/system-name`. Conflicts/invalid names are reverted + locally with a user-facing message; network failures keep the new name locally + and retry (pending-sync flag) on boot, on validation, and on a timer. +- **Schedule client**: conditional GET (`If-None-Match`) of + `GET /api/v2/licenses/{key}/schedule`, on boot, on network-up, and every + 5 minutes. The last payload is cached to + `~/.local/radiantwave/schedule_cache.json` with an **atomic write** (temp file + - rename); a corrupt cache is ignored. Offline uses the cache; no cache and no + network behaves as schedules-off. +- **Schedule engine**: given the payload, the local wall-clock time in the + Kiosk's Timezone setting, and the weekday, picks the active Block. Half-open + `[start, end)` boundaries; midnight-crossing Blocks belong to their start day + (weekly Sunday spills into Monday); invalid Blocks are skipped with a warning + and treated as gaps. Active Block → its Proclamations; `dynamicAudio` true + forces dynamic audio, false uses the locally chosen audio; gaps / no schedule + / toggle off use local selection exactly as before. +- **Schedule toggle** in System Settings (`proclamation.schedule_enabled`, + default on, Kiosk-local, not synced). Off takes no action but the cache keeps + updating. + +--- + +## Migration notes + +- **Portal migrations are additive `up` migrations** and are safe to deploy + while old Kiosks keep running: + - `20260929120000_add_license_key_system_name` — adds + `LicenseKey.systemName` / `systemNameLower` and a unique index on + `(userId, systemNameLower)`. + - `20260929130000_move_schedule_to_license_key` — adds `Schedule.licenseKeyId` + / `type` and `Block.day`, changes the `dynamicAudio` default to `true`, + makes the legacy `Schedule.userId` optional/non-unique, and backfills each + existing per-user Schedule to that account's **oldest** License Key as a + **daily** schedule. Schedules that cannot be mapped (no owning user/license) + are retained with `licenseKeyId NULL` and reported via a migration `NOTICE` + — they are never silently dropped. +- **Existing (per-user) schedules survive the `up` migration** and are mapped to + a License Key on the same account. +- **⚠ The `down` path is lossy for data created under the new model.** Back up + `Schedule`, `Block`, and `BlockProclamations` before any down-migration. + Rolling back past the Phase 3 migration deletes per-License-Key schedules (see + the contract's Open item 22). +- No Portal schema change affects Kiosks (the Kiosk stores nothing new on the + server beyond the System Name label and its schedule mapping). + +--- + +## Recommended rollout order + +Each step is independent; do not skip the verification gates. Do **not** tag, +push, or deploy as part of reading this document — these are instructions. + +### Step 1 — Run Portal migrations and deploy the Portal + +1. Back up the Portal database (at minimum the `Schedule`, `Block`, + `BlockProclamations`, and `LicenseKey` tables). +2. Apply migrations: `npm run deploy` (which runs `npx prisma migrate deploy`). +3. Confirm `npx prisma migrate status` reports the database is up to date and + review the migration logs for the unmappable-schedule `NOTICE`, if any. +4. Deploy the Portal build (the existing CI/deploy path). + +**Rollback (Step 1):** redeploy the previous Portal build. Because the `up` +migrations are additive, the old Portal continues to work against the migrated +schema; no schema rollback is required to redeploy the old Portal. If a schema +rollback is unavoidable, restore from the Step-1 backup (the `down` path is +lossy — see Migration notes). + +### Step 2 — Verify old Kiosks still work + +1. Against the deployed Portal, confirm an **old Kiosk** (no `systemName`, using + the legacy endpoints) still: + - validates via `POST /api/v2/license` (no `systemName`) and receives `200`; + - reads `GET /api/v2/schedule?email=…&licenseKey=…` and still finds its + `blocks`; + - is unaffected by the new fields, and its stored System Name (if any) is + never cleared. +2. Confirm the Portal dashboard/admin show System Names read-only. + +**Rollback (Step 2):** none needed — this step is read-only verification. If old +Kiosks misbehave, roll back the Portal (Step 1 rollback) before proceeding. + +### Step 3 — Release the Kiosk build + +1. Build and validate the Kiosk through the normal path: tag → CI → the + auto-update channel. (Tagging triggers production CI; do it only when ready.) +2. Confirm the released build reports the expected version and updates in place. +3. Spot-check a device after update: 12-hour times, System Settings contents, + schedule on/off, and that an assigned Schedule is fetched and applied. + +**Rollback (Step 3):** use the existing auto-update rollback (re-publish or +pin the previous known-good Kiosk version). The Kiosk tolerates both old and new +Portal payloads, so a Portal rollback alone does not require a Kiosk rollback, +and vice versa. + +--- + +## Known limitations + +- Kiosk API endpoints are **unauthenticated** (key-only), mitigated by a + 10 requests/min/IP rate limit. Authentication is out of scope for this + overhaul. +- The Phase 3 migration's `down` path is lossy for new-model schedules + (see Migration notes and contract Open item 22). +- The Portal does not offer a rename path for System Names by design.