From 6d8bf0f5f08e5a75c890c8e7aa18f1920edf4bb6 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sun, 9 Aug 2026 13:06:02 +0100 Subject: [PATCH] Release v2.6.22 --- CHANGELOG.md | 11 ++++ package.json | 2 +- src/db/index.js | 5 +- src/db/migrations.js | 19 ++++-- src/player/player-client-name.script.html | 7 ++- src/web/routes/admin/client-commands.js | 4 +- test/player-preload-helpers.test.js | 41 ++++++++++++ test/schema-update-log.test.js | 77 +++++++++++++++++++++++ 8 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 test/schema-update-log.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 071fe7c..5fc3db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## 2.6.22 - 2026-08-09 + +### Changed + +- Startup now logs the previously detected schema version, the current app version, and whether pending migrations exist. + +### Fixed + +- Direct-to-URL player sessions now generate and persist a stable onboarding device id in session storage, so connected screens can still be moved and renamed independently without going through the onboarding flow first. +- The connected-clients move action now tolerates rows that only have a live connection id, which keeps move operations working for screens that skipped onboarding. + ## 2.6.21 - 2026-08-09 ### Changed diff --git a/package.json b/package.json index 5f7aca8..37bfc9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.6.21", + "version": "2.6.22", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/db/index.js b/src/db/index.js index d480070..f25acf4 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -1,5 +1,5 @@ const { version: appVersion } = require('#root/package.json'); -const { compareVersions, detectSchemaVersion, recordSchemaVersion, runMigrations } = require('./migrations'); +const { compareVersions, detectSchemaVersion, getPendingMigrations, recordSchemaVersion, runMigrations } = require('./migrations'); // Snapshot only: keep this file aligned with the current schema state. async function ensureSchema(pool, options) { @@ -16,7 +16,8 @@ async function ensureSchema(pool, options) { try { const currentVersion = await detectSchemaVersion(pool); - const updateRequired = compareVersions(currentVersion, appVersion) < 0; + const pendingMigrations = await getPendingMigrations(pool, { currentVersion: currentVersion }); + const updateRequired = pendingMigrations.length > 0; console.info('[schema] previous=' + currentVersion + ' current=' + appVersion + ' update=' + (updateRequired ? 'yes' : 'no')); diff --git a/src/db/migrations.js b/src/db/migrations.js index 1581656..0f405dd 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -806,8 +806,7 @@ function compareVersions(leftVersion, rightVersion) { return 0; } -async function runMigrations(pool, options) { - // Only run migrations that are newer than the installed schema version and not beyond the app version. +async function getPendingMigrations(pool, options) { const targetVersion = String(appVersion || '0.0.0').trim(); const currentVersion = String(options && options.currentVersion || '0.0.0').trim(); const legacyPlayerSchemaPresent = await columnExists(pool, 'd_players', 'device_id'); @@ -828,15 +827,23 @@ async function runMigrations(pool, options) { effectiveCurrentVersion = compareVersions(effectiveCurrentVersion, '2.6.18') < 0 ? '2.6.17' : '2.6.17'; } - for (const migration of VERSIONED_MIGRATIONS) { - if (compareVersions(migration.version, effectiveCurrentVersion) > 0 && compareVersions(migration.version, targetVersion) <= 0) { - await migration.run(pool); - } + return VERSIONED_MIGRATIONS.filter(function (migration) { + return compareVersions(migration.version, effectiveCurrentVersion) > 0 && compareVersions(migration.version, targetVersion) <= 0; + }); +} + +async function runMigrations(pool, options) { + // Only run migrations that are newer than the installed schema version and not beyond the app version. + const pendingMigrations = await getPendingMigrations(pool, options); + + for (const migration of pendingMigrations) { + await migration.run(pool); } } module.exports = { appVersion: appVersion, + getPendingMigrations: getPendingMigrations, runMigrations: runMigrations, detectSchemaVersion: detectSchemaVersion, recordSchemaVersion: recordSchemaVersion, diff --git a/src/player/player-client-name.script.html b/src/player/player-client-name.script.html index 8792cd6..65a8b8e 100644 --- a/src/player/player-client-name.script.html +++ b/src/player/player-client-name.script.html @@ -23,7 +23,12 @@ function getOnboardingDeviceId() { try { var storedDeviceId = getSessionStorageItem(onboardingDeviceIdStorageKey); - return String(storedDeviceId || '').trim(); + if (storedDeviceId) { + return String(storedDeviceId || '').trim(); + } + var nextDeviceId = window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : 'device-' + Date.now() + '-' + Math.random().toString(16).slice(2); + setSessionStorageItem(onboardingDeviceIdStorageKey, nextDeviceId); + return String(nextDeviceId || '').trim(); } catch (_error) { return ''; } diff --git a/src/web/routes/admin/client-commands.js b/src/web/routes/admin/client-commands.js index f7161d9..ad27880 100644 --- a/src/web/routes/admin/client-commands.js +++ b/src/web/routes/admin/client-commands.js @@ -227,7 +227,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) { } if (command === 'setclientname') { - const deviceId = String((req.body && (req.body.deviceId || req.body.clientId)) || req.query.deviceId || req.query.clientId || '').trim(); + const deviceId = String((req.body && (req.body.deviceId || req.body.clientId || req.body.connectionId)) || req.query.deviceId || req.query.clientId || req.query.connectionId || '').trim(); const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim(); if (!deviceId) { return res.status(400).json({ error: 'Device ID is required' }); @@ -344,7 +344,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) { } if (command === 'moveclient') { - const deviceId = String((req.body && (req.body.deviceId || req.body.clientId)) || req.query.deviceId || req.query.clientId || '').trim(); + const deviceId = String((req.body && (req.body.deviceId || req.body.clientId || req.body.connectionId)) || req.query.deviceId || req.query.clientId || req.query.connectionId || '').trim(); const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim(); const targetScreenSlug = String((req.body && (req.body.targetScreenSlug || req.body.screenSlug)) || req.query.targetScreenSlug || req.query.screenSlug || '').trim(); diff --git a/test/player-preload-helpers.test.js b/test/player-preload-helpers.test.js index 0889a3e..28d66b4 100644 --- a/test/player-preload-helpers.test.js +++ b/test/player-preload-helpers.test.js @@ -295,4 +295,45 @@ test('onboarding device ids stay scoped to the tab session', () => { assert.equal(sessionStorage.getItem('pulse-signage-player-device-id'), 'tab-device-id'); assert.equal(localStorage.getItem('pulse-signage-player-device-id'), 'shared-device-id'); +}); + +test('player onboarding device ids are generated when missing', () => { + const sessionStorage = (() => { + const values = new Map(); + return { + getItem(key) { + return values.has(key) ? values.get(key) : null; + }, + setItem(key, value) { + values.set(String(key), String(value)); + } + }; + })(); + + const sandbox = { + window: null, + Date, + Array, + Number, + String, + Boolean, + Object, + Math, + console, + sessionStorage, + localStorage: sessionStorage, + crypto: { + randomUUID() { + return 'generated-device-id'; + } + }, + WebSocket: { OPEN: 1 }, + sendCommandState() {} + }; + sandbox.window = sandbox; + + loadHtmlScript(path.join(__dirname, '..', 'src', 'player', 'player-client-name.script.html'), sandbox); + + assert.equal(sandbox.getOnboardingDeviceId(), 'generated-device-id'); + assert.equal(sessionStorage.getItem('pulse-signage-player-device-id'), 'generated-device-id'); }); \ No newline at end of file diff --git a/test/schema-update-log.test.js b/test/schema-update-log.test.js new file mode 100644 index 0000000..02e1b56 --- /dev/null +++ b/test/schema-update-log.test.js @@ -0,0 +1,77 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +require('../src/common'); + +const { getPendingMigrations } = require('../src/db/migrations'); + +function createPool(responses) { + const queries = []; + return { + queries, + async query(sql, params) { + queries.push([sql, params]); + const text = String(sql); + for (const response of responses) { + if (response.match(text, params)) { + return response.result; + } + } + return [[]]; + } + }; +} + +test('pending migrations are empty when the schema already matches the app version', async () => { + const pool = createPool([ + { + match(sql) { + return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); + }, + result: [[{ column_count: 0 }]] + }, + { + match(_sql, params) { + return Array.isArray(params) && params[0] === 'i_schedule_groups'; + }, + result: [[{ table_count: 0 }]] + }, + { + match(_sql, params) { + return Array.isArray(params) && params[0] === 'i_schedule_entries'; + }, + result: [[{ table_count: 0 }]] + } + ]); + + const pendingMigrations = await getPendingMigrations(pool, { currentVersion: '2.6.21' }); + + assert.equal(pendingMigrations.length, 0); +}); + +test('pending migrations are reported when an older schema still needs scripts', async () => { + const pool = createPool([ + { + match(sql) { + return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); + }, + result: [[{ column_count: 0 }]] + }, + { + match(_sql, params) { + return Array.isArray(params) && params[0] === 'i_schedule_groups'; + }, + result: [[{ table_count: 1 }]] + }, + { + match(_sql, params) { + return Array.isArray(params) && params[0] === 'i_schedule_entries'; + }, + result: [[{ table_count: 1 }]] + } + ]); + + const pendingMigrations = await getPendingMigrations(pool, { currentVersion: '2.6.17' }); + + assert.ok(pendingMigrations.length > 0); +}); \ No newline at end of file