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.
|
||||
|
||||
## 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
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
+3
-2
@@ -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'));
|
||||
|
||||
|
||||
+13
-6
@@ -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,
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(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