docs: document Phase 3 schedule endpoint, model, and resolved open items

This commit is contained in:
NeilClack
2026-09-29 10:35:44 -05:00
parent 9a2888af36
commit f6c937b892
2 changed files with 153 additions and 31 deletions
+113 -8
View File
@@ -245,13 +245,16 @@ paths:
pairing. pairing.
Returns every Block with its start/end times and the ordered list of Returns every Block with its start/end times and the ordered list of
selected Proclamation IDs. Overnight blocks (an end clock time before selected Proclamation IDs (the legacy daily flattening in `blocks`), and
the start) are supported and flagged with `overnight: true` and a is extended additively with `version`, `type`, `daily`, and `weekly`.
wrapping `durationMinutes`. Each block also carries `dynamicAudio`; Overnight blocks (an end clock time before the start) are supported and
when true, the kiosk enables dynamic audio for the block's duration, flagged with `overnight: true` and a wrapping `durationMinutes`. Each
overriding any audio selections made on the device. Proclamation block also carries `dynamicAudio`; when true, the kiosk enables dynamic
content is not included; the kiosk fetches it separately via audio for the block's duration, overriding any audio selections made on
`/api/v2/proclamations`. the device. Proclamation content is not included; the kiosk fetches it
separately via `/api/v2/proclamations`. For `type: "weekly"`, `blocks` is
an empty array (the server cannot flatten weekly in the kiosk's
timezone). This endpoint emits no ETag and never returns 304.
operationId: getSchedule operationId: getSchedule
parameters: parameters:
- in: query - in: query
@@ -309,6 +312,66 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorMessage' $ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/schedule:
get:
tags: [Schedule]
summary: Get a License Key's schedule (Kiosk-facing, conditional)
description: |
Kiosk-facing, License-Key-only schedule read. Returns the target payload
(`version`, `type`, `daily`, `weekly`, `blocks`). `version` is a
content-derived opaque string and is also the `ETag`; a matching
`If-None-Match` returns `304`. An unknown License Key is `404`; a key
with no Schedule returns `type: null` with empty lists. No
authentication (see the API description) beyond the 10/min/IP limit.
operationId: getLicenseSchedule
parameters:
- in: path
name: licenseKey
required: true
schema:
type: string
pattern: '^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
- in: header
name: If-None-Match
required: false
schema:
type: string
description: Prior ETag; when it matches, returns 304.
responses:
'200':
description: The target schedule payload.
headers:
ETag:
schema:
type: string
description: The quoted `version`.
content:
application/json:
schema:
$ref: '#/components/schemas/SchedulePayload'
'304':
description: Not modified; the caller keeps its cached payload.
headers:
ETag:
schema:
type: string
'404':
description: 'Unknown license (`unknown_license`).'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
'429':
description: Rate limit exceeded (10 requests per minute per IP).
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
/api/v2/licenses/{licenseKey}/system-name: /api/v2/licenses/{licenseKey}/system-name:
put: put:
tags: [License] tags: [License]
@@ -661,9 +724,51 @@ components:
email: email:
type: string type: string
format: email format: email
version:
type: string
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks: blocks:
type: array type: array
maxItems: 4 items:
$ref: '#/components/schemas/ScheduleBlock'
SchedulePayload:
type: object
required: [version, type, daily, weekly, blocks]
properties:
version:
type: string
description: Opaque content hash; doubles as the ETag.
type:
type: string
nullable: true
enum: [daily, weekly, null]
daily:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
weekly:
type: object
description: Present with all seven keys when `type` is weekly.
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ScheduleBlock'
blocks:
type: array
description: Legacy daily flattening; empty for weekly.
items: items:
$ref: '#/components/schemas/ScheduleBlock' $ref: '#/components/schemas/ScheduleBlock'
OnboardingRequest: OnboardingRequest:
+40 -23
View File
@@ -495,45 +495,55 @@ later phase.
Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here. Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here.
2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the 2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the
Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no
schedule". schedule". Implemented in Phase 3.
3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained; the 3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained.
brief does not state a cap. Whether weekly allows 4/day or 4 total is Phase 3 applies it **per day list** (each of the seven weekly days gets its
unresolved. own 4-block budget). Whether the cap should be lower is unresolved.
4. **Field naming.** The brief's `start`/`end` were mapped to the existing 4. **Field naming.** The brief's `start`/`end` were mapped to the existing
`startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk. `startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk.
5. **`version` derivation.** Algorithm (content hash vs max `updatedAt` + 5. **`version` derivation.** Resolved in Phase 3: a SHA-1 content hash of the
shape) deferred to implementation; it must be stable for equal content and serialized `{type, daily, weekly}` shape. Stable for equal content, changes
change on mutation. on any mutation.
6. **Unauthenticated key-only access.** The new endpoints authenticate by 6. **Unauthenticated key-only access.** The new endpoints authenticate by
License Key alone (per glossary). Anyone who knows a key could read the License Key alone (per glossary). Anyone who knows a key could read the
schedule or set the name. Mitigated only by the 10/min/IP rate limit. Known schedule or set the name. Mitigated only by the 10/min/IP rate limit. Known
limitation. limitation.
7. **`dynamicAudio` default flip.** DB default changes `false` → `true` for 7. **`dynamicAudio` default flip.** DB default changed `false` → `true` for
**new** Blocks; existing Blocks are left at their stored values (no **new** Blocks in Phase 3; existing Blocks keep their stored values.
backfill assumed). 8. **Per-user → per-License-Key migration.** Resolved in Phase 3: each existing
8. **Per-user → per-License-Key migration.** Accounts with multiple License per-user Schedule is mapped to that account's **oldest** License Key
Keys: which key inherits the existing per-user Schedule is unresolved; (`createdAt ASC`), as a daily schedule. Schedules with no owning
conservative default is the account's first/oldest key. user/license are retained with `licenseKeyId NULL` and reported via a
9. **Cross-day spillover validation.** Weekly `sun` spillover into `mon` (and migration `NOTICE` (not dropped).
daily same-list spillover) must be validated; exact error messaging is 9. **Cross-day spillover validation.** Implemented in Phase 3: weekly
deferred. `sun`→`mon` and daily self-wrap spillover are rejected as overlaps; the
shared `validateSchedule` is authoritative and mirrored client-side.
10. **Account scope for name uniqueness.** "Per account" = the owning User. 10. **Account scope for name uniqueness.** "Per account" = the owning User.
Legacy License Keys with only an `email` and no `userId` are an edge case. Legacy License Keys with only an `email` and no `userId` are an edge case
11. **Geoip timezone display.** Informational only, not implemented in the (written without a per-account uniqueness check).
Portal today. 11. **Geoip timezone display.** Phase 3 shows an informational note that times
run in the **Kiosk's** timezone and uses the **browser-resolved** timezone
as a fallback; no geoip dependency (paid or external) was added. The
Portal still stores no timezone.
12. **ETag on the legacy endpoint.** The legacy `GET /api/v2/schedule` emits no 12. **ETag on the legacy endpoint.** The legacy `GET /api/v2/schedule` emits no
ETag and never returns 304, to preserve old behaviour. ETag and never returns 304, to preserve old behaviour.
13. **`systemNameStatus` on v1.** Not added; v1 is unchanged. 13. **`systemNameStatus` on v1.** Not added; v1 is unchanged.
14. **Trim/case representation.** Names are trimmed; uniqueness is 14. **Trim/case representation.** Names are trimmed; uniqueness is
case-insensitive; the stored casing is preserved as provided. case-insensitive; the stored casing is preserved as provided.
15. **Sync cadence.** The brief's boot/network-up/5-minute defaults are 15. **Sync cadence.** Resolved in Phase 3: the Kiosk loads its on-disk cache on
targets; the current Kiosk has no such timer. boot, then pulls on boot, on a 5-minute timer, and on demand.
16. **Atomic cache file.** Path and format deferred; SQLite is the current 16. **Atomic cache file.** Resolved in Phase 3:
durable store. `~/.local/radiantwave/schedule_cache.json`, written atomically (temp file +
rename); a corrupt cache is ignored.
17. **`type` encoding for no schedule.** `type: null`; `daily`/`weekly` absent 17. **`type` encoding for no schedule.** `type: null`; `daily`/`weekly` absent
or empty. or empty.
18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the 18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the
legacy schedule endpoint. legacy schedule endpoint.
19. **Kiosk local toggle.** `proclamation.schedule_enabled` in `config.yaml`,
default **on**, surfaced as System Settings → Schedule. Not synced. When
off, the engine plays local selections but the cache keeps updating. The
older `proclamation.schedule_active` flag is retained in config but is not
consulted by the Phase 3 engine.
--- ---
@@ -544,3 +554,10 @@ later phase.
(`/api/v2/licenses/...`, additive license-validation fields), sync behavior, (`/api/v2/licenses/...`, additive license-validation fields), sync behavior,
System Name rules, dynamic audio, backward compatibility, touchpoints, System Name rules, dynamic audio, backward compatibility, touchpoints,
open items, and fixtures. open items, and fixtures.
- 2026-09-29 — Phase 3. Schedule is now per License Key with daily/weekly and
`Block.day`; the shared `validateSchedule` is authoritative; the key-only
`GET /api/v2/licenses/{licenseKey}/schedule` (ETag/304) ships and the legacy
endpoint is extended additively; the Portal editor is per-system with a
browser-fallback timezone note; the Kiosk adds the conditional client,
atomic cache, schedule engine, and a System Settings On/Off toggle. Open
items 2, 5, 7, 8, 9, 11, 15, and 16 are updated to reflect the changes.