docs: document Phase 3 schedule endpoint, model, and resolved open items
This commit is contained in:
+113
-8
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user