diff --git a/.gitignore b/.gitignore index b18d49c..6b09581 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ media/ !src/web/lib/media/ !src/web/lib/media/** docker-compose.dev.yml +/dev-demo-seed.js .vscode/ .env npm-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b2a994..2518733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## 2.5.2 - 2026-08-03 + +### Changed + +- Player page auth now writes a cookie for websocket reuse, and player and announcement sockets accept that cookie so they no longer depend on query-string tokens. +- RSS and API region placeholder panels now use shared field-aware chip rendering, with updated spacing and defaults across the slide editor. +- RTMP regions now default audio to enabled in the editor, and the slide rich-text editor no longer allows anchor tags. + +### Fixed + +- Player websocket auth now falls back to the `pulse_page_auth` cookie when the auth query parameter is unavailable. + ## 2.5.1 - 2026-08-03 ### Added diff --git a/package.json b/package.json index ef8d50b..e7e52f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.5.1", + "version": "2.5.2", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/db/bootstrap.js b/src/db/bootstrap.js index 08b1ccc..428bd8d 100644 --- a/src/db/bootstrap.js +++ b/src/db/bootstrap.js @@ -5,7 +5,7 @@ async function bootstrapDatabase(pool) { const [canvasSizeCountRows] = await pool.query('SELECT COUNT(*) AS canvas_size_count FROM c_canvas_sizes'); if (!canvasSizeCountRows.length || Number(canvasSizeCountRows[0].canvas_size_count) === 0) { await pool.query(` - INSERT INTO c_canvas_sizes (name, width, height) VALUES + INSERT IGNORE INTO c_canvas_sizes (name, width, height) VALUES ('Full HD', 1920, 1080), ('HD', 1280, 720), ('4K UHD', 3840, 2160), diff --git a/src/db/migrations.js b/src/db/migrations.js index 8f21bac..0acf094 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -55,16 +55,7 @@ const VERSIONED_MIGRATIONS = [ } // Recreate the screen-to-player foreign key after the column exists and legacy data is copied over. - const [screenPlayerFkRows] = await pool.query( - `SELECT COUNT(*) AS fk_count - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'd_screens' - AND CONSTRAINT_NAME = 'fk_screens_player'` - ); - if (Number(screenPlayerFkRows && screenPlayerFkRows[0] && screenPlayerFkRows[0].fk_count) === 0) { - await pool.query('ALTER TABLE d_screens ADD CONSTRAINT fk_screens_player FOREIGN KEY (player_id) REFERENCES d_players(device_id) ON DELETE RESTRICT'); - } + await ensureForeignKey(pool, 'd_screens', 'fk_screens_player', 'player_id', 'd_players', 'device_id', 'RESTRICT'); if (await columnExists(pool, 'c_template_regions', 'font_family')) { await pool.query('ALTER TABLE c_template_regions DROP COLUMN font_family'); @@ -243,24 +234,59 @@ async function ensureColumn(pool, tableName, columnName, columnDefinition, after } const afterClause = afterColumn ? ' AFTER ' + afterColumn : ''; - await pool.query('ALTER TABLE ' + tableName + ' ADD COLUMN ' + columnName + ' ' + columnDefinition + afterClause); + try { + await pool.query('ALTER TABLE ' + tableName + ' ADD COLUMN ' + columnName + ' ' + columnDefinition + afterClause); + } catch (error) { + if (!error || (error.code !== 'ER_DUP_FIELDNAME' && error.errno !== 1060)) { + throw error; + } + } } async function ensureForeignKey(pool, tableName, constraintName, columnName, referencedTable, referencedColumn, onDeleteAction) { - const [rows] = await pool.query( + const [existingFkRows] = await pool.query( `SELECT COUNT(*) AS fk_count FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? + AND COLUMN_NAME = ? + AND REFERENCED_TABLE_NAME IS NOT NULL`, + [tableName, columnName] + ); + + if (Number(existingFkRows && existingFkRows[0] && existingFkRows[0].fk_count) > 0) { + return; + } + + const [rows] = await pool.query( + `SELECT COUNT(*) AS fk_count + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() AND CONSTRAINT_NAME = ?`, - [tableName, constraintName] + [constraintName] ); if (Number(rows && rows[0] && rows[0].fk_count) > 0) { return; } - await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + constraintName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction); + const fallbackNames = [ + constraintName, + tableName + '_' + columnName + '_fk', + tableName + '_' + columnName + '_fk_2', + tableName + '_' + columnName + '_fk_3' + ]; + + for (const candidateName of fallbackNames) { + try { + await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + candidateName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction); + return; + } catch (error) { + if (!error || (error.code !== 'ER_FK_DUP_NAME' && error.errno !== 1826)) { + throw error; + } + } + } } async function dropColumnIfExists(pool, tableName, columnName) { diff --git a/src/player/announcement-templates/lower-third.template.html b/src/player/announcement-templates/lower-third.template.html index febcaa3..a47e5e9 100644 --- a/src/player/announcement-templates/lower-third.template.html +++ b/src/player/announcement-templates/lower-third.template.html @@ -101,6 +101,7 @@ gap: 2.5rem; min-width: 100%; width: max-content; + backface-visibility: hidden; will-change: transform; animation: lower-third-scroll var(--announcement-scroll-duration, 20s) linear infinite; } @@ -144,10 +145,10 @@ @keyframes lower-third-scroll { from { - transform: translateX(0); + transform: translate3d(0, 0, 0); } to { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } diff --git a/src/player/announcement-templates/top-banner.template.html b/src/player/announcement-templates/top-banner.template.html index 4b56a92..dbe6447 100644 --- a/src/player/announcement-templates/top-banner.template.html +++ b/src/player/announcement-templates/top-banner.template.html @@ -103,6 +103,7 @@ gap: 2.5rem; min-width: 100%; width: max-content; + backface-visibility: hidden; will-change: transform; animation: top-banner-scroll var(--announcement-scroll-duration, 20s) linear infinite; } @@ -146,10 +147,10 @@ @keyframes top-banner-scroll { from { - transform: translateX(0); + transform: translate3d(0, 0, 0); } to { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } diff --git a/src/player/player-page-announcements.js b/src/player/player-page-announcements.js index e2d5b01..3d065ff 100644 --- a/src/player/player-page-announcements.js +++ b/src/player/player-page-announcements.js @@ -215,12 +215,7 @@ return; } - var socketUrl = new URL(announcementSocketPath, window.location.origin); - if (window.__pulsePageAuthToken) { - socketUrl.searchParams.set('auth', window.__pulsePageAuthToken); - } - - var socket = new WebSocket(socketUrl.toString()); + var socket = new WebSocket(new URL(announcementSocketPath, window.location.origin).toString()); announcementSocket = socket; socket.onopen = function () { diff --git a/src/player/public/css/player.css b/src/player/public/css/player.css index bef4abb..68c4034 100644 --- a/src/player/public/css/player.css +++ b/src/player/public/css/player.css @@ -214,6 +214,8 @@ body.thumbnail-preview .player-offline-banner { height: 100%; opacity: 0; transition: opacity 560ms ease; + will-change: opacity; + backface-visibility: hidden; } .slide-shell.is-visible { @@ -243,7 +245,8 @@ body.screen-blackout #app { height: var(--player-canvas-height, 100vh); max-width: 100vw; max-height: 100vh; - transform: translate(-50%, -50%); + transform: translate3d(-50%, -50%, 0); + backface-visibility: hidden; z-index: 9999; pointer-events: none; display: flex; diff --git a/src/player/public/js/player-page-commands.js b/src/player/public/js/player-page-commands.js index d6cdbff..ca6001a 100644 --- a/src/player/public/js/player-page-commands.js +++ b/src/player/public/js/player-page-commands.js @@ -526,12 +526,7 @@ function connectCommandSocket() { if (commandSocket && (commandSocket.readyState === WebSocket.OPEN || commandSocket.readyState === WebSocket.CONNECTING)) { return; } - var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - var socketUrl = new URL(commandSocketPath, window.location.origin); - if (window.__pulsePageAuthToken) { - socketUrl.searchParams.set('auth', window.__pulsePageAuthToken); - } - var socket = new WebSocket(socketUrl.toString()); + var socket = new WebSocket(new URL(commandSocketPath, window.location.origin).toString()); commandSocket = socket; socket.onopen = function () { diff --git a/src/player/render.js b/src/player/render.js index 80477e6..f15807b 100644 --- a/src/player/render.js +++ b/src/player/render.js @@ -132,7 +132,7 @@ function renderPlayerPage(slug, initialData) { bodyClass: bodyClass, body: '