Dev #1

Merged
NeilClack merged 25 commits from dev into main 2026-09-30 04:29:28 +00:00
4 changed files with 198 additions and 2 deletions
Showing only changes of commit 6a3ecaf734 - Show all commits
+1
View File
@@ -0,0 +1 @@
{"version":"7f53efde17d291a1ecf604e266d3803d1495b986","type":"daily","daily":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"weekly":{},"blocks":[{"id":"6a9be0a4-4a1c-4a2c-929d-7b6c6fe56d91","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"65fecafc-abc8-4796-bb12-ed8e9d99e0fe","startTime":"18:00","startMinutes":1080,"endTime":"20:30","endMinutes":1230,"overnight":false,"durationMinutes":150,"dynamicAudio":false,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}]}
@@ -0,0 +1 @@
{"version":"6de852ba72ce1ab3052150640730ee87a7d460b5","type":"weekly","daily":[],"weekly":{"mon":[{"id":"a1054b78-480a-4e87-8465-aac8d677c666","startTime":"06:00","startMinutes":360,"endTime":"07:00","endMinutes":420,"overnight":false,"durationMinutes":60,"dynamicAudio":true,"proclamationIds":["310138f3-adfc-45ce-ae52-07279cd25b48"]},{"id":"25979ec9-f7da-4aa4-864f-f74b1a2da78b","startTime":"07:00","startMinutes":420,"endTime":"08:30","endMinutes":510,"overnight":false,"durationMinutes":90,"dynamicAudio":true,"proclamationIds":["3805242e-4930-4dd2-afb1-130593b1dbab"]}],"tue":[],"wed":[],"thu":[],"fri":[],"sat":[],"sun":[{"id":"e2237d98-834c-433b-8979-b56ddad2ef7a","startTime":"23:00","startMinutes":1380,"endTime":"01:00","endMinutes":60,"overnight":true,"durationMinutes":120,"dynamicAudio":true,"proclamationIds":["fcd5a291-3d6e-4678-9d85-9b550e80f1c7"]}]},"blocks":[]}
+32 -2
View File
@@ -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.
+164
View File
@@ -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.