docker updates.

This commit is contained in:
2026-07-14 13:56:54 +01:00
parent ff23474a54
commit c6ec8f6ad9
6 changed files with 156 additions and 61 deletions
+46
View File
@@ -0,0 +1,46 @@
name: Publish Docker Image
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: git.lzstealth.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: git.lzstealth.com/LZStealth/pulse-signage
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=sha
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+13 -1
View File
@@ -68,9 +68,19 @@ The app reads its settings from environment variables.
The recommended way to run the project is with Docker Compose. The recommended way to run the project is with Docker Compose.
```bash ```bash
docker-compose up --build npm run docker:up
``` ```
The main Compose file now uses the published Docker image and still reads values from the repository `.env` file. `DB_HOST` comes from that file for the app containers, so you can point the web UI and player at an external database host or change it to `mysql` if you want to use the bundled MySQL service.
If you only want to test the image build from the current checkout without using Compose, run:
```bash
npm run docker:build
```
To publish the Docker image to the Gitea container registry, push to `main` or create a `v*` tag. The workflow in [.gitea/workflows/docker-publish.yml](.gitea/workflows/docker-publish.yml) builds `git.lzstealth.com/LZStealth/pulse-signage` and pushes `latest`, `sha`, and tagged releases.
This starts: This starts:
- the web admin app on port `3000` - the web admin app on port `3000`
@@ -79,6 +89,8 @@ This starts:
The web app and player app share the same uploaded media volume, so files uploaded in the admin UI are available to the player. The web app and player app share the same uploaded media volume, so files uploaded in the admin UI are available to the player.
If you want to stop the stack later, run `npm run docker:down`. To follow logs, run `npm run docker:logs`.
## Important Notes ## Important Notes
- The web app must be able to reach the player through `PLAYER_INTERNAL_BASE_URL`. - The web app must be able to reach the player through `PLAYER_INTERNAL_BASE_URL`.
+14 -9
View File
@@ -1,12 +1,10 @@
version: "3.9"
services: services:
webui: webui:
build: image: ${PULSE_SIGNAGE_IMAGE:-git.lzstealth.com/LZStealth/pulse-signage:latest}
context: .
dockerfile: Dockerfile
container_name: signage-webui container_name: signage-webui
restart: unless-stopped restart: unless-stopped
env_file:
- .env
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
@@ -20,6 +18,12 @@ services:
PLAYER_INTERNAL_BASE_URL: ${PLAYER_INTERNAL_BASE_URL:-http://player:3001} PLAYER_INTERNAL_BASE_URL: ${PLAYER_INTERNAL_BASE_URL:-http://player:3001}
PLAYER_PUBLIC_BASE_URL: ${PLAYER_PUBLIC_BASE_URL:-http://localhost:3001} PLAYER_PUBLIC_BASE_URL: ${PLAYER_PUBLIC_BASE_URL:-http://localhost:3001}
UPLOAD_DIR: ${UPLOAD_DIR:-/app/uploads} UPLOAD_DIR: ${UPLOAD_DIR:-/app/uploads}
SESSION_MAX_AGE_DAYS: ${SESSION_MAX_AGE_DAYS:-14}
DASHBOARD_REFRESH_INTERVAL_MS: ${DASHBOARD_REFRESH_INTERVAL_MS:-2000}
DEFAULT_ADMIN_USERNAME: ${DEFAULT_ADMIN_USERNAME:-admin}
DEFAULT_ADMIN_NAME: ${DEFAULT_ADMIN_NAME:-Admin}
DEFAULT_ADMIN_PASSWORD: ${DEFAULT_ADMIN_PASSWORD:-admin}
PASSWORD_HASH_ITERATIONS: ${PASSWORD_HASH_ITERATIONS:-310000}
volumes: volumes:
- uploads:/app/uploads - uploads:/app/uploads
command: ["npm", "run", "start:webui"] command: ["npm", "run", "start:webui"]
@@ -30,11 +34,11 @@ services:
- signage-net - signage-net
player: player:
build: image: ${PULSE_SIGNAGE_IMAGE:-git.lzstealth.com/LZStealth/pulse-signage:latest}
context: .
dockerfile: Dockerfile
container_name: signage-player container_name: signage-player
restart: unless-stopped restart: unless-stopped
env_file:
- .env
ports: ports:
- "3001:3001" - "3001:3001"
environment: environment:
@@ -59,6 +63,8 @@ services:
image: mysql:8.4 image: mysql:8.4
container_name: signage-mysql container_name: signage-mysql
restart: unless-stopped restart: unless-stopped
env_file:
- .env
ports: ports:
- "3306:3306" - "3306:3306"
environment: environment:
@@ -67,7 +73,6 @@ services:
MYSQL_PASSWORD: ${DB_PASSWORD:-signage_password} MYSQL_PASSWORD: ${DB_PASSWORD:-signage_password}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password}
command: command:
- --default-authentication-plugin=mysql_native_password
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
volumes: volumes:
+5 -1
View File
@@ -9,7 +9,11 @@
"start:webui": "node -r dotenv/config src/webui.js", "start:webui": "node -r dotenv/config src/webui.js",
"start:player": "node -r dotenv/config src/player.js", "start:player": "node -r dotenv/config src/player.js",
"dev:webui": "nodemon -r dotenv/config src/webui.js", "dev:webui": "nodemon -r dotenv/config src/webui.js",
"dev:player": "nodemon -r dotenv/config src/player.js" "dev:player": "nodemon -r dotenv/config src/player.js",
"docker:build": "docker build -t pulse-signage:test .",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:logs": "docker-compose logs -f --tail=100"
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
+64 -47
View File
@@ -14,6 +14,23 @@ function createPool() {
}); });
} }
async function addColumnIfMissing(pool, tableName, columnName, columnDefinition) {
const [rows] = await pool.query(
`SELECT COUNT(*) AS column_count
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?`,
[tableName, columnName]
);
if (rows.length && Number(rows[0].column_count) > 0) {
return;
}
await pool.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` ${columnDefinition}`);
}
async function ensureSchema(pool) { async function ensureSchema(pool) {
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS canvas_sizes ( CREATE TABLE IF NOT EXISTS canvas_sizes (
@@ -26,9 +43,9 @@ async function ensureSchema(pool) {
UNIQUE KEY uq_canvas_sizes_dimensions (width, height) UNIQUE KEY uq_canvas_sizes_dimensions (width, height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'canvas_sizes', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'canvas_sizes', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'canvas_sizes', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS playlists (
@@ -39,10 +56,10 @@ async function ensureSchema(pool) {
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS fade_between_slides TINYINT(1) NOT NULL DEFAULT 0 AFTER name'); await addColumnIfMissing(pool, 'playlists', 'fade_between_slides', 'TINYINT(1) NOT NULL DEFAULT 0');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'playlists', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'playlists', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'playlists', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS slide_templates ( CREATE TABLE IF NOT EXISTS slide_templates (
@@ -55,11 +72,11 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS canvas_size_id INT NULL AFTER name'); await addColumnIfMissing(pool, 'slide_templates', 'canvas_size_id', 'INT NULL');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS background_image_path VARCHAR(512) NULL'); await addColumnIfMissing(pool, 'slide_templates', 'background_image_path', 'VARCHAR(512) NULL');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'slide_templates', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'slide_templates', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'slide_templates', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
INSERT IGNORE INTO canvas_sizes (name, width, height) VALUES INSERT IGNORE INTO canvas_sizes (name, width, height) VALUES
@@ -103,10 +120,10 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS font_family VARCHAR(100) NULL AFTER label'); await addColumnIfMissing(pool, 'slide_template_regions', 'font_family', 'VARCHAR(100) NULL');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'slide_template_regions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'slide_template_regions', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'slide_template_regions', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS slides ( CREATE TABLE IF NOT EXISTS slides (
@@ -122,14 +139,14 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS body TEXT NULL AFTER title'); await addColumnIfMissing(pool, 'slides', 'body', 'TEXT NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS template_id INT NULL AFTER body'); await addColumnIfMissing(pool, 'slides', 'template_id', 'INT NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS content_json JSON NULL AFTER template_id'); await addColumnIfMissing(pool, 'slides', 'content_json', 'JSON NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS media_path VARCHAR(512) NULL AFTER content_json'); await addColumnIfMissing(pool, 'slides', 'media_path', 'VARCHAR(512) NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS media_type VARCHAR(100) NULL AFTER media_path'); await addColumnIfMissing(pool, 'slides', 'media_type', 'VARCHAR(100) NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'slides', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'slides', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS playlist_slides ( CREATE TABLE IF NOT EXISTS playlist_slides (
@@ -151,16 +168,16 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS duration_seconds INT NOT NULL DEFAULT 10 AFTER position'); await addColumnIfMissing(pool, 'playlist_slides', 'duration_seconds', 'INT NOT NULL DEFAULT 10');
await pool.query("ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_mode VARCHAR(20) NOT NULL DEFAULT 'always' AFTER duration_seconds"); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_mode', "VARCHAR(20) NOT NULL DEFAULT 'always'");
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_start_datetime DATETIME NULL AFTER schedule_mode'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_datetime', 'DATETIME NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_end_datetime DATETIME NULL AFTER schedule_start_datetime'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_datetime', 'DATETIME NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_start_time TIME NULL AFTER schedule_end_datetime'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_time', 'TIME NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_end_time TIME NULL AFTER schedule_start_time'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_time', 'TIME NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_days_json JSON NULL AFTER schedule_end_time'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_days_json', 'JSON NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'playlist_slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'playlist_slides', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'playlist_slides', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS screens ( CREATE TABLE IF NOT EXISTS screens (
@@ -174,10 +191,10 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS playlist_id INT NULL AFTER slug'); await addColumnIfMissing(pool, 'screens', 'playlist_id', 'INT NULL');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'screens', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'screens', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'screens', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -191,13 +208,13 @@ async function ensureSchema(pool) {
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS name VARCHAR(255) NULL AFTER id'); await addColumnIfMissing(pool, 'users', 'name', 'VARCHAR(255) NULL');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash CHAR(64) NOT NULL AFTER username'); await addColumnIfMissing(pool, 'users', 'password_hash', 'CHAR(64) NOT NULL');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_salt VARCHAR(64) NOT NULL AFTER password_hash'); await addColumnIfMissing(pool, 'users', 'password_salt', 'VARCHAR(64) NOT NULL');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_iterations INT NOT NULL AFTER password_salt'); await addColumnIfMissing(pool, 'users', 'password_iterations', 'INT NOT NULL');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at'); await addColumnIfMissing(pool, 'users', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'users', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at'); await addColumnIfMissing(pool, 'users', 'modified_by', 'INT NULL');
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS auth_sessions ( CREATE TABLE IF NOT EXISTS auth_sessions (
@@ -209,8 +226,8 @@ async function ensureSchema(pool) {
CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
await pool.query('ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at'); await addColumnIfMissing(pool, 'auth_sessions', 'created_by', 'INT NULL');
await pool.query('ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER last_used_at'); await addColumnIfMissing(pool, 'auth_sessions', 'modified_by', 'INT NULL');
const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users'); const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users');
if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) { if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) {
+14 -3
View File
@@ -1,6 +1,17 @@
<script> <script>
const slug = {{SLUG_JSON}}; function parseTemplateJson(templateValue, fallbackValue) {
const initialData = {{INITIAL_DATA_JSON}}; if (typeof templateValue !== 'string' || /^\s*\{\{.*\}\}\s*$/.test(templateValue)) {
return fallbackValue;
}
try {
return JSON.parse(templateValue);
} catch (_error) {
return fallbackValue;
}
}
const slug = parseTemplateJson('{{SLUG_JSON}}', '');
const initialData = parseTemplateJson('{{INITIAL_DATA_JSON}}', {});
const app = document.getElementById('app'); const app = document.getElementById('app');
let slides = Array.isArray(initialData && initialData.slides) ? initialData.slides.map(normalizeSlide) : []; let slides = Array.isArray(initialData && initialData.slides) ? initialData.slides.map(normalizeSlide) : [];
let currentPlaylistSignature = ''; let currentPlaylistSignature = '';
@@ -31,7 +42,7 @@
let slideExpiresAt = null; let slideExpiresAt = null;
const slideFadeDurationMs = 560; const slideFadeDurationMs = 560;
const commandSocketPath = '/ws/screens/' + encodeURIComponent(slug); const commandSocketPath = '/ws/screens/' + encodeURIComponent(slug);
const commandClientStorageKey = 'digital-signage-player-client-id:' + slug; const commandClientStorageKey = 'pulse-signage-player-client-id:' + slug;
// Escape text before inserting it into HTML. // Escape text before inserting it into HTML.
function escapeHtml(value) { function escapeHtml(value) {