docker updates.
This commit is contained in:
@@ -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 }}
|
||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user