Release v2.6.22
This commit is contained in:
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## 2.6.21 - 2026-08-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.6.21",
|
"version": "2.6.22",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
const { version: appVersion } = require('#root/package.json');
|
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.
|
// Snapshot only: keep this file aligned with the current schema state.
|
||||||
async function ensureSchema(pool, options) {
|
async function ensureSchema(pool, options) {
|
||||||
@@ -16,7 +16,8 @@ async function ensureSchema(pool, options) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentVersion = await detectSchemaVersion(pool);
|
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'));
|
console.info('[schema] previous=' + currentVersion + ' current=' + appVersion + ' update=' + (updateRequired ? 'yes' : 'no'));
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -806,8 +806,7 @@ function compareVersions(leftVersion, rightVersion) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runMigrations(pool, options) {
|
async function getPendingMigrations(pool, options) {
|
||||||
// Only run migrations that are newer than the installed schema version and not beyond the app version.
|
|
||||||
const targetVersion = String(appVersion || '0.0.0').trim();
|
const targetVersion = String(appVersion || '0.0.0').trim();
|
||||||
const currentVersion = String(options && options.currentVersion || '0.0.0').trim();
|
const currentVersion = String(options && options.currentVersion || '0.0.0').trim();
|
||||||
const legacyPlayerSchemaPresent = await columnExists(pool, 'd_players', 'device_id');
|
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';
|
effectiveCurrentVersion = compareVersions(effectiveCurrentVersion, '2.6.18') < 0 ? '2.6.17' : '2.6.17';
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const migration of VERSIONED_MIGRATIONS) {
|
return VERSIONED_MIGRATIONS.filter(function (migration) {
|
||||||
if (compareVersions(migration.version, effectiveCurrentVersion) > 0 && compareVersions(migration.version, targetVersion) <= 0) {
|
return compareVersions(migration.version, effectiveCurrentVersion) > 0 && compareVersions(migration.version, targetVersion) <= 0;
|
||||||
await migration.run(pool);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
|
getPendingMigrations: getPendingMigrations,
|
||||||
runMigrations: runMigrations,
|
runMigrations: runMigrations,
|
||||||
detectSchemaVersion: detectSchemaVersion,
|
detectSchemaVersion: detectSchemaVersion,
|
||||||
recordSchemaVersion: recordSchemaVersion,
|
recordSchemaVersion: recordSchemaVersion,
|
||||||
|
|||||||
@@ -23,7 +23,12 @@
|
|||||||
function getOnboardingDeviceId() {
|
function getOnboardingDeviceId() {
|
||||||
try {
|
try {
|
||||||
var storedDeviceId = getSessionStorageItem(onboardingDeviceIdStorageKey);
|
var storedDeviceId = getSessionStorageItem(onboardingDeviceIdStorageKey);
|
||||||
|
if (storedDeviceId) {
|
||||||
return String(storedDeviceId || '').trim();
|
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) {
|
} catch (_error) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'setclientname') {
|
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();
|
const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim();
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
return res.status(400).json({ error: 'Device ID is required' });
|
return res.status(400).json({ error: 'Device ID is required' });
|
||||||
@@ -344,7 +344,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'moveclient') {
|
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 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();
|
const targetScreenSlug = String((req.body && (req.body.targetScreenSlug || req.body.screenSlug)) || req.query.targetScreenSlug || req.query.screenSlug || '').trim();
|
||||||
|
|
||||||
|
|||||||
@@ -296,3 +296,44 @@ test('onboarding device ids stay scoped to the tab session', () => {
|
|||||||
assert.equal(sessionStorage.getItem('pulse-signage-player-device-id'), 'tab-device-id');
|
assert.equal(sessionStorage.getItem('pulse-signage-player-device-id'), 'tab-device-id');
|
||||||
assert.equal(localStorage.getItem('pulse-signage-player-device-id'), 'shared-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');
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user