From ed2f23bb5dc1eb96d410c38b830c373d53a16da6 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Fri, 4 Sep 2026 21:39:28 +0100 Subject: [PATCH] docs: update deployment and API references --- README.md | 3 +- docker-compose/README.md | 173 +++++++++++++++++++++++++-------------- docs/README.md | 1 + docs/api.md | 89 ++++++++++++++++---- docs/schema.md | 49 +++++++++-- 5 files changed, 229 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index d8178ab..9e4098e 100644 --- a/README.md +++ b/README.md @@ -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. -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 @@ -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. - [Database schema](docs/schema.md) - the tables and data model the app maintains. - [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. ## Explore The Docs diff --git a/docker-compose/README.md b/docker-compose/README.md index 6534684..6cd19cc 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -2,6 +2,21 @@ 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 - [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` - `PULSE_SIGNAGE_SHARED_SECRET` +- `WEB_PUBLIC_URL` +- `BRIDGE_INTERNAL_URL` - `DEFAULT_ADMIN_USERNAME` - `DEFAULT_ADMIN_NAME` - `DEFAULT_ADMIN_PASSWORD` @@ -62,11 +79,13 @@ Responsibilities: Key configuration: +- `PLAYER_PUBLIC_URL` - `PLAYER_INTERNAL_URL` - `PLAYER_IDENTIFIER` - `BRIDGE_PUBLIC_URL` in remote mode - `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` @@ -96,46 +115,106 @@ Responsibilities: Key configuration: -- `MYSQL_DATABASE` -- `MYSQL_USER` -- `MYSQL_PASSWORD` +- `DB_NAME` +- `DB_USER` +- `DB_PASSWORD` - `MYSQL_ROOT_PASSWORD` ## 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: -- `PULSE_SIGNAGE_WEB_IMAGE` - image to run for the web app and bridge services, typically `.../pulse-signage-web:latest` -- `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 -- `DB_*` - MySQL credentials and database name for the stack -- `MYSQL_ROOT_PASSWORD` - root password for the local MySQL container -- `WEB_PUBLIC_URL` - public URL of the web application -- `WEB_INTERNAL_URL` - internal URL the bridge uses to call the web app directly -- `PLAYER_IDENTIFIER` - unique local player identifier -- `PLAYER_PUBLIC_URL` - URL used by the kiosk launcher and direct player access -- `PLAYER_INTERNAL_URL` - internal URL the web app uses for local player calls -- `BRIDGE_INTERNAL_URL` - bridge URL the web app uses for player snapshot and command forwarding -- `DEFAULT_ADMIN_*` - bootstrap admin account values +| Variable | Purpose | Default | +| --- | --- | --- | +| `PULSE_SIGNAGE_WEB_IMAGE` | Image for the web app and bridge services. | `git.lzstealth.com/lzstealth/pulse-signage-web:latest` | +| `PULSE_SIGNAGE_PLAYER_IMAGE` | Image for the player services. | `git.lzstealth.com/lzstealth/pulse-signage-player:latest` | +| `PULSE_SIGNAGE_SHARED_SECRET` | Shared request-signing secret. | Blank; set this for a secured deployment. | +| `DB_HOST` | MySQL host name. | `mysql` | +| `DB_PORT` | MySQL port. | `3306` | +| `DB_NAME` | MySQL database name. | `pulse-signage` | +| `DB_USER` | MySQL user name. | `pulse-signage` | +| `DB_PASSWORD` | MySQL user password. | `signage_password` | +| `MYSQL_ROOT_PASSWORD` | Local MySQL root password. | `root_password` | +| `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: -- `PULSE_SIGNAGE_PLAYER_IMAGE` - image to run on the device, typically `.../pulse-signage-player:latest` -- `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 -- `PLAYER_PUBLIC_URL` - optional URL used by the kiosk launcher when the remote player is directly reachable -- `BRIDGE_PUBLIC_URL` - bridge URL the player connects back to -- `PLAYER_AGENT_RECONNECT_DELAY_MS` - reconnect delay for the player agent +| Variable | Purpose | Default | +| --- | --- | --- | +| `PULSE_SIGNAGE_PLAYER_IMAGE` | Image to run on the device. | `git.lzstealth.com/lzstealth/pulse-signage-player:latest` | +| `PULSE_SIGNAGE_SHARED_SECRET` | Shared request-signing secret; must match the public stack. | Blank; set it to the public stack's secret. | +| `PLAYER_IDENTIFIER` | Unique remote player identifier. | `player-remote` | +| `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. @@ -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. -## 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 Public stack ports: @@ -210,16 +261,16 @@ Each compose file creates its own named network: ## 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. - 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 `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. -2. Copy `.env.remote.example` to a device-specific `.env` file for the remote player. -3. Make sure `PULSE_SIGNAGE_SHARED_SECRET` matches everywhere. -4. Start the public stack first, then start the remote player after the bridge is reachable. +1. Follow [Public Stack Setup](#public-stack-setup) to start the public stack with Docker Compose. +2. Set the same `PULSE_SIGNAGE_SHARED_SECRET` in the public and remote environments. +3. Set `BRIDGE_PUBLIC_URL` to the externally reachable bridge URL. +4. Start the published remote player with the workflow above. 5. Verify that the player appears in Connected clients before testing screen commands. diff --git a/docs/README.md b/docs/README.md index 4fb0d23..eefdd14 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. - [Database schema](schema.md) - the tables and data model used by the app. - [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 diff --git a/docs/api.md b/docs/api.md index 1eb4cca..27ec1a4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. -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. @@ -26,23 +26,50 @@ Access: the configured player may load only its persisted paired screen. An unpa ### `GET /api/onboarding/status` 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: -- `deviceId` required +- `deviceId` optional; the player device ID is used when omitted ### `GET /api/onboarding/screens` 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` 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: -- `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` Renews the current page-auth token before it expires. @@ -54,15 +81,28 @@ Response fields: - `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. 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: -- `deviceId` required - `clientName` required - `screenSlug` required +- `pairingCode` required +- `clientId` required Response fields: @@ -76,7 +116,7 @@ Response fields: ### `GET /api/media/config` 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: @@ -85,15 +125,15 @@ Response fields: ### `PUT /api/media/{filename}` 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}` 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` 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: @@ -118,7 +158,7 @@ Access: internal-only. ### `GET /api/screens/{slug}/playlist` 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: @@ -130,10 +170,19 @@ Response fields: - `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: 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: @@ -143,9 +192,19 @@ Response fields: - `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. 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: diff --git a/docs/schema.md b/docs/schema.md index 7b38a4f..0bc7d42 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -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_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`, `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. +### `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` @@ -134,7 +156,7 @@ Tables generally use a numeric auto-increment `id` primary key. The relationship ### `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. - Foreign key: - `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_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. @@ -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. - `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 @@ -252,6 +276,12 @@ erDiagram } A_SESSIONS { } + A_ACCOUNT_TOKENS { + } + A_USER_INVITATIONS { + } + A_LOGIN_ATTEMPTS { + } C_CANVAS_SIZES { } C_PLAYLISTS { @@ -282,6 +312,8 @@ erDiagram } I_API_SOURCES { } + I_WEATHER_LOCATIONS { + } I_TIMETABLE_GROUPS { } I_TIMETABLE_ENTRIES { @@ -300,18 +332,19 @@ erDiagram A_ROLES ||--o{ A_ROLE_PERMISSIONS : has A_PERMISSIONS ||--o{ A_ROLE_PERMISSIONS : granted_to 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{ C_PLAYLISTS : used_by + 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{ C_SLIDES : used_by + 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{ D_SCREENS : uses - D_SCREENS ||--o{ D_ONBOARDING_DEVICES : binds + 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