# 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.