mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
#236: the wall canvas was secretly framebuffer space rather than the wall as the audience sees it. Invisible while every panel is the normal way up, and actively misleading the moment one isn't — two portrait-mounted panels standing side by side had to be STACKED VERTICALLY in the editor, with a pre-rotated copy of every video, before the output came out right. It worked, but only after trial and error, and it meant a portrait wall could never reuse content as-is. Each panel now carries a mounting rotation (0/90/180/270 clockwise, the same convention as the per-device orientation setting), the canvas means the physical wall, and the player works out the mapping. The geometry lives in one place, server/lib/wall-geometry.js, because four players have to agree on it to the pixel across a seam. Existing walls need no migration and do not move. Every wall in the field is rotation 0, and that case takes the original expression verbatim on all three players rather than the algebraically-equal centre-based one — the two differ in the last float bit, and a float's worth of disagreement between two panels is a hairline seam down a wall that was aligned yesterday. Pinned by the first test in wall-geometry.test.js and by wall-payload.test.js. While a display is in a wall its panel rotation replaces its own orientation: both describe the same physical fact, so honouring both turned the content twice. #235: a wall replaced its members' cards, so one dead panel of a four-panel wall was invisible from the dashboard, and inspecting a single screen meant pulling it out of the live wall and putting it back. The wall screen now lists its panels with live online state, a per-panel screenshot request, and a link to each device's page; the dashboard wall card carries per-member status chips that track socket updates. Tests: wall-geometry.test.js re-simulates the CSS box independently and asserts each panel's viewport maps onto exactly its own rect of wall space, for every rotation, plus a mixed wall and the Tizen player's hand-ported copy executed against the canonical rule. Full server suite green (1260). Not verified here: the Android and Tizen renders on real hardware. Kotlin compiles clean; the maths is shared/tested, the view plumbing is not.
1876 lines
61 KiB
YAML
1876 lines
61 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: ScreenTinker Public API
|
|
version: 1.9.29
|
|
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.
|
|
- name: pip
|
|
description: Picture-in-picture / overlay push (image or web on top of the running playlist).
|
|
|
|
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
|
|
|
|
# --- Network addresses -------------------------------------------------------------
|
|
# Two different addresses, easy to confuse, so both are spelled out. They answer
|
|
# different questions and either can be null.
|
|
ip_address:
|
|
type: [string, "null"]
|
|
description: |
|
|
The device's **public (WAN)** address as observed by the server when the player
|
|
connected — i.e. what the internet sees. Behind a reverse proxy this is taken from
|
|
the first `X-Forwarded-For` entry, otherwise the socket peer address. Every device
|
|
on one site normally shares this. Null until the device has connected at least once.
|
|
local_ip:
|
|
type: [string, "null"]
|
|
description: |
|
|
The device's **own address on its local network** (e.g. `192.168.1.42`), as
|
|
reported by the player itself. This is the one to use to reach a panel directly on
|
|
site. Null on players that do not report it, or where the platform withholds it.
|
|
|
|
# --- Latest telemetry --------------------------------------------------------------
|
|
# Flattened from the most recent telemetry report. All null for a device that has
|
|
# never reported, and for platforms that cannot supply a given metric — treat every
|
|
# field here as optional rather than assuming a web player reports what Android does.
|
|
wifi_ssid:
|
|
type: [string, "null"]
|
|
description: |
|
|
Wi-Fi network name, or null on a wired/unknown connection.
|
|
|
|
Special value `"permission"` means the device is on Wi-Fi but the operating system
|
|
withheld the name — on Android 10+ reading the SSID requires a location permission
|
|
that ScreenTinker only asks for if an operator opts in. Treat `"permission"` as
|
|
"connected, name unavailable", not as a network literally called that.
|
|
wifi_rssi:
|
|
type: [integer, "null"]
|
|
description: Signal strength in dBm (negative; closer to zero is stronger).
|
|
battery_level:
|
|
type: [integer, "null"]
|
|
description: Battery percentage 0-100, or null on mains-powered hardware.
|
|
battery_charging:
|
|
type: [integer, "null"]
|
|
description: 1 charging, 0 not charging, null unknown.
|
|
storage_free_mb:
|
|
type: [integer, "null"]
|
|
storage_total_mb:
|
|
type: [integer, "null"]
|
|
ram_free_mb:
|
|
type: [integer, "null"]
|
|
ram_total_mb:
|
|
type: [integer, "null"]
|
|
cpu_usage:
|
|
type: [number, "null"]
|
|
description: Recent CPU utilisation as a percentage, where the platform exposes it.
|
|
uptime_seconds:
|
|
type: [integer, "null"]
|
|
description: Seconds since the device booted.
|
|
|
|
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 | directory-search
|
|
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"] }
|
|
muted:
|
|
type: boolean
|
|
description: >
|
|
Mute this item's audio. Applied to playing devices in real time
|
|
(device:mute-changed) and persisted into the published snapshot, so it
|
|
also takes effect on the next playlist load. (#129)
|
|
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 }.' }
|
|
|
|
# ---------------------------------------------------------------------- pip
|
|
/pip:
|
|
post:
|
|
tags: [pip]
|
|
summary: Show a picture-in-picture overlay on a device or group
|
|
description: |
|
|
Pushes an image or web overlay onto a device (or every device in a group) on top
|
|
of the running playlist, in real time (device:pip-show). The player fetches `uri`
|
|
itself (same trust model as remote_url content; the server does not proxy it). A
|
|
`web` overlay renders an arbitrary page in an iframe, so this requires scope: full. (#109)
|
|
x-required-scope: full
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [device_id, type, uri]
|
|
properties:
|
|
device_id: { type: string, description: A device id OR a group id (expanded to its members). }
|
|
type: { type: string, enum: [image, web] }
|
|
uri: { type: string, description: Absolute http(s) URL the player fetches directly. }
|
|
position: { type: string, enum: [top-left, top-right, bottom-left, bottom-right, center], default: top-right }
|
|
width: { type: integer, description: 'px (40-3840)', default: 480 }
|
|
height: { type: integer, description: 'px (40-3840)', default: 360 }
|
|
duration: { type: integer, description: 'seconds; 0 = until cleared (0-86400)', default: 0 }
|
|
title: { type: string }
|
|
title_color: { type: string, description: '#RRGGBB' }
|
|
background_color: { type: string, description: '#RRGGBB (transparency via opacity)' }
|
|
opacity: { type: number, description: '0-1', default: 1 }
|
|
border_radius: { type: integer, description: 'px (0-512)', default: 0 }
|
|
close_button: { type: boolean }
|
|
responses:
|
|
'200': { description: '{ success, pip_id, target, sent, offline, total, results }.' }
|
|
'400': { description: Validation error (type / uri scheme / position / numeric bounds / color). }
|
|
'404': { description: Device or group not found in this workspace. }
|
|
delete:
|
|
tags: [pip]
|
|
summary: Clear a picture-in-picture overlay
|
|
description: 'Clears the overlay on a device or group (device:pip-clear). Requires scope: full. (#109)'
|
|
x-required-scope: full
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [device_id]
|
|
properties:
|
|
device_id: { type: string, description: A device id OR a group id. }
|
|
pip_id: { type: string, description: Omit to clear whatever overlay is showing. }
|
|
responses:
|
|
'200': { description: '{ success, target, sent, offline, total, results }.' }
|
|
'404': { description: Device or group not found in this workspace. }
|
|
/pip/clear:
|
|
post:
|
|
tags: [pip]
|
|
summary: Clear a picture-in-picture overlay (alias for DELETE /pip)
|
|
description: 'Same as DELETE /pip; provided for clients that prefer POST. Requires scope: full. (#109)'
|
|
x-required-scope: full
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [device_id]
|
|
properties:
|
|
device_id: { type: string, description: A device id OR a group id. }
|
|
pip_id: { type: string, description: Omit to clear whatever overlay is showing. }
|
|
responses:
|
|
'200': { description: '{ success, target, sent, offline, total, results }.' }
|
|
'404': { description: Device or group not found in this workspace. }
|
|
|
|
# ---------------------------------------------------------------- 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
|
|
enum: [0, 90, 180, 270]
|
|
default: 0
|
|
description: >-
|
|
How this panel is physically mounted, as degrees CLOCKWISE that its image
|
|
must be turned to come out upright on the wall (the same convention as a
|
|
device's `orientation`). canvas_* are in WALL space — the wall as the
|
|
audience sees it — so a portrait-mounted 1920x1080 panel is a tall tile
|
|
with rotation 90, and content needs no pre-rotating. Anything other than
|
|
0/90/180/270 is stored as 0. While a panel is in a wall this replaces its
|
|
own `orientation`, so the two can never rotate the content twice.
|
|
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. }
|