Files
radiantwave/docs/scheduler-overhaul-release-notes.md
T

8.4 KiB
Raw Permalink Blame History

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

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.