docs: update deployment and API references

This commit is contained in:
2026-09-04 21:39:28 +01:00
parent 954e0edc3f
commit ed2f23bb5d
5 changed files with 229 additions and 86 deletions
+1 -2
View File
@@ -25,7 +25,7 @@ It gives you one place to publish playlists, slides, announcements, and live upd
Docker Compose is the recommended way to deploy Pulse Signage. It keeps the web app, player, bridge, and database together in a predictable setup. Docker Compose is the recommended way to deploy Pulse Signage. It keeps the web app, player, bridge, and database together in a predictable setup.
If you want the details, start with the [Compose guide](docker-compose/README.md). For a complete installation, follow the [public stack setup](docker-compose/README.md#public-stack-setup). For screens on separate devices, use the [remote player setup](docker-compose/README.md#remote-player-setup).
## Docs ## Docs
@@ -33,7 +33,6 @@ If you want the details, start with the [Compose guide](docker-compose/README.md
- [API reference](docs/api.md) - the player HTTP surface and onboarding endpoints. - [API reference](docs/api.md) - the player HTTP surface and onboarding endpoints.
- [Database schema](docs/schema.md) - the tables and data model the app maintains. - [Database schema](docs/schema.md) - the tables and data model the app maintains.
- [WebSocket reference](docs/websocket.md) - the live player and snapshot channels. - [WebSocket reference](docs/websocket.md) - the live player and snapshot channels.
- [Compose guide](docker-compose/README.md) - deployment options and service layout.
- [Changelog](CHANGELOG.md) - release history and notable changes. - [Changelog](CHANGELOG.md) - release history and notable changes.
## Explore The Docs ## Explore The Docs
+112 -61
View File
@@ -2,6 +2,21 @@
This folder contains the Docker Compose definitions for Pulse Signage, including the public stack and the remote player stack. This folder contains the Docker Compose definitions for Pulse Signage, including the public stack and the remote player stack.
## Contents
- [Files](#files)
- [Stack Overview](#stack-overview)
- [Services](#services)
- [Environment Files](#environment-files)
- [Public Stack Setup](#public-stack-setup)
- [Remote Player Setup](#remote-player-setup)
- [Shared Secret](#shared-secret)
- [Ports](#ports)
- [Volumes](#volumes)
- [Networks](#networks)
- [Notes](#notes)
- [Deployment Checklist](#deployment-checklist)
## Files ## Files
- [docker-compose.yml](docker-compose.yml) - full public stack with web, player, player bridge, and MySQL. - [docker-compose.yml](docker-compose.yml) - full public stack with web, player, player bridge, and MySQL.
@@ -45,6 +60,8 @@ Key configuration:
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` - `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
- `PULSE_SIGNAGE_SHARED_SECRET` - `PULSE_SIGNAGE_SHARED_SECRET`
- `WEB_PUBLIC_URL`
- `BRIDGE_INTERNAL_URL`
- `DEFAULT_ADMIN_USERNAME` - `DEFAULT_ADMIN_USERNAME`
- `DEFAULT_ADMIN_NAME` - `DEFAULT_ADMIN_NAME`
- `DEFAULT_ADMIN_PASSWORD` - `DEFAULT_ADMIN_PASSWORD`
@@ -62,11 +79,13 @@ Responsibilities:
Key configuration: Key configuration:
- `PLAYER_PUBLIC_URL`
- `PLAYER_INTERNAL_URL` - `PLAYER_INTERNAL_URL`
- `PLAYER_IDENTIFIER` - `PLAYER_IDENTIFIER`
- `BRIDGE_PUBLIC_URL` in remote mode - `BRIDGE_PUBLIC_URL` in remote mode
- `PULSE_SIGNAGE_SHARED_SECRET` - `PULSE_SIGNAGE_SHARED_SECRET`
- database settings in local mode - `WEB_PUBLIC_URL`
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` in local mode
### `player-bridge` ### `player-bridge`
@@ -96,46 +115,106 @@ Responsibilities:
Key configuration: Key configuration:
- `MYSQL_DATABASE` - `DB_NAME`
- `MYSQL_USER` - `DB_USER`
- `MYSQL_PASSWORD` - `DB_PASSWORD`
- `MYSQL_ROOT_PASSWORD` - `MYSQL_ROOT_PASSWORD`
## Environment Files ## Environment Files
### `.env.example` | File | Used by | How it is loaded |
| --- | --- | --- |
| `.env.example` | Public stack | Copy to `.env`; Compose loads it automatically, or pass it with `--env-file`. |
| `.env.remote.example` | Published remote player | Copy to `.env.remote`; pass it with `--env-file .env.remote`. |
Use this file as a starting point for the public compose stack. The example files are templates. Copy the appropriate file, review its defaults, and replace secrets or placeholder URLs before deploying.
### Public stack: `.env.example`
Use this file as a starting point for the public Compose stack.
Important values: Important values:
- `PULSE_SIGNAGE_WEB_IMAGE` - image to run for the web app and bridge services, typically `.../pulse-signage-web:latest` | Variable | Purpose | Default |
- `PULSE_SIGNAGE_PLAYER_IMAGE` - image to run for the player services, typically `.../pulse-signage-player:latest` | --- | --- | --- |
- `PULSE_SIGNAGE_SHARED_SECRET` - long random secret shared by the web, player, and bridge services for authenticated requests | `PULSE_SIGNAGE_WEB_IMAGE` | Image for the web app and bridge services. | `git.lzstealth.com/lzstealth/pulse-signage-web:latest` |
- `DB_*` - MySQL credentials and database name for the stack | `PULSE_SIGNAGE_PLAYER_IMAGE` | Image for the player services. | `git.lzstealth.com/lzstealth/pulse-signage-player:latest` |
- `MYSQL_ROOT_PASSWORD` - root password for the local MySQL container | `PULSE_SIGNAGE_SHARED_SECRET` | Shared request-signing secret. | Blank; set this for a secured deployment. |
- `WEB_PUBLIC_URL` - public URL of the web application | `DB_HOST` | MySQL host name. | `mysql` |
- `WEB_INTERNAL_URL` - internal URL the bridge uses to call the web app directly | `DB_PORT` | MySQL port. | `3306` |
- `PLAYER_IDENTIFIER` - unique local player identifier | `DB_NAME` | MySQL database name. | `pulse-signage` |
- `PLAYER_PUBLIC_URL` - URL used by the kiosk launcher and direct player access | `DB_USER` | MySQL user name. | `pulse-signage` |
- `PLAYER_INTERNAL_URL` - internal URL the web app uses for local player calls | `DB_PASSWORD` | MySQL user password. | `signage_password` |
- `BRIDGE_INTERNAL_URL` - bridge URL the web app uses for player snapshot and command forwarding | `MYSQL_ROOT_PASSWORD` | Local MySQL root password. | `root_password` |
- `DEFAULT_ADMIN_*` - bootstrap admin account values | `WEB_PUBLIC_URL` | Public URL of the web application. | `http://localhost:8080` |
| `WEB_INTERNAL_URL` | Internal URL the bridge uses to call the web app. | `http://web:8080` |
| `PLAYER_IDENTIFIER` | Unique local player identifier. | `player-local` |
| `PLAYER_PUBLIC_URL` | URL used by the kiosk launcher and direct player access. | `http://localhost:8081` |
| `PLAYER_INTERNAL_URL` | Internal URL used for local player calls. | `http://player:8081` |
| `BRIDGE_INTERNAL_URL` | Bridge URL used for snapshots and command forwarding. | `http://player-bridge:8090` |
| `DEFAULT_ADMIN_USERNAME` | Bootstrap admin username. | `admin` |
| `DEFAULT_ADMIN_NAME` | Bootstrap admin display name. | `Admin` |
| `DEFAULT_ADMIN_PASSWORD` | Bootstrap admin password. | `password123!` |
### `.env.remote.example` ### Remote player: `.env.remote.example`
Use this file on a remote player device. Use this file as the starting point for a remote player device. The production remote Compose file reads values from Compose's environment, so pass the copied file explicitly with `--env-file`.
Important values: Important values:
- `PULSE_SIGNAGE_PLAYER_IMAGE` - image to run on the device, typically `.../pulse-signage-player:latest` | Variable | Purpose | Default |
- `PULSE_SIGNAGE_SHARED_SECRET` - must match the public stack and should be the same long random value used everywhere in the deployment | --- | --- | --- |
- `PLAYER_IDENTIFIER` - unique remote player identifier | `PULSE_SIGNAGE_PLAYER_IMAGE` | Image to run on the device. | `git.lzstealth.com/lzstealth/pulse-signage-player:latest` |
- `PLAYER_PUBLIC_URL` - optional URL used by the kiosk launcher when the remote player is directly reachable | `PULSE_SIGNAGE_SHARED_SECRET` | Shared request-signing secret; must match the public stack. | Blank; set it to the public stack's secret. |
- `BRIDGE_PUBLIC_URL` - bridge URL the player connects back to | `PLAYER_IDENTIFIER` | Unique remote player identifier. | `player-remote` |
- `PLAYER_AGENT_RECONNECT_DELAY_MS` - reconnect delay for the player agent | `PLAYER_PUBLIC_URL` | Optional URL for direct player access. | `http://remote-player.example.com:8081` |
| `BRIDGE_PUBLIC_URL` | Bridge URL the player connects back to. | `http://player-bridge.example.com:8090`; replace this placeholder. |
| `PLAYER_AGENT_RECONNECT_DELAY_MS` | Delay before reconnecting to the bridge. | `5000` |
### `PULSE_SIGNAGE_SHARED_SECRET` ## Public Stack Setup
Install Docker Engine with Docker Compose, then run the public stack from this directory:
```sh
cp .env.example .env
docker compose -f docker-compose.yml up -d
```
The command pulls the published images, creates the network and volumes, and starts the web app, local player, player bridge, and MySQL services. Check the installation with:
```sh
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs -f web
```
## Remote Player Setup
A remote deployment has two parts:
- the public stack runs the web app, database, and player bridge
- each remote device runs only the player and connects back to the bridge
The remote player does not need database credentials. Set `BRIDGE_PUBLIC_URL` to the externally reachable bridge URL, including its port when required. It must point to the bridge service, not the web dashboard URL. The bridge must be reachable from the device and allow both HTTP requests and the player websocket connection at `/ws/players`.
### Published remote player
On the remote device:
```sh
cp .env.remote.example .env.remote
docker compose --env-file .env.remote -f docker-compose.remote.yml up -d
```
The published remote stack exposes the player on host port `8081`. Check its connection and startup output with:
```sh
docker compose --env-file .env.remote -f docker-compose.remote.yml ps
docker compose --env-file .env.remote -f docker-compose.remote.yml logs -f player
```
Start the public stack and confirm that its bridge is reachable before starting the remote player. Once the player connects, it should appear in the dashboard's Connected clients view. If it does not, verify the bridge URL, shared secret, firewall or reverse-proxy websocket support, and the player logs.
## Shared Secret
This secret is the shared signing key for requests between the services. Use a single value for every service that needs to talk to the same stack, including the web app, player, bridge, and any remote player that connects back to that bridge. This secret is the shared signing key for requests between the services. Use a single value for every service that needs to talk to the same stack, including the web app, player, bridge, and any remote player that connects back to that bridge.
@@ -149,34 +228,6 @@ If you want a quick local value, generate one with a password manager or a comma
Leave it blank only if you intentionally want to run without request signing in a throwaway local setup. Leave it blank only if you intentionally want to run without request signing in a throwaway local setup.
## Main Configuration Variables
| Variable | Used By | Purpose |
| --- | --- | --- |
| `PULSE_SIGNAGE_WEB_IMAGE` | web, bridge | Docker image to run for the web app and bridge services. |
| `PULSE_SIGNAGE_PLAYER_IMAGE` | player, remote player | Docker image to run for the player services. |
| `PULSE_SIGNAGE_SHARED_SECRET` | web, player, bridge, remote player | Shared secret for authenticated requests between services. |
| `DB_HOST` | web, player, bridge | Database host name. |
| `DB_PORT` | web, player, bridge | Database port. |
| `DB_NAME` | web, player, bridge, mysql | Database name. |
| `DB_USER` | web, player, bridge, mysql | Database user. |
| `DB_PASSWORD` | web, player, bridge, mysql | Database password. |
| `MYSQL_ROOT_PASSWORD` | mysql | Root password for the local MySQL container. |
| `MYSQL_DATABASE` | mysql | Database name used by the local MySQL container. |
| `MYSQL_USER` | mysql | Database user used by the local MySQL container. |
| `MYSQL_PASSWORD` | mysql | Database password used by the local MySQL container. |
| `WEB_PUBLIC_URL` | web, player-bridge | Public URL of the web application. |
| `WEB_INTERNAL_URL` | player-bridge | Internal web URL used by the bridge to call the dashboard app directly. |
| `PLAYER_IDENTIFIER` | player | Stable player identifier. |
| `PLAYER_PUBLIC_URL` | player, remote player | URL used by the kiosk launcher and direct player access; optional for bridge-only remote players. |
| `PLAYER_INTERNAL_URL` | web, player | Internal player URL used by the dashboard and player runtime. |
| `BRIDGE_INTERNAL_URL` | web | Bridge URL used by the web app for player snapshot and command forwarding. |
| `DEFAULT_ADMIN_USERNAME` | web | Bootstrap admin username. |
| `DEFAULT_ADMIN_NAME` | web | Bootstrap admin display name. |
| `DEFAULT_ADMIN_PASSWORD` | web | Bootstrap admin password. |
| `BRIDGE_PUBLIC_URL` | remote player | URL of the bridge service. |
| `PLAYER_AGENT_RECONNECT_DELAY_MS` | remote player | Delay before reconnecting to the bridge. |
## Ports ## Ports
Public stack ports: Public stack ports:
@@ -210,16 +261,16 @@ Each compose file creates its own named network:
## Notes ## Notes
- The public stack expects the app services and MySQL to share the same `PULSE_SIGNAGE_SHARED_SECRET`. - The public stack expects the web, player, and bridge services to share the same `PULSE_SIGNAGE_SHARED_SECRET`.
- A remote player must use the same `PULSE_SIGNAGE_SHARED_SECRET` as the bridge it connects to. - A remote player must use the same `PULSE_SIGNAGE_SHARED_SECRET` as the bridge it connects to.
- The bridge service is the dashboard-facing command path for connected remote players. - The bridge service is the dashboard-facing command path for connected remote players.
- The remote player should point `BRIDGE_PUBLIC_URL` at the bridge, not at the public web endpoint. - The remote player should point `BRIDGE_PUBLIC_URL` at the bridge, not at the public web endpoint.
- The `PULSE_SIGNAGE_WEB_IMAGE` and `PULSE_SIGNAGE_PLAYER_IMAGE` tags default to the published `pulse-signage-web` and `pulse-signage-player` repositories with `latest` tags, but they can be overridden for custom releases. - The `PULSE_SIGNAGE_WEB_IMAGE` and `PULSE_SIGNAGE_PLAYER_IMAGE` tags default to the published `pulse-signage-web` and `pulse-signage-player` repositories with `latest` tags, but they can be overridden for custom releases.
## Recommended Setup ## Deployment Checklist
1. Copy `.env.example` to a local `.env` file for the public stack. 1. Follow [Public Stack Setup](#public-stack-setup) to start the public stack with Docker Compose.
2. Copy `.env.remote.example` to a device-specific `.env` file for the remote player. 2. Set the same `PULSE_SIGNAGE_SHARED_SECRET` in the public and remote environments.
3. Make sure `PULSE_SIGNAGE_SHARED_SECRET` matches everywhere. 3. Set `BRIDGE_PUBLIC_URL` to the externally reachable bridge URL.
4. Start the public stack first, then start the remote player after the bridge is reachable. 4. Start the published remote player with the workflow above.
5. Verify that the player appears in Connected clients before testing screen commands. 5. Verify that the player appears in Connected clients before testing screen commands.
+1
View File
@@ -7,6 +7,7 @@ This folder contains the technical reference material for Pulse Signage.
- [API reference](api.md) - the player HTTP surface and onboarding endpoints. - [API reference](api.md) - the player HTTP surface and onboarding endpoints.
- [Database schema](schema.md) - the tables and data model used by the app. - [Database schema](schema.md) - the tables and data model used by the app.
- [WebSocket reference](websocket.md) - the live player and snapshot channels. - [WebSocket reference](websocket.md) - the live player and snapshot channels.
- [Compose guide](../docker-compose/README.md) - Docker Compose deployment and service configuration.
## How To Read It ## How To Read It
+74 -15
View File
@@ -6,7 +6,7 @@ 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. This document covers the player HTTP surface only. The admin dashboard exposes its own routes for screen commands and onboarding management.
Access note: most player endpoints are unauthenticated because they are meant to run inside a trusted deployment network. Anything that mutates state or writes files should be treated as internal-only unless you add your own auth layer in front of it. Pairing uses a short-lived random PIN displayed by the kiosk; the PIN is accepted only through the authenticated Web UI pairing flow. 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. 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.
@@ -26,23 +26,50 @@ Access: the configured player may load only its persisted paired screen. An unpa
### `GET /api/onboarding/status` ### `GET /api/onboarding/status`
Returns the persisted onboarding status for a device. Returns the persisted onboarding status for a device.
Access: public within the trusted player deployment. Access: requires a page-auth token with the `onboarding` or `player` scope when shared-secret authentication is enabled.
Query fields: Query fields:
- `deviceId` required - `deviceId` optional; the player device ID is used when omitted
### `GET /api/onboarding/screens` ### `GET /api/onboarding/screens`
Returns the list of screens available for onboarding. Returns the list of screens available for onboarding.
Access: public within the trusted player deployment. Access: requires a page-auth token with the `onboarding` scope when shared-secret authentication is enabled.
### `GET /api/onboarding/qr` ### `GET /api/onboarding/qr`
Returns an SVG QR code that points to the onboarding form. Returns an SVG QR code that points to the onboarding form.
Access: public within the trusted player deployment. Access: public on the player service; the bridge version requires a signed server request when shared-secret authentication is enabled.
Query fields: Query fields:
- `deviceId` required - `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` ### `POST /api/auth/page`
Renews the current page-auth token before it expires. Renews the current page-auth token before it expires.
@@ -54,15 +81,28 @@ Response fields:
- `issuedAt` - `issuedAt`
- `expiresAt` - `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` ### `POST /api/onboarding`
Binds a device to a screen and client name. Binds a device to a screen and client name.
Access: internal-only. Browser submissions must go through the authenticated Web UI pairing page. The Web UI resolves the short-lived kiosk PIN to a device ID before forwarding the signed request. Protect this endpoint if the player service is reachable outside your trusted network. 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: Accepted request fields:
- `deviceId` required
- `clientName` required - `clientName` required
- `screenSlug` required - `screenSlug` required
- `pairingCode` required
- `clientId` required
Response fields: Response fields:
@@ -76,7 +116,7 @@ Response fields:
### `GET /api/media/config` ### `GET /api/media/config`
Returns the upload directory configured for the player service. Returns the upload directory configured for the player service.
Access: internal-only. Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
Response fields: Response fields:
@@ -85,15 +125,15 @@ Response fields:
### `PUT /api/media/{filename}` ### `PUT /api/media/{filename}`
Writes an uploaded file into the player upload directory. Writes an uploaded file into the player upload directory.
Access: internal-only and write-protected behind your deployment boundary. Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
### `DELETE /api/media/{filename}` ### `DELETE /api/media/{filename}`
Deletes a file from the player upload directory. Deletes a file from the player upload directory.
Access: internal-only and write-protected behind your deployment boundary. Access: internal-only and requires signed request headers when shared-secret authentication is enabled.
### `GET /api/rtmp/session` ### `GET /api/rtmp/session`
Creates or reuses an RTMP-to-HLS session for a source URL. Creates or reuses an RTMP-to-HLS session for a source URL.
Access: internal-only. Access: requires a page-auth token with the `player` scope when shared-secret authentication is enabled.
Query fields: Query fields:
@@ -118,7 +158,7 @@ Access: internal-only.
### `GET /api/screens/{slug}/playlist` ### `GET /api/screens/{slug}/playlist`
Returns the current playlist payload for a screen. Returns the current playlist payload for a screen.
Access: public within the trusted player deployment. 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: Response fields:
@@ -130,10 +170,19 @@ Response fields:
- `timetableGroups` - `timetableGroups`
- `revision` - `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}/connections`
### `GET /api/screens/{slug}/clients` ### `GET /api/screens/{slug}/clients`
Returns the live player connection snapshot for a screen. Returns the live player connection snapshot for a screen.
Access: public within the trusted player deployment, but it exposes live connection state. Access: internal-only and requires signed request headers when shared-secret authentication is enabled; it exposes live connection state.
Response fields: Response fields:
@@ -143,9 +192,19 @@ Response fields:
- `connections` - `connections`
- `degraded` - `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` ### `POST /api/screens/{slug}/commands`
Sends a command to the player connections for a screen. Sends a command to the player connections for a screen.
Access: internal-only. The admin dashboard should remain the protected control surface for commands. 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: Accepted request fields:
+41 -8
View File
@@ -13,12 +13,34 @@ Tables generally use a numeric auto-increment `id` primary key. The relationship
- `a_role_permissions` - many-to-many mapping between roles and permissions. - `a_role_permissions` - many-to-many mapping between roles and permissions.
- `a_user_roles` - many-to-many mapping between users and roles. - `a_user_roles` - many-to-many mapping between users and roles.
- `a_sessions` - persisted admin session tokens. - `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` ### `a_users`
- `id`, `name`, `username`, `password_hash`, `password_salt`, `password_iterations`, `must_change_password`, `account_locked`, `created_at`, `created_by`, `modified_at`, `modified_by` - `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. - `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` ### `a_roles`
- `id`, `role_key`, `name`, `description`, `created_at`, `created_by`, `modified_at`, `modified_by` - `id`, `role_key`, `name`, `description`, `created_at`, `created_by`, `modified_at`, `modified_by`
@@ -134,7 +156,7 @@ Tables generally use a numeric auto-increment `id` primary key. The relationship
### `d_onboarding_devices` ### `d_onboarding_devices`
- `id`, `device_id`, `client_name`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by` - `id`, `device_id`, `client_name`, `screen_id`, `created_at`, `created_by`, `modified_at`, `modified_by`, `last_seen_at`
- `device_id` is unique. - `device_id` is unique.
- Foreign key: - Foreign key:
- `screen_id` -> `d_screens.id` with `ON DELETE SET NULL` - `screen_id` -> `d_screens.id` with `ON DELETE SET NULL`
@@ -166,6 +188,7 @@ Tables generally use a numeric auto-increment `id` primary key. The relationship
- `i_rss_feeds` - RSS feed definitions and refresh cadence. - `i_rss_feeds` - RSS feed definitions and refresh cadence.
- `i_rss_feed_items` - cached RSS feed items. - `i_rss_feed_items` - cached RSS feed items.
- `i_api_sources` - API source definitions and last response snapshot. - `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_groups` - grouped timetable definitions used by the timetable region.
- `i_timetable_entries` - dated entries that belong to a timetable group. - `i_timetable_entries` - dated entries that belong to a timetable group.
@@ -237,6 +260,7 @@ Tables generally use a numeric auto-increment `id` primary key. The relationship
- The schema is initialized with `CREATE TABLE IF NOT EXISTS`, so new installs can start from an empty database. - 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/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. - `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 ```mermaid
erDiagram erDiagram
@@ -252,6 +276,12 @@ erDiagram
} }
A_SESSIONS { A_SESSIONS {
} }
A_ACCOUNT_TOKENS {
}
A_USER_INVITATIONS {
}
A_LOGIN_ATTEMPTS {
}
C_CANVAS_SIZES { C_CANVAS_SIZES {
} }
C_PLAYLISTS { C_PLAYLISTS {
@@ -282,6 +312,8 @@ erDiagram
} }
I_API_SOURCES { I_API_SOURCES {
} }
I_WEATHER_LOCATIONS {
}
I_TIMETABLE_GROUPS { I_TIMETABLE_GROUPS {
} }
I_TIMETABLE_ENTRIES { I_TIMETABLE_ENTRIES {
@@ -300,18 +332,19 @@ erDiagram
A_ROLES ||--o{ A_ROLE_PERMISSIONS : has A_ROLES ||--o{ A_ROLE_PERMISSIONS : has
A_PERMISSIONS ||--o{ A_ROLE_PERMISSIONS : granted_to A_PERMISSIONS ||--o{ A_ROLE_PERMISSIONS : granted_to
A_USERS ||--o{ A_SESSIONS : owns A_USERS ||--o{ A_SESSIONS : owns
A_USERS ||--o{ O_AUDIT_EVENTS : acts A_USERS ||--o{ A_ACCOUNT_TOKENS : has
A_USERS o|--o{ O_AUDIT_EVENTS : acts
C_CANVAS_SIZES ||--o{ C_TEMPLATES : used_by C_CANVAS_SIZES o|--o{ C_TEMPLATES : used_by
C_CANVAS_SIZES ||--o{ C_PLAYLISTS : used_by C_CANVAS_SIZES o|--o{ C_PLAYLISTS : used_by
C_TEMPLATES ||--o{ C_TEMPLATE_REGIONS : contains C_TEMPLATES ||--o{ C_TEMPLATE_REGIONS : contains
C_TEMPLATES ||--o{ C_SLIDES : used_by C_TEMPLATES o|--o{ C_SLIDES : used_by
C_PLAYLISTS ||--o{ C_PLAYLIST_SLIDES : contains C_PLAYLISTS ||--o{ C_PLAYLIST_SLIDES : contains
C_SLIDES ||--o{ C_PLAYLIST_SLIDES : included_in C_SLIDES ||--o{ C_PLAYLIST_SLIDES : included_in
C_PLAYLIST_SLIDES ||--o{ C_PLAYLIST_SLIDE_SCHEDULE_RULES : has_rules C_PLAYLIST_SLIDES ||--o{ C_PLAYLIST_SLIDE_SCHEDULE_RULES : has_rules
C_PLAYLISTS ||--o{ D_SCREENS : uses C_PLAYLISTS o|--o{ D_SCREENS : uses
D_SCREENS ||--o{ D_ONBOARDING_DEVICES : binds D_SCREENS o|--o{ D_ONBOARDING_DEVICES : binds
D_ANNOUNCEMENTS ||--o{ D_ANNOUNCEMENT_SCREENS : targets D_ANNOUNCEMENTS ||--o{ D_ANNOUNCEMENT_SCREENS : targets
D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives