Release v2.12.0
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Technical Guides
|
||||
|
||||
These references describe the service interfaces and internal data model used to integrate with, deploy, and maintain Pulse Signage.
|
||||
|
||||
- [API reference](api.md) - player HTTP endpoints and onboarding integration.
|
||||
- [Database schema](schema.md) - application tables and relationships.
|
||||
- [WebSocket reference](websocket.md) - player control and snapshot channels.
|
||||
|
||||
For service configuration and deployment, see the [Compose guide](../../docker-compose/README.md).
|
||||
@@ -0,0 +1,390 @@
|
||||
# Player API Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Player service base URL: `http://localhost:8081`
|
||||
|
||||
This document covers the player HTTP surface only. The admin dashboard exposes its own routes for screen commands and onboarding management.
|
||||
|
||||
Access note: when `PULSE_SIGNAGE_SHARED_SECRET` is set, player-page endpoints require a valid `x-pulse-page-auth` token with the appropriate scope, and server-to-server endpoints require the signed request headers. When the secret is unset, these checks are disabled for compatibility, so the player service should remain inside a trusted deployment network. Pairing uses a short-lived random PIN displayed by the kiosk; the PIN is accepted only through the authenticated Web UI pairing flow.
|
||||
|
||||
When `PULSE_SIGNAGE_SHARED_SECRET` is set, the player pages sign same-origin API fetches with `x-pulse-page-auth`, and the web app signs server-to-player requests with `x-pulse-request-timestamp` plus `x-pulse-request-signature`. Page tokens auto-renew before expiry while the page stays active, and signed server requests are only accepted when their timestamp is fresh. If the secret is unset, those checks stay disabled for compatibility.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /`
|
||||
Returns the player onboarding landing page.
|
||||
Access: public within the trusted player deployment.
|
||||
|
||||
### `GET /onboard`
|
||||
Redirects to the authenticated Web UI pairing page for compatibility with older QR codes.
|
||||
Access: the Web UI pairing page requires a logged-in Web UI session.
|
||||
|
||||
### `GET /screen/{slug}`
|
||||
Returns the rendered player page for a screen.
|
||||
Access: the configured player may load only its persisted paired screen. An unpaired player is redirected to `/`; a different screen slug is rejected. The route is public within the trusted player deployment, but it no longer changes the player's binding.
|
||||
|
||||
### `GET /api/onboarding/status`
|
||||
Returns the persisted onboarding status for a device.
|
||||
Access: requires a page-auth token with the `onboarding` or `player` scope when shared-secret authentication is enabled.
|
||||
|
||||
Query fields:
|
||||
|
||||
- `deviceId` optional; the player device ID is used when omitted
|
||||
|
||||
### `GET /api/onboarding/screens`
|
||||
Returns the list of screens available for onboarding.
|
||||
Access: requires a page-auth token with the `onboarding` scope when shared-secret authentication is enabled.
|
||||
|
||||
### `GET /api/onboarding/qr`
|
||||
Returns an SVG QR code that points to the onboarding form.
|
||||
Access: public on the player service; the bridge version requires a signed server request when shared-secret authentication is enabled.
|
||||
|
||||
Query fields:
|
||||
|
||||
- `deviceId` optional; the player device ID is used when omitted
|
||||
|
||||
### `GET /api/onboarding/resolve`
|
||||
Resolves a short-lived kiosk pairing code to its device and client identifiers.
|
||||
Access: requires an onboarding page token or signed server request when shared-secret authentication is enabled.
|
||||
|
||||
Query fields:
|
||||
|
||||
- `pairingCode` required
|
||||
|
||||
Response fields:
|
||||
|
||||
- `deviceId`
|
||||
- `clientId`
|
||||
|
||||
### `GET /api/onboarding/session`
|
||||
Returns the current pairing session for the player page.
|
||||
Access: requires an onboarding page-auth token when shared-secret authentication is enabled.
|
||||
|
||||
Query fields:
|
||||
|
||||
- `deviceId` optional
|
||||
- `clientId` optional
|
||||
|
||||
Response fields:
|
||||
|
||||
- `deviceId`
|
||||
- `pairingCode`
|
||||
|
||||
### `POST /api/auth/page`
|
||||
Renews the current page-auth token before it expires.
|
||||
Access: internal to the player page and onboarding page. The request must include a valid `x-pulse-page-auth` header.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `token`
|
||||
- `issuedAt`
|
||||
- `expiresAt`
|
||||
|
||||
### `POST /api/screen-move-authorize`
|
||||
Stores a short-lived screen-move authorization token in an HTTP-only cookie.
|
||||
Access: requires a valid screen-move page-auth token in the request body.
|
||||
|
||||
Accepted request fields:
|
||||
|
||||
- `moveToken` required
|
||||
|
||||
Response fields:
|
||||
|
||||
- `ok`
|
||||
|
||||
### `POST /api/onboarding`
|
||||
Binds a device to a screen and client name.
|
||||
Access: internal-only. Requires an onboarding page-auth token or signed request when shared-secret authentication is enabled, plus a valid short-lived kiosk pairing code. Browser submissions must go through the authenticated Web UI pairing page.
|
||||
|
||||
Accepted request fields:
|
||||
|
||||
- `clientName` required
|
||||
- `screenSlug` required
|
||||
- `pairingCode` required
|
||||
- `clientId` required
|
||||
|
||||
Response fields:
|
||||
|
||||
- `deviceId`
|
||||
- `clientName`
|
||||
- `screenId`
|
||||
- `screenSlug`
|
||||
- `screenName`
|
||||
- `playerUrl`
|
||||
- `queued`
|
||||
|
||||
### `GET /api/media/config`
|
||||
Returns the upload directory configured for the player service.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `mediaDir`
|
||||
- `uploadDir`
|
||||
|
||||
### `PUT /api/media/{filename}`
|
||||
Writes an uploaded file into the player upload directory.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
|
||||
|
||||
### `DELETE /api/media/{filename}`
|
||||
Deletes a file from the player upload directory.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
|
||||
|
||||
### `GET /api/rtmp/session`
|
||||
Creates or reuses an RTMP-to-HLS session for a source URL.
|
||||
Access: requires a page-auth token with the `player` scope when shared-secret authentication is enabled.
|
||||
|
||||
Query fields:
|
||||
|
||||
- `source` required
|
||||
- `disableAudio` optional
|
||||
|
||||
Response fields:
|
||||
|
||||
- `key`
|
||||
- `playlistUrl`
|
||||
- `disableAudio`
|
||||
- `ready`
|
||||
- `live`
|
||||
|
||||
### `GET /api/rtmp/streams/{key}/index.m3u8`
|
||||
Returns the RTMP session HLS manifest.
|
||||
Access: internal-only.
|
||||
|
||||
### `GET /api/rtmp/streams/{key}/{fileName}`
|
||||
Returns an RTMP HLS segment or related stream file.
|
||||
Access: internal-only.
|
||||
|
||||
### `GET /api/screens/{slug}/playlist`
|
||||
Returns the current playlist payload for a screen.
|
||||
Access: requires a page-auth token with the `player` scope when shared-secret authentication is enabled. The player also checks that the browser is authorized for the requested screen.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `screen`
|
||||
- `playlist`
|
||||
- `slides`
|
||||
- `rssFeeds`
|
||||
- `apiSources`
|
||||
- `timetableGroups`
|
||||
- `revision`
|
||||
|
||||
### `GET /api/screens/{slug}/announcement`
|
||||
Returns the active announcement for a screen.
|
||||
Access: requires a page-auth token with the `player` scope when shared-secret authentication is enabled. The player also checks that the browser is authorized for the requested screen.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `announcement`
|
||||
- `revision`
|
||||
|
||||
### `GET /api/screens/{slug}/connections`
|
||||
### `GET /api/screens/{slug}/clients`
|
||||
Returns the live player connection snapshot for a screen.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled; it exposes live connection state.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `screen`
|
||||
- `screenSlug`
|
||||
- `count`
|
||||
- `connections`
|
||||
- `degraded`
|
||||
|
||||
### `POST /api/screens/{slug}/announcements/refresh`
|
||||
Notifies connected players that the active announcement should be refreshed.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `ok`
|
||||
- `screenSlug`
|
||||
- `sent`
|
||||
|
||||
### `POST /api/screens/{slug}/commands`
|
||||
Sends a command to the player connections for a screen.
|
||||
Access: internal-only and requires signed request headers when shared-secret authentication is enabled. The admin dashboard should remain the protected control surface for commands.
|
||||
|
||||
Accepted request fields:
|
||||
|
||||
- `command` required
|
||||
- `connectionId` optional
|
||||
- `clientId` optional
|
||||
- `blackout` optional when `command=blackout`
|
||||
|
||||
Command-specific fields:
|
||||
|
||||
- `url` for `redirect`
|
||||
- `clientName` for `setclientname`
|
||||
- `deviceId` for `setclientname`
|
||||
|
||||
Supported commands:
|
||||
|
||||
- `refresh`
|
||||
- `reload`
|
||||
- `redirect`
|
||||
- `pause`
|
||||
- `blackout`
|
||||
- `previous`
|
||||
- `next`
|
||||
- `left`
|
||||
- `right`
|
||||
- `setclientname`
|
||||
|
||||
If `connectionId` or `clientId` is provided, the command targets a single player connection. Otherwise it is broadcast to all connections for that screen.
|
||||
|
||||
Response fields:
|
||||
|
||||
- `screen`
|
||||
- `screenSlug`
|
||||
- `command`
|
||||
- `connectionId`
|
||||
- `sent`
|
||||
- `degraded`
|
||||
|
||||
## Response Shapes
|
||||
|
||||
### Onboarding Status Response
|
||||
|
||||
`GET /api/onboarding/status` returns an object with:
|
||||
|
||||
- `deviceId`
|
||||
- `onboarded`
|
||||
- `clientName`
|
||||
- `screenId`
|
||||
- `screenSlug`
|
||||
- `screenName`
|
||||
- `playerUrl`
|
||||
|
||||
### Onboarding Screens Response
|
||||
|
||||
`GET /api/onboarding/screens` returns an object with:
|
||||
|
||||
- `screens`
|
||||
|
||||
### QR Response
|
||||
|
||||
`GET /api/onboarding/qr` returns SVG markup.
|
||||
|
||||
### Upload Config Response
|
||||
|
||||
`GET /api/media/config` returns an object with:
|
||||
|
||||
- `mediaDir`
|
||||
- `uploadDir`
|
||||
|
||||
### RTMP Session Response
|
||||
|
||||
`GET /api/rtmp/session` returns an object with:
|
||||
|
||||
- `key`
|
||||
- `playlistUrl`
|
||||
- `disableAudio`
|
||||
- `ready`
|
||||
- `live`
|
||||
|
||||
### Onboarding Write Response
|
||||
|
||||
`POST /api/onboarding` returns an object with:
|
||||
|
||||
- `deviceId`
|
||||
- `clientName`
|
||||
- `screenId`
|
||||
- `screenSlug`
|
||||
- `screenName`
|
||||
- `playerUrl`
|
||||
- `queued`
|
||||
|
||||
### Playlist Response
|
||||
|
||||
`GET /api/screens/{slug}/playlist` returns an object with:
|
||||
|
||||
- `screen`
|
||||
- `playlist`
|
||||
- `slides`
|
||||
- `rssFeeds`
|
||||
- `apiSources`
|
||||
- `timetableGroups`
|
||||
- `revision`
|
||||
|
||||
### Connections Response
|
||||
|
||||
`GET /api/screens/{slug}/connections` and `GET /api/screens/{slug}/clients` return an object with:
|
||||
|
||||
- `screen`
|
||||
- `screenSlug`
|
||||
- `count`
|
||||
- `connections`
|
||||
- `degraded`
|
||||
|
||||
### Command Response
|
||||
|
||||
`POST /api/screens/{slug}/commands` returns an object with:
|
||||
|
||||
- `screen`
|
||||
- `screenSlug`
|
||||
- `command`
|
||||
- `connectionId`
|
||||
- `sent`
|
||||
- `degraded`
|
||||
|
||||
## Data Models
|
||||
|
||||
### Screen
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `slug`
|
||||
- `playlist_id`
|
||||
- `created_at`
|
||||
- `modified_at`
|
||||
|
||||
### Playlist
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `fade_between_slides`
|
||||
- `skip_unavailable_rtmp`
|
||||
- `canvas_id`
|
||||
|
||||
### Slide
|
||||
|
||||
- `id`
|
||||
- `title`
|
||||
- `body`
|
||||
- `duration_seconds`
|
||||
- `use_video_duration`
|
||||
- `disable_audio`
|
||||
- `scheduleRules`
|
||||
- `media_url`
|
||||
- `media_type`
|
||||
- `kind`
|
||||
- `template_id`
|
||||
- `template`
|
||||
- `content`
|
||||
|
||||
### Connection
|
||||
|
||||
- `id`
|
||||
- `clientId`
|
||||
- `clientName`
|
||||
- `deviceId`
|
||||
- `label`
|
||||
- `userAgent`
|
||||
- `viewport`
|
||||
- `page`
|
||||
- `currentSlide`
|
||||
- `currentSlideId`
|
||||
- `currentSlideTitle`
|
||||
- `paused`
|
||||
- `blackout`
|
||||
- `clientIp`
|
||||
- `remoteAddress`
|
||||
- `connectedAt`
|
||||
- `lastSeenAt`
|
||||
|
||||
## Notes
|
||||
|
||||
- The player API is the only surface documented here.
|
||||
- Web UI/admin routes are intentionally omitted, except where the player service itself exposes onboarding and upload endpoints.
|
||||
@@ -0,0 +1,353 @@
|
||||
# Database Schema
|
||||
|
||||
This app creates and maintains its schema at startup through `src/db/index.js`.
|
||||
The sections below summarize the current tables and their purpose.
|
||||
|
||||
Tables generally use a numeric auto-increment `id` primary key. The relationship table `d_announcement_screens` intentionally uses the composite `(announcement_id, screen_id)` primary key instead. Natural and relationship keys remain as unique constraints where needed. Timestamps are stored as `created_at` and `modified_at` when a table supports auditing.
|
||||
|
||||
## Admin
|
||||
|
||||
- `a_users` - admin user accounts and password hashes.
|
||||
- `a_roles` - named role definitions.
|
||||
- `a_permissions` - permission catalog seeded from the application constants.
|
||||
- `a_role_permissions` - many-to-many mapping between roles and permissions.
|
||||
- `a_user_roles` - many-to-many mapping between users and roles.
|
||||
- `a_sessions` - persisted admin session tokens.
|
||||
- `a_account_tokens` - short-lived account verification and password-reset tokens.
|
||||
- `a_user_invitations` - pending user invitations and assigned role ids.
|
||||
- `a_login_attempts` - login rate-limit and lockout state.
|
||||
|
||||
### `a_users`
|
||||
|
||||
- `id`, `name`, `username`, `email`, `email_verified_at`, `pending_email`, `pending_email_token_hash`, `pending_email_expires_at`, `password_hash`, `password_salt`, `password_iterations`, `must_change_password`, `account_locked`, `last_login_at`, `last_login_ip`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `username` is unique.
|
||||
|
||||
### `a_account_tokens`
|
||||
|
||||
- `id`, `user_id`, `token_type`, `token_hash`, `expires_at`, `used_at`, `created_at`
|
||||
- `token_hash` is unique.
|
||||
- Foreign key:
|
||||
- `user_id` -> `a_users.id` with `ON DELETE CASCADE`
|
||||
- Indexed by `(token_type, token_hash, expires_at)` and `(user_id, token_type)`.
|
||||
|
||||
### `a_user_invitations`
|
||||
|
||||
- `id`, `email`, `name`, `role_ids_json`, `token_hash`, `expires_at`, `used_at`, `created_at`, `created_by`
|
||||
- `token_hash` is unique.
|
||||
- Indexed by `(email, used_at, expires_at)` and `(created_by, created_at)`.
|
||||
|
||||
### `a_login_attempts`
|
||||
|
||||
- `id`, `rate_key`, `failed_count`, `last_failed_at`, `locked_until`, `created_at`, `modified_at`
|
||||
- `rate_key` is unique.
|
||||
|
||||
### `a_roles`
|
||||
|
||||
- `id`, `role_key`, `name`, `description`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `role_key` is unique.
|
||||
- `name` is unique.
|
||||
|
||||
### `a_permissions`
|
||||
|
||||
- `id`, `permission_key`, `name`, `section_name`, `description`, `created_at`, `modified_at`
|
||||
- `permission_key` is unique.
|
||||
|
||||
### `a_role_permissions`
|
||||
|
||||
- `id`, `role_id`, `permission_id`, `created_at`, `modified_at`
|
||||
- Foreign keys:
|
||||
- `role_id` -> `a_roles.id`
|
||||
- `permission_id` -> `a_permissions.id`
|
||||
- Unique key: `(role_id, permission_id)`
|
||||
|
||||
### `a_user_roles`
|
||||
|
||||
- `id`, `user_id`, `role_id`, `created_at`, `modified_at`
|
||||
- Foreign keys:
|
||||
- `user_id` -> `a_users.id`
|
||||
- `role_id` -> `a_roles.id`
|
||||
- Unique key: `(user_id, role_id)`
|
||||
|
||||
### `a_sessions`
|
||||
|
||||
- `id`, `session_hash`, `user_id`, `ip_address`, `user_agent`, `expires_at`, `created_at`, `created_by`, `last_used_at`, `modified_by`
|
||||
- `session_hash` is unique.
|
||||
- Foreign key:
|
||||
- `user_id` -> `a_users.id`
|
||||
|
||||
## Content
|
||||
|
||||
- `c_canvas_sizes` - reusable canvas presets for templates.
|
||||
- `c_playlists` - playlist definitions, playback options, and canvas assignment.
|
||||
- `c_templates` - slide templates with canvas and background settings.
|
||||
- `c_template_regions` - template region layout and metadata.
|
||||
- `c_slides` - slide records with template binding, JSON content, and thumbnail path.
|
||||
- `c_playlist_slides` - ordered playlist items and timing.
|
||||
- `c_playlist_slide_schedule_rules` - rule rows attached to playlist slides.
|
||||
|
||||
### `c_canvas_sizes`
|
||||
|
||||
- `id`, `name`, `width`, `height`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `(width, height)` is unique.
|
||||
|
||||
### `c_playlists`
|
||||
|
||||
- `id`, `name`, `fade_between_slides`, `skip_unavailable_rtmp`, `canvas_id`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `canvas_id` -> `c_canvas_sizes.id` with `ON DELETE SET NULL`
|
||||
|
||||
### `c_templates`
|
||||
|
||||
- `id`, `name`, `canvas_size_id`, `background_image_path`, `background_color`, `background_gradient`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `background_gradient` stores normalized linear gradient settings as JSON text, including angle and colour stops.
|
||||
- Foreign key:
|
||||
- `canvas_size_id` -> `c_canvas_sizes.id` with `ON DELETE SET NULL`
|
||||
|
||||
### `c_template_regions`
|
||||
|
||||
- `id`, `template_id`, `region_key`, `region_type`, `label`, `font_family`, `lock_ratio`, `animation_json`, `x`, `y`, `width`, `height`, `z_index`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `template_id` -> `c_templates.id` with `ON DELETE CASCADE`
|
||||
|
||||
### `c_slides`
|
||||
|
||||
- `id`, `title`, `template_id`, `content_json`, `thumbnail_path`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `template_id` -> `c_templates.id` with `ON DELETE SET NULL`
|
||||
|
||||
### `c_playlist_slides`
|
||||
|
||||
- `id`, `playlist_id`, `slide_id`, `position`, `duration_seconds`, `use_video_duration`, `disable_audio`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign keys:
|
||||
- `playlist_id` -> `c_playlists.id` with `ON DELETE CASCADE`
|
||||
- `slide_id` -> `c_slides.id` with `ON DELETE CASCADE`
|
||||
|
||||
### `c_playlist_slide_schedule_rules`
|
||||
|
||||
- `id`, `playlist_slide_id`, `position`, `start_datetime`, `end_datetime`, `start_time`, `end_time`, `schedule_days_json`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `playlist_slide_id` -> `c_playlist_slides.id` with `ON DELETE CASCADE`
|
||||
- Index:
|
||||
- `(playlist_slide_id, position)`
|
||||
|
||||
- Each playlist slide can have zero or more schedule rules.
|
||||
- Rules are evaluated with OR across rows and with AND across the fields inside a single rule.
|
||||
|
||||
## Devices
|
||||
|
||||
- `d_players` - player registry and connection metadata.
|
||||
- `d_screens` - screen records and playlist assignment.
|
||||
- `d_onboarding_devices` - device-to-screen bindings and onboarded client names.
|
||||
|
||||
### `d_players`
|
||||
|
||||
- `id`, `identifier`, `public_base_url`, `internal_base_url`, `last_seen_at`, `created_at`, `modified_at`
|
||||
- `id` is the primary key.
|
||||
- `identifier` is unique and is the stable player identity used by the app.
|
||||
|
||||
### `d_screens`
|
||||
|
||||
- `id`, `name`, `slug`, `playlist_id`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `slug` is unique.
|
||||
- Foreign keys:
|
||||
- `playlist_id` -> `c_playlists.id` with `ON DELETE SET NULL`
|
||||
|
||||
- Screens are no longer tied to a player foreign key directly; player registration and live connection metadata are tracked separately in `d_players`.
|
||||
|
||||
### `d_onboarding_devices`
|
||||
|
||||
- `id`, `device_id`, `client_name`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by`, `last_seen_at`
|
||||
- `device_id` is unique.
|
||||
- Foreign key:
|
||||
- `screen_id` -> `d_screens.id` with `ON DELETE SET NULL`
|
||||
|
||||
## Onboarding
|
||||
|
||||
- The onboarding flow uses `d_onboarding_devices` to bind a device to a screen and persist the client name.
|
||||
|
||||
## Announcements
|
||||
|
||||
- `d_announcements` - announcement content and display metadata.
|
||||
- `d_announcement_screens` - announcement-to-screen assignments.
|
||||
|
||||
### `d_announcements`
|
||||
|
||||
- `id`, `message`, `short_label`, `announcement_type`, `color_key`, `icon_key`, `duration_seconds`, `expires_at`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `announcement_type` defaults to `lower-third`.
|
||||
|
||||
### `d_announcement_screens`
|
||||
|
||||
- `announcement_id`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign keys:
|
||||
- `announcement_id` -> `d_announcements.id` with `ON DELETE CASCADE`
|
||||
- `screen_id` -> `d_screens.id` with `ON DELETE CASCADE`
|
||||
- Unique key: `(announcement_id, screen_id)`
|
||||
|
||||
## Integrations
|
||||
|
||||
- `i_rss_feeds` - RSS feed definitions and refresh cadence.
|
||||
- `i_rss_feed_items` - cached RSS feed items.
|
||||
- `i_api_sources` - API source definitions and last response snapshot.
|
||||
- `i_weather_locations` - configured weather locations and last response snapshot.
|
||||
- `i_timetable_groups` - grouped timetable definitions used by the timetable region.
|
||||
- `i_timetable_entries` - dated entries that belong to a timetable group.
|
||||
|
||||
### `i_rss_feeds`
|
||||
|
||||
- `id`, `name`, `feed_url`, `update_interval_value`, `update_interval_unit`, `item_limit`, `enabled`, `last_pulled_at`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
|
||||
### `i_rss_feed_items`
|
||||
|
||||
- `id`, `rss_feed_id`, `position`, `item_json`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `rss_feed_id` -> `i_rss_feeds.id` with `ON DELETE CASCADE`
|
||||
- Unique key:
|
||||
- `(rss_feed_id, position)`
|
||||
|
||||
### `i_api_sources`
|
||||
|
||||
- `id`, `name`, `api_url`, `auth_method`, `auth_username`, `auth_password`, `auth_bearer_token`, `auth_header_name`, `auth_header_value`, `token_url`, `token_request_body_json`, `token_response_path`, `token_refresh_url`, `token_refresh_request_body_json`, `token_refresh_response_path`, `token_header_name`, `token_header_prefix`, `items_path`, `update_interval_value`, `update_interval_unit`, `enabled`, `last_pulled_at`, `last_pull_error`, `last_response_status`, `last_response_content_type`, `last_response_json`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
|
||||
### `i_weather_locations`
|
||||
|
||||
- `id`, `name`, `location_label`, `latitude`, `longitude`, `timezone`, `provider`, `temperature_unit`, `wind_unit`, `precipitation_unit`, `update_interval_value`, `update_interval_unit`, `enabled`, `last_pulled_at`, `last_pull_error`, `last_response_json`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Stores configured weather locations and the most recent provider response used for forecast previews and weather regions.
|
||||
|
||||
### `i_timetable_groups`
|
||||
|
||||
- `id`, `name`, `short_description`, `timezone`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `timezone` defaults to `Europe/London`.
|
||||
|
||||
### `i_timetable_entries`
|
||||
|
||||
- `id`, `schedule_group_id`, `title`, `short_description`, `start_datetime`, `end_datetime`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- Foreign key:
|
||||
- `schedule_group_id` -> `i_timetable_groups.id` with `ON DELETE CASCADE`
|
||||
- Index:
|
||||
- `(schedule_group_id, start_datetime)`
|
||||
|
||||
## Operations
|
||||
|
||||
- `o_background_tasks` - queue and history for background jobs.
|
||||
- `o_app_state` - generic app state and version markers stored as key/value pairs.
|
||||
- `o_app_settings` - administrator-configurable application settings stored by key.
|
||||
- `o_audit_events` - retained audit events for administrator activity and system changes.
|
||||
|
||||
### `o_background_tasks`
|
||||
|
||||
- `id`, `task_key`, `task_type`, `title`, `category`, `status`, `payload_json`, `metadata_json`, `attempts`, `created_at`, `created_by`, `started_at`, `finished_at`, `error_message`
|
||||
- Indexed by `status`, `task_key`, and `task_type`.
|
||||
|
||||
### `o_app_state`
|
||||
|
||||
- `id`, `state_key`, `state_value`, `created_at`, `modified_at`
|
||||
- `state_key` is unique.
|
||||
- `schema_version` is stored here so startup can detect the previously recorded schema version before deciding whether migrations need to run.
|
||||
|
||||
### `o_app_settings`
|
||||
|
||||
- `id`, `setting_key`, `setting_value`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||
- `setting_key` is unique.
|
||||
- Values are stored as JSON and validated against the application setting definitions in `src/data/app-settings.js`.
|
||||
|
||||
### `o_audit_events`
|
||||
|
||||
- `id`, `occurred_at`, `category`, `event_type`, `actor_user_id`, `target_type`, `target_id`, `target_label`, `ip_address`, `user_agent`, `details_json`
|
||||
- Indexed by occurrence time, category and event type, actor, and target.
|
||||
|
||||
## Notes
|
||||
|
||||
- The schema is initialized with `CREATE TABLE IF NOT EXISTS`, so new installs can start from an empty database.
|
||||
- `src/db/bootstrap.js` seeds the canvas size defaults, default permissions, and the default administrator role.
|
||||
- `src/db/migrations.js` records the current schema version in `o_app_state` during startup so later launches can tell whether an update is happening.
|
||||
- The ER diagram shows declared foreign keys and the logical audit actor association; player registry and JSON-based references are intentionally not shown as foreign-key relationships.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
A_USERS {
|
||||
}
|
||||
A_ROLES {
|
||||
}
|
||||
A_PERMISSIONS {
|
||||
}
|
||||
A_ROLE_PERMISSIONS {
|
||||
}
|
||||
A_USER_ROLES {
|
||||
}
|
||||
A_SESSIONS {
|
||||
}
|
||||
A_ACCOUNT_TOKENS {
|
||||
}
|
||||
A_USER_INVITATIONS {
|
||||
}
|
||||
A_LOGIN_ATTEMPTS {
|
||||
}
|
||||
C_CANVAS_SIZES {
|
||||
}
|
||||
C_PLAYLISTS {
|
||||
}
|
||||
C_TEMPLATES {
|
||||
}
|
||||
C_TEMPLATE_REGIONS {
|
||||
}
|
||||
C_SLIDES {
|
||||
}
|
||||
C_PLAYLIST_SLIDES {
|
||||
}
|
||||
C_PLAYLIST_SLIDE_SCHEDULE_RULES {
|
||||
}
|
||||
D_PLAYERS {
|
||||
}
|
||||
D_SCREENS {
|
||||
}
|
||||
D_ONBOARDING_DEVICES {
|
||||
}
|
||||
D_ANNOUNCEMENTS {
|
||||
}
|
||||
D_ANNOUNCEMENT_SCREENS {
|
||||
}
|
||||
I_RSS_FEEDS {
|
||||
}
|
||||
I_RSS_FEED_ITEMS {
|
||||
}
|
||||
I_API_SOURCES {
|
||||
}
|
||||
I_WEATHER_LOCATIONS {
|
||||
}
|
||||
I_TIMETABLE_GROUPS {
|
||||
}
|
||||
I_TIMETABLE_ENTRIES {
|
||||
}
|
||||
O_APP_STATE {
|
||||
}
|
||||
O_APP_SETTINGS {
|
||||
}
|
||||
O_BACKGROUND_TASKS {
|
||||
}
|
||||
O_AUDIT_EVENTS {
|
||||
}
|
||||
|
||||
A_USERS ||--o{ A_USER_ROLES : has
|
||||
A_ROLES ||--o{ A_USER_ROLES : assigned_to
|
||||
A_ROLES ||--o{ A_ROLE_PERMISSIONS : has
|
||||
A_PERMISSIONS ||--o{ A_ROLE_PERMISSIONS : granted_to
|
||||
A_USERS ||--o{ A_SESSIONS : owns
|
||||
A_USERS ||--o{ A_ACCOUNT_TOKENS : has
|
||||
A_USERS o|--o{ O_AUDIT_EVENTS : acts
|
||||
|
||||
C_CANVAS_SIZES o|--o{ C_TEMPLATES : used_by
|
||||
C_CANVAS_SIZES o|--o{ C_PLAYLISTS : used_by
|
||||
C_TEMPLATES ||--o{ C_TEMPLATE_REGIONS : contains
|
||||
C_TEMPLATES o|--o{ C_SLIDES : used_by
|
||||
C_PLAYLISTS ||--o{ C_PLAYLIST_SLIDES : contains
|
||||
C_SLIDES ||--o{ C_PLAYLIST_SLIDES : included_in
|
||||
C_PLAYLIST_SLIDES ||--o{ C_PLAYLIST_SLIDE_SCHEDULE_RULES : has_rules
|
||||
|
||||
C_PLAYLISTS o|--o{ D_SCREENS : uses
|
||||
D_SCREENS o|--o{ D_ONBOARDING_DEVICES : binds
|
||||
D_ANNOUNCEMENTS ||--o{ D_ANNOUNCEMENT_SCREENS : targets
|
||||
D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives
|
||||
|
||||
I_RSS_FEEDS ||--o{ I_RSS_FEED_ITEMS : caches
|
||||
I_TIMETABLE_GROUPS ||--o{ I_TIMETABLE_ENTRIES : contains
|
||||
```
|
||||
@@ -0,0 +1,202 @@
|
||||
# Player WebSocket Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Player service websocket base URL: `ws://localhost:8081`
|
||||
|
||||
This document uses OpenAPI-style sections, but stays in plain markdown.
|
||||
|
||||
The `PULSE_SIGNAGE_SHARED_SECRET` setting does not change the websocket message format here. It is used to sign the player control socket URL and the server-side snapshot subscription; the message payloads themselves remain the same.
|
||||
|
||||
## Channels
|
||||
|
||||
### `GET /ws/screens/{slug}`
|
||||
Player control channel.
|
||||
|
||||
This is the bidirectional socket used by the player page. The player sends status messages to the server, and the server sends commands back to the player.
|
||||
|
||||
Access: the player page must include a valid `auth` query parameter signed with the shared secret. The browser page refreshes this token automatically while it is active.
|
||||
|
||||
### `GET /ws/screens/{slug}/events`
|
||||
Player snapshot channel.
|
||||
|
||||
This is the server-to-dashboard snapshot stream for live player connection state.
|
||||
|
||||
Access: internal-only. The web backend subscribes with signed request headers; browsers should not connect directly.
|
||||
|
||||
### `GET /ws/screens/{slug}/announcements`
|
||||
Player announcement channel.
|
||||
|
||||
This is the server-to-player push channel used to wake the player when an announcement changes. The browser player reconnects automatically and refreshes its announcement state when it receives an `announcement-refresh` message.
|
||||
|
||||
Access: the player page must include a valid `auth` query parameter signed with the shared secret.
|
||||
|
||||
## Player Control Channel
|
||||
|
||||
### Messages from player to server
|
||||
|
||||
The player sends a state snapshot message.
|
||||
|
||||
#### `state`
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "state",
|
||||
"clientId": "client-id",
|
||||
"clientName": "friendly label",
|
||||
"deviceId": "device-id",
|
||||
"userAgent": "browser ua",
|
||||
"page": "http://.../screen/demo",
|
||||
"viewport": { "width": 1920, "height": 1080 },
|
||||
"paused": false,
|
||||
"blackout": false,
|
||||
"currentSlide": null
|
||||
}
|
||||
```
|
||||
|
||||
When `currentSlide` is present, the player includes the active slide id, title, kind, and playlist signature.
|
||||
|
||||
The server recognizes `clientId`, `clientName`, `deviceId`, `userAgent`, `page`, `viewport`, `paused`, `blackout`, and `currentSlide` from state messages.
|
||||
|
||||
### Messages from server to player
|
||||
|
||||
The server sends command messages with:
|
||||
|
||||
- `type: "command"`
|
||||
- `command`
|
||||
- optional `sentAt`
|
||||
- optional `targetConnectionId`
|
||||
- optional `blackout` for blackout commands
|
||||
- optional `url` for redirect commands
|
||||
- optional `clientName` and `deviceId` for client-name updates
|
||||
|
||||
Supported commands:
|
||||
|
||||
- `blackout`
|
||||
- `next`
|
||||
- `pause`
|
||||
- `previous`
|
||||
- `refresh`
|
||||
- `reload`
|
||||
- `redirect`
|
||||
- `setclientname`
|
||||
|
||||
#### `refresh`
|
||||
Asks the player to refetch the current playlist.
|
||||
|
||||
#### `reload`
|
||||
Forces a browser page reload.
|
||||
|
||||
#### `pause`
|
||||
Toggles paused state on the player.
|
||||
|
||||
#### `blackout`
|
||||
Sets blackout state explicitly.
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "command",
|
||||
"command": "blackout",
|
||||
"blackout": false
|
||||
}
|
||||
```
|
||||
|
||||
Use `false` to restore and `true` to blackout.
|
||||
|
||||
#### `redirect`
|
||||
Requests the player page to navigate to a new location. The destination is supplied as `url` by the caller that forwarded the command.
|
||||
|
||||
#### `previous`
|
||||
Moves to the previous slide.
|
||||
|
||||
#### `next`
|
||||
Moves to the next slide.
|
||||
|
||||
#### `setclientname`
|
||||
Updates the client name associated with the player session and onboarding record.
|
||||
The command payload should include `clientName` and may include `deviceId` when the caller is updating a specific onboarding binding.
|
||||
|
||||
## Announcement Channel
|
||||
|
||||
### Messages from server to player
|
||||
|
||||
The server sends a lightweight refresh notification.
|
||||
|
||||
#### `announcement-refresh`
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "announcement-refresh",
|
||||
"slug": "demo",
|
||||
"sentAt": "2026-08-01T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
When the player receives this message, it refetches the current announcement state over HTTP.
|
||||
|
||||
## Snapshot Channel
|
||||
|
||||
### Messages from server to client
|
||||
|
||||
The server sends snapshot payloads on `/ws/screens/{slug}/events`.
|
||||
|
||||
Example payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snapshot",
|
||||
"slug": "demo",
|
||||
"connections": [],
|
||||
"sentAt": "2026-07-14T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
The snapshot channel is server-to-client only.
|
||||
|
||||
## Data Models
|
||||
|
||||
### Connection Snapshot
|
||||
|
||||
- `id`
|
||||
- `clientId`
|
||||
- `clientName`
|
||||
- `deviceId`
|
||||
- `label`
|
||||
- `userAgent`
|
||||
- `viewport`
|
||||
- `page`
|
||||
- `currentSlide`
|
||||
- `currentSlideId`
|
||||
- `currentSlideTitle`
|
||||
- `paused`
|
||||
- `blackout`
|
||||
- `clientIp`
|
||||
- `remoteAddress`
|
||||
- `connectedAt`
|
||||
- `lastSeenAt`
|
||||
|
||||
### Command Message
|
||||
|
||||
- `type`
|
||||
- `command`
|
||||
- `sentAt`
|
||||
- `targetConnectionId`
|
||||
- `blackout`
|
||||
|
||||
### Snapshot Message
|
||||
|
||||
- `type`
|
||||
- `slug`
|
||||
- `connections`
|
||||
- `sentAt`
|
||||
|
||||
## Notes
|
||||
|
||||
- This document covers only the player websocket channels.
|
||||
- Web UI/admin websocket behavior is intentionally omitted.
|
||||
Reference in New Issue
Block a user