Dev #1
+113
-8
@@ -245,13 +245,16 @@ paths:
|
||||
pairing.
|
||||
|
||||
Returns every Block with its start/end times and the ordered list of
|
||||
selected Proclamation IDs. Overnight blocks (an end clock time before
|
||||
the start) are supported and flagged with `overnight: true` and a
|
||||
wrapping `durationMinutes`. Each block also carries `dynamicAudio`;
|
||||
when true, the kiosk enables dynamic audio for the block's duration,
|
||||
overriding any audio selections made on the device. Proclamation
|
||||
content is not included; the kiosk fetches it separately via
|
||||
`/api/v2/proclamations`.
|
||||
selected Proclamation IDs (the legacy daily flattening in `blocks`), and
|
||||
is extended additively with `version`, `type`, `daily`, and `weekly`.
|
||||
Overnight blocks (an end clock time before the start) are supported and
|
||||
flagged with `overnight: true` and a wrapping `durationMinutes`. Each
|
||||
block also carries `dynamicAudio`; when true, the kiosk enables dynamic
|
||||
audio for the block's duration, overriding any audio selections made on
|
||||
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
|
||||
parameters:
|
||||
- in: query
|
||||
@@ -309,6 +312,66 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
put:
|
||||
tags: [License]
|
||||
@@ -661,9 +724,51 @@ components:
|
||||
email:
|
||||
type: string
|
||||
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:
|
||||
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:
|
||||
$ref: '#/components/schemas/ScheduleBlock'
|
||||
OnboardingRequest:
|
||||
|
||||
@@ -495,45 +495,55 @@ later phase.
|
||||
Phase 1 (Kiosk) and Phase 2 (System Name) are assumed here.
|
||||
2. **`blocks` for weekly.** Chosen as an empty array (server cannot know the
|
||||
Kiosk timezone). Consequence: old Kiosks see weekly schedules as "no
|
||||
schedule".
|
||||
3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained; the
|
||||
brief does not state a cap. Whether weekly allows 4/day or 4 total is
|
||||
unresolved.
|
||||
schedule". Implemented in Phase 3.
|
||||
3. **Block cap.** The existing `MAX_BLOCKS = 4` per day list is retained.
|
||||
Phase 3 applies it **per day list** (each of the seven weekly days gets its
|
||||
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
|
||||
`startTime`/`endTime`; `startMinutes`/`endMinutes` retained for the Kiosk.
|
||||
5. **`version` derivation.** Algorithm (content hash vs max `updatedAt` +
|
||||
shape) deferred to implementation; it must be stable for equal content and
|
||||
change on mutation.
|
||||
5. **`version` derivation.** Resolved in Phase 3: a SHA-1 content hash of the
|
||||
serialized `{type, daily, weekly}` shape. Stable for equal content, changes
|
||||
on any mutation.
|
||||
6. **Unauthenticated key-only access.** The new endpoints authenticate by
|
||||
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
|
||||
limitation.
|
||||
7. **`dynamicAudio` default flip.** DB default changes `false` → `true` for
|
||||
**new** Blocks; existing Blocks are left at their stored values (no
|
||||
backfill assumed).
|
||||
8. **Per-user → per-License-Key migration.** Accounts with multiple License
|
||||
Keys: which key inherits the existing per-user Schedule is unresolved;
|
||||
conservative default is the account's first/oldest key.
|
||||
9. **Cross-day spillover validation.** Weekly `sun` spillover into `mon` (and
|
||||
daily same-list spillover) must be validated; exact error messaging is
|
||||
deferred.
|
||||
7. **`dynamicAudio` default flip.** DB default changed `false` → `true` for
|
||||
**new** Blocks in Phase 3; existing Blocks keep their stored values.
|
||||
8. **Per-user → per-License-Key migration.** Resolved in Phase 3: each existing
|
||||
per-user Schedule is mapped to that account's **oldest** License Key
|
||||
(`createdAt ASC`), as a daily schedule. Schedules with no owning
|
||||
user/license are retained with `licenseKeyId NULL` and reported via a
|
||||
migration `NOTICE` (not dropped).
|
||||
9. **Cross-day spillover validation.** Implemented in Phase 3: weekly
|
||||
`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.
|
||||
Legacy License Keys with only an `email` and no `userId` are an edge case.
|
||||
11. **Geoip timezone display.** Informational only, not implemented in the
|
||||
Portal today.
|
||||
Legacy License Keys with only an `email` and no `userId` are an edge case
|
||||
(written without a per-account uniqueness check).
|
||||
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
|
||||
ETag and never returns 304, to preserve old behaviour.
|
||||
13. **`systemNameStatus` on v1.** Not added; v1 is unchanged.
|
||||
14. **Trim/case representation.** Names are trimmed; uniqueness is
|
||||
case-insensitive; the stored casing is preserved as provided.
|
||||
15. **Sync cadence.** The brief's boot/network-up/5-minute defaults are
|
||||
targets; the current Kiosk has no such timer.
|
||||
16. **Atomic cache file.** Path and format deferred; SQLite is the current
|
||||
durable store.
|
||||
15. **Sync cadence.** Resolved in Phase 3: the Kiosk loads its on-disk cache on
|
||||
boot, then pulls on boot, on a 5-minute timer, and on demand.
|
||||
16. **Atomic cache file.** Resolved in Phase 3:
|
||||
`~/.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
|
||||
or empty.
|
||||
18. **Legacy alias.** `key` remains accepted as a `licenseKey` alias on the
|
||||
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,
|
||||
System Name rules, dynamic audio, backward compatibility, touchpoints,
|
||||
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