Release 2.22.0: normalize playlist schedules and unify player sync
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m21s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-10-27 22:54:38 +00:00
1 parent 7748129307
commit b71958cb38
39 files changed
+1174 -314

No files matched your search

+117 -1
View File
@@ -93,7 +93,7 @@ test('pending migrations are empty when the schema already matches the app versi
test('2.20.1 release follows the consolidated 2.20.0 migration', async () => {
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.19.1' });
assert.deepEqual(pending.map(migration => migration.version), ['2.20.0', '2.20.1']);
assert.deepEqual(pending.filter(migration => migration.version.startsWith('2.20.')).map(migration => migration.version), ['2.20.0', '2.20.1']);
});
test('2.20.1 removes retired security setting rows', async () => {
@@ -627,4 +627,120 @@ test('2.19.0 indexed-reference migration backfills slide and template defaults',
assert.ok(queries.every(([sql]) => !sql.includes('idx_content_reference_owner')));
assert.ok(insertedReferences.some(row => row[0] === 'slide' && row[1] === 2 && row[3] === 'media' && row[4] === '/media/uploads/hero.png'));
assert.ok(insertedReferences.some(row => row[0] === 'template' && row[1] === 4 && row[2] === 'headline' && row[3] === 'font-family' && row[4] === 'acme display'));
});
function createPlaylistScheduleMigrationPool(groupRows, sourceRuleCount) {
const queries = [];
const legacyGroupColumns = new Set([
'group_key', 'group_name', 'group_pick_count', 'group_shuffle',
'group_mode', 'group_repeat_count', 'group_schedule_rules_json'
]);
let hasGroupId = false;
let hasLegacyScheduleTable = true;
let migratedSlideRuleCount = 0;
const groupInsertParams = [];
const groupRuleInsertParams = [];
const memberGroupUpdates = [];
return {
queries,
groupInsertParams,
groupRuleInsertParams,
memberGroupUpdates,
async query(sql, params) {
const text = String(sql);
queries.push([text, params]);
if (text.includes('FROM information_schema.COLUMNS')) {
const columnName = params[1];
const exists = columnName === 'group_id' ? hasGroupId : legacyGroupColumns.has(columnName);
return [[{ column_count: exists ? 1 : 0 }]];
}
if (text.includes('FROM information_schema.TABLES')) {
return [[{ table_count: params[0] === 'c_playlist_slide_schedule_rules' && hasLegacyScheduleTable ? 1 : 0 }]];
}
if (text.includes('FROM information_schema.KEY_COLUMN_USAGE')) {
return [[{ fk_count: 0 }]];
}
if (text.startsWith('ALTER TABLE c_playlist_slides ADD COLUMN group_id')) {
hasGroupId = true;
}
if (text.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN')) {
text.split('DROP COLUMN ').slice(1).forEach(part => legacyGroupColumns.delete(part.split(/[ ,]/)[0]));
}
if (text.includes('SELECT id, playlist_id, group_key, group_name')) {
return [structuredClone(groupRows)];
}
if (text.startsWith('INSERT INTO c_playlist_groups')) {
groupInsertParams.push(params);
return [{ insertId: 77 }];
}
if (text.startsWith('INSERT INTO c_playlist_schedule_rules') && text.includes('VALUES')) {
groupRuleInsertParams.push(params);
}
if (text.startsWith('UPDATE c_playlist_slides SET group_id')) {
memberGroupUpdates.push(params);
}
if (text.startsWith('SELECT COUNT(*) AS rule_count FROM c_playlist_slide_schedule_rules')) {
return [[{ rule_count: sourceRuleCount }]];
}
if (text.startsWith('DELETE FROM c_playlist_schedule_rules WHERE playlist_slide_id')) {
migratedSlideRuleCount = 0;
}
if (text.includes('INSERT INTO c_playlist_schedule_rules') && text.includes('SELECT playlist_slide_id')) {
migratedSlideRuleCount = sourceRuleCount;
}
if (text.startsWith('SELECT COUNT(*) AS rule_count FROM c_playlist_schedule_rules WHERE playlist_slide_id')) {
return [[{ rule_count: migratedSlideRuleCount }]];
}
if (text === 'DROP TABLE c_playlist_slide_schedule_rules') {
hasLegacyScheduleTable = false;
}
return [[]];
}
};
}
function buildLegacyPlaylistGroupRows(groupRulesJson, secondPickCount) {
return [
{ id: 501, playlist_id: 8, group_key: 'events', group_name: 'Events', group_pick_count: 1, group_shuffle: 1, group_mode: 'select', group_repeat_count: 1, group_schedule_rules_json: groupRulesJson, created_at: '2026-10-01 10:00:00', created_by: 4, modified_at: '2026-10-02 10:00:00', modified_by: 5 },
{ id: 502, playlist_id: 8, group_key: 'events', group_name: 'Events', group_pick_count: secondPickCount || 1, group_shuffle: 1, group_mode: 'select', group_repeat_count: 1, group_schedule_rules_json: groupRulesJson, created_at: '2026-10-01 10:00:00', created_by: 4, modified_at: '2026-10-02 10:00:00', modified_by: 5 }
];
}
test('2.22.0 playlist schedule migration consolidates shared groups and copies both rule owners', async () => {
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.21.6' });
const migration = pending.find(item => item.version === '2.22.0');
assert.ok(migration);
const groupRules = JSON.stringify([{ start_datetime: '2026-10-01T10:00', days: [2] }]);
const pool = createPlaylistScheduleMigrationPool(buildLegacyPlaylistGroupRows(groupRules), 2);
await migration.run(pool);
assert.equal(pool.groupInsertParams.length, 1);
assert.deepEqual(pool.groupInsertParams[0].slice(0, 7), [8, 'events', 'Events', 1, 1, 'select', 1]);
assert.equal(pool.memberGroupUpdates.length, 2);
assert.deepEqual(pool.memberGroupUpdates, [[77, 501], [77, 502]]);
assert.deepEqual(pool.groupRuleInsertParams, [[77, 0, '2026-10-01T10:00', null, null, null, '[2]', 5, 5]]);
assert.ok(pool.queries.some(([sql]) => sql.includes('INSERT INTO c_playlist_schedule_rules') && sql.includes('SELECT playlist_slide_id')));
assert.ok(pool.queries.some(([sql]) => sql === 'DROP TABLE c_playlist_slide_schedule_rules'));
const legacyGroupColumnDrops = pool.queries.filter(([sql]) => sql.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN'));
assert.equal(legacyGroupColumnDrops.length, 1);
assert.match(legacyGroupColumnDrops[0][0], /DROP COLUMN group_key, DROP COLUMN group_name, DROP COLUMN group_pick_count, DROP COLUMN group_shuffle, DROP COLUMN group_mode, DROP COLUMN group_repeat_count, DROP COLUMN group_schedule_rules_json/);
const schema = require('node:fs').readFileSync(require.resolve('../src/db/index.js'), 'utf8');
assert.match(schema, /CREATE TABLE IF NOT EXISTS c_playlist_groups/);
assert.match(schema, /CREATE TABLE IF NOT EXISTS c_playlist_schedule_rules/);
assert.doesNotMatch(schema, /group_schedule_rules_json/);
});
test('2.22.0 playlist schedule migration aborts before dropping inconsistent legacy group data', async () => {
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.21.6' });
const migration = pending.find(item => item.version === '2.22.0');
assert.ok(migration);
const pool = createPlaylistScheduleMigrationPool(buildLegacyPlaylistGroupRows('[]', 2), 0);
await assert.rejects(migration.run(pool), /inconsistent settings across members/);
assert.equal(pool.groupInsertParams.length, 0);
assert.equal(pool.queries.some(([sql]) => sql === 'DROP TABLE c_playlist_slide_schedule_rules'), false);
assert.equal(pool.queries.some(([sql]) => sql.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN')), false);
});