mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-15 02:33:15 -06:00
Self-review follow-ups, kept as a separate commit so the review trail is honest. - Spec drift: POST /widgets/preview was documented scope 'read' but the method-based tokenScopeGate enforces 'write' for any POST, so a read-token integrator following the published docs would hit a surprise 403. The code is right; fix the SPEC to match it. - Guard it forever: test/openapi-contract.test.js cross-checks every spec operation's x-required-scope against the enforcement rule, and that every documented path is a public (token-reachable) router - both derived from the same config/api-surface.js. Adds js-yaml (devDep) to parse the spec. Spec/enforcement drift now fails CI. - Vendored Redoc: add frontend/vendor/README.md (library, version 2.3.9, source, update steps) and drop the dangling //# sourceMappingURL line so /docs doesn't 404 in devtools. Remaining (non-security) test-coverage gaps tracked in #92. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1729 lines
53 KiB
YAML
1729 lines
53 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: ScreenTinker Public API
|
|
version: 1.9.0
|
|
description: |
|
|
Public, token-scoped REST API for ScreenTinker digital signage.
|
|
|
|
## Authentication
|
|
Every endpoint (except the two unauthenticated device-render endpoints) accepts a
|
|
**scoped personal access token** presented as `Authorization: Bearer st_...`. The
|
|
dashboard's session JWT is also accepted, but this contract targets token consumers.
|
|
|
|
## Scope ladder (read < write < full)
|
|
Tokens carry one of three scopes; higher scopes subsume lower ones:
|
|
- `read` — all `GET` endpoints.
|
|
- `write` — all resource mutations (`POST` / `PUT` / `PATCH` / `DELETE`).
|
|
- `full` — operational fleet commands (currently only
|
|
`POST /groups/{id}/command`: reboot / shutdown / screen on/off).
|
|
|
|
Each operation advertises its minimum scope via the `x-required-scope` extension
|
|
and restates it in the operation description. A token with insufficient scope is
|
|
rejected with `403`.
|
|
|
|
All resources are scoped to the token's workspace; cross-workspace access is not
|
|
available to tokens.
|
|
servers:
|
|
- url: /api
|
|
|
|
tags:
|
|
- name: content
|
|
description: Media library items (uploads, remote URLs, YouTube).
|
|
- name: folders
|
|
description: Content folder hierarchy.
|
|
- name: playlists
|
|
description: Playlists and their items / per-item schedules.
|
|
- name: assignments
|
|
description: Device playlist items (per-device assignment helpers).
|
|
- name: devices
|
|
description: Registered display devices.
|
|
- name: layouts
|
|
description: Multi-zone screen layouts and zones.
|
|
- name: groups
|
|
description: Device groups and bulk operations.
|
|
- name: schedules
|
|
description: Time-based content scheduling.
|
|
- name: walls
|
|
description: Video walls (multi-device grids).
|
|
- name: reports
|
|
description: Proof-of-play and uptime reporting.
|
|
- name: widgets
|
|
description: Dynamic widgets (clock, weather, RSS, etc.) and their render output.
|
|
- name: activity
|
|
description: Activity log.
|
|
- name: kiosk
|
|
description: Interactive kiosk pages and their render output.
|
|
|
|
security:
|
|
- ApiToken: []
|
|
- SessionJWT: []
|
|
|
|
components:
|
|
securitySchemes:
|
|
ApiToken:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: "st_<token>"
|
|
description: |
|
|
Scoped personal access token. Send as `Authorization: Bearer st_...`.
|
|
The token's scope (`read`, `write`, or `full`) gates which operations
|
|
it may call (see the scope ladder in the API description).
|
|
SessionJWT:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
Dashboard session JWT. Also accepted on every authenticated endpoint,
|
|
but this spec targets scoped-token consumers.
|
|
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: string
|
|
|
|
Device:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
status:
|
|
type: string
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
playlist_id:
|
|
type: [string, "null"]
|
|
layout_id:
|
|
type: [string, "null"]
|
|
description: Assigned layout (null = fullscreen). Recently added.
|
|
timezone:
|
|
type: string
|
|
orientation:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
created_at:
|
|
type: integer
|
|
|
|
Playlist:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
description:
|
|
type: string
|
|
status:
|
|
type: string
|
|
description: draft | published
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
item_count:
|
|
type: integer
|
|
display_count:
|
|
type: integer
|
|
|
|
PlaylistItem:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
playlist_id:
|
|
type: string
|
|
content_id:
|
|
type: [string, "null"]
|
|
widget_id:
|
|
type: [string, "null"]
|
|
zone_id:
|
|
type: [string, "null"]
|
|
description: Optional layout-zone placement. Recently added.
|
|
sort_order:
|
|
type: integer
|
|
duration_sec:
|
|
type: integer
|
|
|
|
Content:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
filename:
|
|
type: string
|
|
mime_type:
|
|
type: string
|
|
file_size:
|
|
type: integer
|
|
duration_sec:
|
|
type: [number, "null"]
|
|
remote_url:
|
|
type: [string, "null"]
|
|
thumbnail_path:
|
|
type: [string, "null"]
|
|
folder_id:
|
|
type: [string, "null"]
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
|
|
Layout:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
width:
|
|
type: integer
|
|
height:
|
|
type: integer
|
|
is_template:
|
|
type: integer
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
zones:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Zone'
|
|
|
|
Zone:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
layout_id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
x_percent:
|
|
type: number
|
|
y_percent:
|
|
type: number
|
|
width_percent:
|
|
type: number
|
|
height_percent:
|
|
type: number
|
|
z_index:
|
|
type: integer
|
|
zone_type:
|
|
type: string
|
|
fit_mode:
|
|
type: string
|
|
background_color:
|
|
type: string
|
|
|
|
Widget:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
widget_type:
|
|
type: string
|
|
description: clock | weather | rss | text | webpage | social | directory-board
|
|
name:
|
|
type: string
|
|
config:
|
|
type: object
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
|
|
Group:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
color:
|
|
type: string
|
|
description: "#RRGGBB"
|
|
playlist_id:
|
|
type: [string, "null"]
|
|
device_count:
|
|
type: integer
|
|
workspace_id:
|
|
type: [string, "null"]
|
|
|
|
Schedule:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
device_id:
|
|
type: [string, "null"]
|
|
group_id:
|
|
type: [string, "null"]
|
|
zone_id:
|
|
type: [string, "null"]
|
|
content_id:
|
|
type: [string, "null"]
|
|
widget_id:
|
|
type: [string, "null"]
|
|
layout_id:
|
|
type: [string, "null"]
|
|
playlist_id:
|
|
type: [string, "null"]
|
|
title:
|
|
type: string
|
|
start_time:
|
|
type: string
|
|
end_time:
|
|
type: string
|
|
timezone:
|
|
type: string
|
|
recurrence:
|
|
type: [string, "null"]
|
|
priority:
|
|
type: integer
|
|
enabled:
|
|
type: integer
|
|
|
|
parameters:
|
|
Limit:
|
|
name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
description: Max rows to return.
|
|
Offset:
|
|
name: offset
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
description: Row offset for pagination.
|
|
|
|
paths:
|
|
# ------------------------------------------------------------------ content
|
|
/content:
|
|
get:
|
|
tags: [content]
|
|
summary: List content in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- name: folder
|
|
in: query
|
|
schema: { type: string }
|
|
- name: folder_id
|
|
in: query
|
|
schema: { type: string }
|
|
description: '"root"/"" = root-level only; UUID = that folder.'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Offset'
|
|
responses:
|
|
'200':
|
|
description: Array of content items.
|
|
post:
|
|
tags: [content]
|
|
summary: Upload a content file (multipart)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
responses:
|
|
'201': { description: Created content item. }
|
|
/content/folders:
|
|
get:
|
|
tags: [content]
|
|
summary: List distinct content folder names with counts
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200':
|
|
description: 'Array of { folder, count }.'
|
|
/content/remote:
|
|
post:
|
|
tags: [content]
|
|
summary: Add a remote-URL content item
|
|
description: 'Requires scope: write. SSRF-gated (http/https, no internal hosts).'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [url]
|
|
properties:
|
|
url: { type: string }
|
|
name: { type: string }
|
|
mime_type: { type: string }
|
|
responses:
|
|
'201': { description: Created content item. }
|
|
/content/youtube:
|
|
post:
|
|
tags: [content]
|
|
summary: Add a YouTube video as content
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [url]
|
|
properties:
|
|
url: { type: string }
|
|
name: { type: string }
|
|
responses:
|
|
'201': { description: Created content item. }
|
|
/content/{id}:
|
|
get:
|
|
tags: [content]
|
|
summary: Get content metadata
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Content item. }
|
|
'404': { description: Not found. }
|
|
put:
|
|
tags: [content]
|
|
summary: Update content metadata
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
filename: { type: string }
|
|
mime_type: { type: string }
|
|
remote_url: { type: string }
|
|
folder: { type: string }
|
|
folder_id: { type: string }
|
|
responses:
|
|
'200': { description: Updated content item. }
|
|
delete:
|
|
tags: [content]
|
|
summary: Delete content
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success, affectedDevices }.' }
|
|
/content/{id}/replace:
|
|
put:
|
|
tags: [content]
|
|
summary: Replace the content file (multipart)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
file: { type: string, format: binary }
|
|
responses:
|
|
'200': { description: Updated content item. }
|
|
/content/{id}/file:
|
|
get:
|
|
tags: [content]
|
|
summary: Download the content file
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Binary file stream. }
|
|
/content/{id}/thumbnail:
|
|
get:
|
|
tags: [content]
|
|
summary: Download the content thumbnail
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Binary image stream. }
|
|
|
|
# ------------------------------------------------------------------ folders
|
|
/folders:
|
|
get:
|
|
tags: [folders]
|
|
summary: List content folders in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200': { description: Array of folders. }
|
|
post:
|
|
tags: [folders]
|
|
summary: Create a folder
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
parent_id: { type: string }
|
|
responses:
|
|
'201': { description: Created folder. }
|
|
/folders/{id}:
|
|
put:
|
|
tags: [folders]
|
|
summary: Rename or move a folder
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
parent_id: { type: [string, "null"] }
|
|
responses:
|
|
'200': { description: Updated folder. }
|
|
delete:
|
|
tags: [folders]
|
|
summary: Delete a folder
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
|
|
# ---------------------------------------------------------------- playlists
|
|
/playlists:
|
|
get:
|
|
tags: [playlists]
|
|
summary: List playlists in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200': { description: Array of playlists. }
|
|
post:
|
|
tags: [playlists]
|
|
summary: Create a playlist
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
description: { type: string }
|
|
responses:
|
|
'201': { description: Created playlist. }
|
|
/playlists/{id}:
|
|
get:
|
|
tags: [playlists]
|
|
summary: Get a playlist with its items
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Playlist with items. }
|
|
put:
|
|
tags: [playlists]
|
|
summary: Update playlist name/description
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
description: { type: string }
|
|
responses:
|
|
'200': { description: Updated playlist. }
|
|
delete:
|
|
tags: [playlists]
|
|
summary: Delete a playlist
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/playlists/{id}/publish:
|
|
post:
|
|
tags: [playlists]
|
|
summary: Publish a playlist (snapshot + push to devices)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Published playlist with items. }
|
|
/playlists/{id}/discard:
|
|
post:
|
|
tags: [playlists]
|
|
summary: Discard draft changes (revert to published snapshot)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Reverted playlist with items. }
|
|
/playlists/{id}/items:
|
|
get:
|
|
tags: [playlists]
|
|
summary: List a playlist's items
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of playlist items. }
|
|
post:
|
|
tags: [playlists]
|
|
summary: Add a content or widget item to a playlist
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
content_id: { type: string }
|
|
widget_id: { type: string }
|
|
zone_id: { type: string }
|
|
sort_order: { type: integer }
|
|
duration_sec: { type: integer }
|
|
responses:
|
|
'201': { description: Created playlist item. }
|
|
/playlists/{id}/items/reorder:
|
|
post:
|
|
tags: [playlists]
|
|
summary: Reorder playlist items
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [order]
|
|
properties:
|
|
order:
|
|
type: array
|
|
items: { type: integer }
|
|
responses:
|
|
'200': { description: Reordered items. }
|
|
/playlists/{id}/items/{itemId}:
|
|
put:
|
|
tags: [playlists]
|
|
summary: Update a playlist item
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: itemId, in: path, required: true, schema: { type: integer } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
sort_order: { type: integer }
|
|
duration_sec: { type: integer }
|
|
zone_id: { type: [string, "null"] }
|
|
responses:
|
|
'200': { description: Updated item. }
|
|
delete:
|
|
tags: [playlists]
|
|
summary: Delete a playlist item
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: itemId, in: path, required: true, schema: { type: integer } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/playlists/{id}/items/{itemId}/schedules:
|
|
get:
|
|
tags: [playlists]
|
|
summary: Get a playlist item's schedule blocks
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: itemId, in: path, required: true, schema: { type: integer } }
|
|
responses:
|
|
'200': { description: Array of schedule blocks. }
|
|
put:
|
|
tags: [playlists]
|
|
summary: Replace a playlist item's schedule blocks
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: itemId, in: path, required: true, schema: { type: integer } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [blocks]
|
|
properties:
|
|
blocks:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
days:
|
|
type: array
|
|
items: { type: integer }
|
|
start: { type: string }
|
|
end: { type: string }
|
|
start_date: { type: [string, "null"] }
|
|
end_date: { type: [string, "null"] }
|
|
responses:
|
|
'200': { description: Stored schedule blocks. }
|
|
/playlists/{id}/assign:
|
|
post:
|
|
tags: [playlists]
|
|
summary: Assign the playlist to a device
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [device_id]
|
|
properties:
|
|
device_id: { type: string }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
|
|
# -------------------------------------------------------------- assignments
|
|
/assignments/device/{deviceId}:
|
|
get:
|
|
tags: [assignments]
|
|
summary: List a device's playlist items
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of items. }
|
|
post:
|
|
tags: [assignments]
|
|
summary: Add a content/widget item to a device's playlist
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
content_id: { type: string }
|
|
widget_id: { type: string }
|
|
zone_id: { type: string }
|
|
duration_sec: { type: integer }
|
|
sort_order: { type: integer }
|
|
responses:
|
|
'201': { description: Created item. }
|
|
/assignments/device/{deviceId}/reorder:
|
|
post:
|
|
tags: [assignments]
|
|
summary: Reorder a device's playlist items
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [order]
|
|
properties:
|
|
order:
|
|
type: array
|
|
items: { type: integer }
|
|
responses:
|
|
'200': { description: Reordered items. }
|
|
/assignments/device/{deviceId}/copy-to/{targetDeviceId}:
|
|
post:
|
|
tags: [assignments]
|
|
summary: Copy a device's playlist to another device
|
|
description: 'Requires scope: write. Both devices must share a workspace.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
- { name: targetDeviceId, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
replace: { type: boolean }
|
|
responses:
|
|
'200': { description: '{ success, copied }.' }
|
|
/assignments/{id}:
|
|
put:
|
|
tags: [assignments]
|
|
summary: Update a playlist item
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: integer } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
sort_order: { type: integer }
|
|
duration_sec: { type: integer }
|
|
zone_id: { type: [string, "null"] }
|
|
responses:
|
|
'200': { description: Updated item. }
|
|
delete:
|
|
tags: [assignments]
|
|
summary: Delete a playlist item
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: integer } }
|
|
responses:
|
|
'200': { description: '{ success, content_id }.' }
|
|
|
|
# ------------------------------------------------------------------ devices
|
|
/devices:
|
|
get:
|
|
tags: [devices]
|
|
summary: List devices in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Offset'
|
|
responses:
|
|
'200':
|
|
description: Array of devices.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Device' }
|
|
/devices/{id}:
|
|
get:
|
|
tags: [devices]
|
|
summary: Get a device with telemetry and assignments
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Device detail.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Device' }
|
|
'404': { description: Not found. }
|
|
put:
|
|
tags: [devices]
|
|
summary: Update a device
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
notes: { type: string }
|
|
timezone: { type: string }
|
|
orientation: { type: string }
|
|
default_content_id: { type: string }
|
|
layout_id: { type: [string, "null"] }
|
|
responses:
|
|
'200':
|
|
description: Updated device.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Device' }
|
|
delete:
|
|
tags: [devices]
|
|
summary: Delete a device
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
|
|
# ------------------------------------------------------------------ layouts
|
|
/layouts:
|
|
get:
|
|
tags: [layouts]
|
|
summary: List layouts (workspace + templates)
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- name: templates
|
|
in: query
|
|
schema: { type: string }
|
|
description: '"true" to list only templates.'
|
|
responses:
|
|
'200':
|
|
description: Array of layouts with zones.
|
|
post:
|
|
tags: [layouts]
|
|
summary: Create a layout
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
width: { type: integer }
|
|
height: { type: integer }
|
|
zones:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Zone' }
|
|
responses:
|
|
'201':
|
|
description: Created layout.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Layout' }
|
|
/layouts/{id}:
|
|
get:
|
|
tags: [layouts]
|
|
summary: Get a layout with zones
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Layout with zones.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Layout' }
|
|
put:
|
|
tags: [layouts]
|
|
summary: Update a layout (and optionally replace zones)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
width: { type: integer }
|
|
height: { type: integer }
|
|
zones:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Zone' }
|
|
responses:
|
|
'200': { description: Updated layout. }
|
|
delete:
|
|
tags: [layouts]
|
|
summary: Delete a layout
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/layouts/{id}/zones:
|
|
post:
|
|
tags: [layouts]
|
|
summary: Add a zone to a layout
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Zone' }
|
|
responses:
|
|
'201':
|
|
description: Created zone.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Zone' }
|
|
/layouts/{id}/zones/{zoneId}:
|
|
put:
|
|
tags: [layouts]
|
|
summary: Update a zone
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: zoneId, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Zone' }
|
|
responses:
|
|
'200': { description: Updated zone. }
|
|
delete:
|
|
tags: [layouts]
|
|
summary: Delete a zone
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: zoneId, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/layouts/{id}/duplicate:
|
|
post:
|
|
tags: [layouts]
|
|
summary: Duplicate a layout into the current workspace
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
responses:
|
|
'201': { description: Duplicated layout. }
|
|
/layouts/device/{deviceId}:
|
|
put:
|
|
tags: [layouts]
|
|
summary: Assign a layout to a device
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
layout_id: { type: [string, "null"] }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
|
|
# ------------------------------------------------------------------- groups
|
|
/groups:
|
|
get:
|
|
tags: [groups]
|
|
summary: List device groups in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200':
|
|
description: Array of groups.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Group' }
|
|
post:
|
|
tags: [groups]
|
|
summary: Create a device group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
color: { type: string }
|
|
responses:
|
|
'201':
|
|
description: Created group.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Group' }
|
|
/groups/{id}:
|
|
put:
|
|
tags: [groups]
|
|
summary: Update a group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
color: { type: string }
|
|
responses:
|
|
'200': { description: Updated group. }
|
|
delete:
|
|
tags: [groups]
|
|
summary: Delete a group (converts group schedules to per-device)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success, schedules_converted, devices }.' }
|
|
/groups/{id}/devices:
|
|
get:
|
|
tags: [groups]
|
|
summary: List devices in a group
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of devices. }
|
|
post:
|
|
tags: [groups]
|
|
summary: Add a device to a group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [device_id]
|
|
properties:
|
|
device_id: { type: string }
|
|
responses:
|
|
'201': { description: '{ success, playlist_id }.' }
|
|
/groups/{id}/devices/{deviceId}:
|
|
delete:
|
|
tags: [groups]
|
|
summary: Remove a device from a group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/groups/{id}/assign-content:
|
|
post:
|
|
tags: [groups]
|
|
summary: Add content to every device in a group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [content_id]
|
|
properties:
|
|
content_id: { type: string }
|
|
duration_sec: { type: integer }
|
|
responses:
|
|
'200': { description: '{ success, devices_updated }.' }
|
|
/groups/{id}/assign-playlist:
|
|
post:
|
|
tags: [groups]
|
|
summary: Assign a playlist to every device in a group
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [playlist_id]
|
|
properties:
|
|
playlist_id: { type: string }
|
|
responses:
|
|
'200': { description: '{ success, devices_updated }.' }
|
|
/groups/{id}/command:
|
|
post:
|
|
tags: [groups]
|
|
summary: Send an operational command to every device in a group
|
|
description: |
|
|
Sends reboot / shutdown / screen on/off / launch / update to all devices
|
|
in the group. Requires scope: full (the highest scope; tokens with only
|
|
read or write are rejected).
|
|
x-required-scope: full
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [type]
|
|
properties:
|
|
type:
|
|
type: string
|
|
enum: [screen_on, screen_off, launch, update, reboot, shutdown]
|
|
payload: { type: object }
|
|
responses:
|
|
'200': { description: '{ success, sent, offline, total, results }.' }
|
|
|
|
# ---------------------------------------------------------------- schedules
|
|
/schedules:
|
|
get:
|
|
tags: [schedules]
|
|
summary: List schedules (filterable)
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- { name: group_id, in: query, schema: { type: string } }
|
|
- { name: start, in: query, schema: { type: string } }
|
|
- { name: end, in: query, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Array of schedules.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Schedule' }
|
|
post:
|
|
tags: [schedules]
|
|
summary: Create a schedule (targets one device OR one group)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [start_time, end_time]
|
|
properties:
|
|
device_id: { type: string }
|
|
group_id: { type: string }
|
|
zone_id: { type: string }
|
|
content_id: { type: string }
|
|
widget_id: { type: string }
|
|
layout_id: { type: string }
|
|
playlist_id: { type: string }
|
|
title: { type: string }
|
|
start_time: { type: string }
|
|
end_time: { type: string }
|
|
timezone: { type: string }
|
|
recurrence: { type: string }
|
|
recurrence_end: { type: string }
|
|
priority: { type: integer }
|
|
color: { type: string }
|
|
responses:
|
|
'201':
|
|
description: Created schedule.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Schedule' }
|
|
/schedules/device/{deviceId}:
|
|
get:
|
|
tags: [schedules]
|
|
summary: Get schedules for a device (device + group level)
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of schedules. }
|
|
/schedules/week:
|
|
get:
|
|
tags: [schedules]
|
|
summary: Expanded week view for a device (resolves recurrences)
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, required: true, schema: { type: string } }
|
|
- { name: date, in: query, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of schedule instances. }
|
|
/schedules/{id}:
|
|
put:
|
|
tags: [schedules]
|
|
summary: Update a schedule
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Schedule' }
|
|
responses:
|
|
'200': { description: Updated schedule. }
|
|
delete:
|
|
tags: [schedules]
|
|
summary: Delete a schedule
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
|
|
# -------------------------------------------------------------------- walls
|
|
/walls:
|
|
get:
|
|
tags: [walls]
|
|
summary: List video walls (with attached devices)
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200': { description: Array of walls. }
|
|
post:
|
|
tags: [walls]
|
|
summary: Create a video wall
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
grid_cols: { type: integer }
|
|
grid_rows: { type: integer }
|
|
bezel_h_mm: { type: number }
|
|
bezel_v_mm: { type: number }
|
|
playlist_id: { type: string }
|
|
responses:
|
|
'201': { description: Created wall. }
|
|
/walls/{id}:
|
|
get:
|
|
tags: [walls]
|
|
summary: Get a video wall with devices
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Wall with devices. }
|
|
put:
|
|
tags: [walls]
|
|
summary: Update a video wall
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
grid_cols: { type: integer }
|
|
grid_rows: { type: integer }
|
|
bezel_h_mm: { type: number }
|
|
bezel_v_mm: { type: number }
|
|
sync_mode: { type: string }
|
|
leader_device_id: { type: string }
|
|
content_id: { type: string }
|
|
playlist_id: { type: string }
|
|
responses:
|
|
'200': { description: Updated wall. }
|
|
delete:
|
|
tags: [walls]
|
|
summary: Delete a video wall
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/walls/{id}/devices:
|
|
put:
|
|
tags: [walls]
|
|
summary: Set the wall's device grid positions (replaces member set)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [devices]
|
|
properties:
|
|
devices:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
device_id: { type: string }
|
|
grid_col: { type: integer }
|
|
grid_row: { type: integer }
|
|
rotation: { type: integer }
|
|
canvas_x: { type: number }
|
|
canvas_y: { type: number }
|
|
canvas_width: { type: number }
|
|
canvas_height: { type: number }
|
|
responses:
|
|
'200': { description: Updated wall with devices. }
|
|
/walls/{id}/content:
|
|
put:
|
|
tags: [walls]
|
|
summary: Set wall content (legacy single-video path)
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
content_id: { type: string }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/walls/{id}/device-config/{deviceId}:
|
|
get:
|
|
tags: [walls]
|
|
summary: Get wall config for a specific member device
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
- { name: deviceId, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Per-device wall config. }
|
|
|
|
# ------------------------------------------------------------------ reports
|
|
/reports/plays:
|
|
get:
|
|
tags: [reports]
|
|
summary: Query play logs
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- { name: content_id, in: query, schema: { type: string } }
|
|
- { name: start, in: query, schema: { type: string } }
|
|
- { name: end, in: query, schema: { type: string } }
|
|
- { name: limit, in: query, schema: { type: integer } }
|
|
responses:
|
|
'200': { description: Array of play-log rows. }
|
|
/reports/summary:
|
|
get:
|
|
tags: [reports]
|
|
summary: Aggregated play summary report
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- { name: start, in: query, schema: { type: string } }
|
|
- { name: end, in: query, schema: { type: string } }
|
|
- { name: group_by, in: query, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: 'Summary object: overall, by_content, by_device, by_hour, by_day.'
|
|
/reports/export:
|
|
get:
|
|
tags: [reports]
|
|
summary: Export proof-of-play as CSV
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- { name: start, in: query, schema: { type: string } }
|
|
- { name: end, in: query, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: CSV file.
|
|
content:
|
|
text/csv:
|
|
schema: { type: string }
|
|
/reports/uptime:
|
|
get:
|
|
tags: [reports]
|
|
summary: Device uptime report
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- { name: start, in: query, schema: { type: string } }
|
|
- { name: end, in: query, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Array of per-device uptime rows. }
|
|
|
|
# ------------------------------------------------------------------ widgets
|
|
/widgets:
|
|
get:
|
|
tags: [widgets]
|
|
summary: List widgets in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200':
|
|
description: Array of widgets.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Widget' }
|
|
post:
|
|
tags: [widgets]
|
|
summary: Create a widget
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [widget_type, name]
|
|
properties:
|
|
widget_type: { type: string }
|
|
name: { type: string }
|
|
config: { type: object }
|
|
responses:
|
|
'201':
|
|
description: Created widget.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Widget' }
|
|
/widgets/preview:
|
|
post:
|
|
tags: [widgets]
|
|
summary: Render an unsaved widget config to HTML (non-persisting)
|
|
description: 'Requires scope: write (any POST needs write under the scope ladder); renders to HTML without persisting anything.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [widget_type]
|
|
properties:
|
|
widget_type: { type: string }
|
|
config: { type: object }
|
|
responses:
|
|
'200':
|
|
description: Rendered widget HTML.
|
|
content:
|
|
text/html:
|
|
schema: { type: string }
|
|
/widgets/{id}:
|
|
get:
|
|
tags: [widgets]
|
|
summary: Get a widget
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Widget.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Widget' }
|
|
put:
|
|
tags: [widgets]
|
|
summary: Update a widget
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
config: { type: object }
|
|
responses:
|
|
'200': { description: Updated widget. }
|
|
delete:
|
|
tags: [widgets]
|
|
summary: Delete a widget
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/widgets/{id}/render:
|
|
get:
|
|
tags: [widgets]
|
|
summary: Render a widget as an HTML page (public)
|
|
description: |
|
|
Unauthenticated device-render endpoint. Players fetch this HTML directly,
|
|
so no token or scope is required.
|
|
security: []
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Rendered widget HTML.
|
|
content:
|
|
text/html:
|
|
schema: { type: string }
|
|
|
|
# -------------------------------------------------------------------- kiosk
|
|
/kiosk:
|
|
get:
|
|
tags: [kiosk]
|
|
summary: List kiosk pages in the current workspace
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
responses:
|
|
'200': { description: Array of kiosk pages. }
|
|
post:
|
|
tags: [kiosk]
|
|
summary: Create a kiosk page
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
config: { type: object }
|
|
responses:
|
|
'201': { description: Created kiosk page. }
|
|
/kiosk/{id}:
|
|
get:
|
|
tags: [kiosk]
|
|
summary: Get a kiosk page
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: Kiosk page. }
|
|
put:
|
|
tags: [kiosk]
|
|
summary: Update a kiosk page
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
config: { type: object }
|
|
responses:
|
|
'200': { description: Updated kiosk page. }
|
|
delete:
|
|
tags: [kiosk]
|
|
summary: Delete a kiosk page
|
|
description: 'Requires scope: write.'
|
|
x-required-scope: write
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200': { description: '{ success }.' }
|
|
/kiosk/{id}/render:
|
|
get:
|
|
tags: [kiosk]
|
|
summary: Render a kiosk page as an HTML page (public)
|
|
description: |
|
|
Unauthenticated device-render endpoint. Devices fetch this HTML directly,
|
|
so no token or scope is required.
|
|
security: []
|
|
parameters:
|
|
- { name: id, in: path, required: true, schema: { type: string } }
|
|
responses:
|
|
'200':
|
|
description: Rendered kiosk HTML.
|
|
content:
|
|
text/html:
|
|
schema: { type: string }
|
|
|
|
# ----------------------------------------------------------------- activity
|
|
/activity:
|
|
get:
|
|
tags: [activity]
|
|
summary: Get the activity log
|
|
description: 'Requires scope: read.'
|
|
x-required-scope: read
|
|
parameters:
|
|
- { name: device_id, in: query, schema: { type: string } }
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Offset'
|
|
responses:
|
|
'200': { description: Array of activity entries. }
|