Release 2.6.23

This commit is contained in:
2026-08-09 13:38:30 +01:00
parent 3c3864e6ac
commit 78a34e4105
11 changed files with 109 additions and 13 deletions
+1
View File
@@ -3,6 +3,7 @@
## Versioning and releases ## Versioning and releases
- Treat `package.json` as the source of truth for the application version. - Treat `package.json` as the source of truth for the application version.
- Keep `package.json`, `build/package.player.json`, and `build/package.web.json` on the same version number.
- When the app version changes, update `CHANGELOG.md` in the same change. - When the app version changes, update `CHANGELOG.md` in the same change.
- Keep database migration versions aligned with the release they actually belong to. - Keep database migration versions aligned with the release they actually belong to.
- If only part of a migration batch belongs to a newer release, split that batch into a separate migration entry instead of relabeling the earlier release. - If only part of a migration batch belongs to a newer release, split that batch into a separate migration entry instead of relabeling the earlier release.
+6
View File
@@ -2,6 +2,12 @@
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.23 - 2026-08-09
### Fixed
- API and RSS refresh jobs now notify affected player screens after a successful update, so API-backed slides pick up new data without waiting for a manual page reload.
## 2.6.22 - 2026-08-09 ## 2.6.22 - 2026-08-09
### Changed ### Changed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-player", "name": "pulse-signage-player",
"version": "2.6.22", "version": "2.6.23",
"private": false, "private": false,
"description": "Pulse Signage player application bundle", "description": "Pulse Signage player application bundle",
"main": "src/common.js", "main": "src/common.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-web", "name": "pulse-signage-web",
"version": "2.6.22", "version": "2.6.23",
"private": false, "private": false,
"description": "Pulse Signage web and bridge application bundle", "description": "Pulse Signage web and bridge application bundle",
"main": "src/common.js", "main": "src/common.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.6.22", "version": "2.6.23",
"private": false, "private": false,
"description": "Pulse Signage application with MySQL and media storage", "description": "Pulse Signage application with MySQL and media storage",
"repository": { "repository": {
+1
View File
@@ -207,6 +207,7 @@ async function start() {
mediaDir: webConfig.mediaDir, mediaDir: webConfig.mediaDir,
backgroundTaskQueue: backgroundTaskQueue, backgroundTaskQueue: backgroundTaskQueue,
webBootstrap: webBootstrap, webBootstrap: webBootstrap,
notifyPlayerScreens: notifyPlayerScreens,
loadCurrentUser: loadCurrentUser, loadCurrentUser: loadCurrentUser,
initializeBackgroundTasks: initializeBackgroundTasks, initializeBackgroundTasks: initializeBackgroundTasks,
captureSlideThumbnail: captureSlideThumbnail, captureSlideThumbnail: captureSlideThumbnail,
@@ -24,7 +24,7 @@ function registerDataSourceRefreshTask(options) {
if (!apiSource) { if (!apiSource) {
throw new Error('API source not found.'); throw new Error('API source not found.');
} }
return refreshApiSource(pool, common, apiSource, Number(payload.actorId) || null); return refreshApiSource(pool, common, apiSource, Number(payload.actorId) || null, options.notifyPlayerScreens);
} }
if (sourceType === 'rss-feed') { if (sourceType === 'rss-feed') {
@@ -32,7 +32,7 @@ function registerDataSourceRefreshTask(options) {
if (!rssFeed) { if (!rssFeed) {
throw new Error('RSS feed not found.'); throw new Error('RSS feed not found.');
} }
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, Number(payload.actorId) || null); return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, Number(payload.actorId) || null, options.notifyPlayerScreens);
} }
throw new Error('Unsupported data source refresh task.'); throw new Error('Unsupported data source refresh task.');
@@ -34,7 +34,7 @@ function registerRecurringDataSourceRefreshes(options) {
sourceName: apiSource.name sourceName: apiSource.name
}, },
run: function () { run: function () {
return refreshApiSource(pool, common, apiSource, null); return refreshApiSource(pool, common, apiSource, null, options.notifyPlayerScreens);
} }
}); });
}); });
@@ -52,7 +52,7 @@ function registerRecurringDataSourceRefreshes(options) {
sourceName: rssFeed.name sourceName: rssFeed.name
}, },
run: function () { run: function () {
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null); return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null, options.notifyPlayerScreens);
} }
}); });
}); });
@@ -115,11 +115,11 @@ function createDataSourceTaskService(options) {
} }
async function refreshApiSourceInBackground(apiSourceId, actorId) { async function refreshApiSourceInBackground(apiSourceId, actorId) {
return refreshApiSource(pool, common, apiSourceId, actorId); return refreshApiSource(pool, common, apiSourceId, actorId, options.notifyPlayerScreens);
} }
async function refreshRssFeedInBackground(rssFeedId, feedUrl, itemLimit, actorId) { async function refreshRssFeedInBackground(rssFeedId, feedUrl, itemLimit, actorId) {
return refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId); return refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId, options.notifyPlayerScreens);
} }
return { return {
@@ -33,13 +33,13 @@ function scheduleStartupDataSourceRefreshes(options) {
(apiSourcesData.apiSources || []).forEach(function (apiSource) { (apiSourcesData.apiSources || []).forEach(function (apiSource) {
startupSources.push(buildStartupSource('api-source', apiSource.id, apiSource.name, function () { startupSources.push(buildStartupSource('api-source', apiSource.id, apiSource.name, function () {
return refreshApiSource(pool, common, apiSource, null); return refreshApiSource(pool, common, apiSource, null, options.notifyPlayerScreens);
})); }));
}); });
(rssFeedsData.rssFeeds || []).forEach(function (rssFeed) { (rssFeedsData.rssFeeds || []).forEach(function (rssFeed) {
startupSources.push(buildStartupSource('rss-feed', rssFeed.id, rssFeed.name, function () { startupSources.push(buildStartupSource('rss-feed', rssFeed.id, rssFeed.name, function () {
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null); return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null, options.notifyPlayerScreens);
})); }));
}); });
+89 -2
View File
@@ -1,4 +1,75 @@
async function refreshApiSource(pool, common, apiSourceOrId, actorId) { async function getAffectedScreenSlugs(connection, common, slideMatchKey, sourceId) {
const [slideRows] = await connection.query('SELECT id, content_json FROM c_slides WHERE content_json IS NOT NULL');
const slideIds = [];
const seenSlideIds = new Set();
slideRows.forEach(function (row) {
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(row.content_json) : null;
if (!content || typeof content !== 'object') {
return;
}
const stack = [content];
while (stack.length) {
const value = stack.pop();
if (!value || typeof value !== 'object') {
continue;
}
if (Array.isArray(value)) {
value.forEach(function (item) {
stack.push(item);
});
continue;
}
if (Object.prototype.hasOwnProperty.call(value, slideMatchKey) && Number(value[slideMatchKey]) === Number(sourceId)) {
const slideId = Number(row.id);
if (Number.isFinite(slideId) && slideId > 0 && !seenSlideIds.has(slideId)) {
seenSlideIds.add(slideId);
slideIds.push(slideId);
}
break;
}
Object.keys(value).forEach(function (key) {
stack.push(value[key]);
});
}
});
if (!slideIds.length) {
return [];
}
const [screenRows] = await connection.query(
`SELECT DISTINCT s.slug
FROM d_screens s
JOIN c_playlist_slides ps ON ps.playlist_id = s.playlist_id
WHERE ps.slide_id IN (?)
AND s.slug IS NOT NULL`,
[slideIds]
);
return screenRows.map(function (row) {
return String(row.slug || '').trim();
}).filter(Boolean);
}
async function notifyAffectedScreens(connection, common, notifyPlayerScreens, slideMatchKey, sourceId) {
if (typeof notifyPlayerScreens !== 'function') {
return;
}
const slugs = await getAffectedScreenSlugs(connection, common, slideMatchKey, sourceId);
if (!slugs.length) {
return;
}
await notifyPlayerScreens(slugs, 'refresh');
}
async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlayerScreens) {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
try { try {
const apiSource = apiSourceOrId && typeof apiSourceOrId === 'object' const apiSource = apiSourceOrId && typeof apiSourceOrId === 'object'
@@ -25,6 +96,14 @@ async function refreshApiSource(pool, common, apiSourceOrId, actorId) {
[new Date(), pullError || null, responseDetails ? responseDetails.responseStatus : null, responseDetails ? responseDetails.responseContentType : null, responseDetails ? responseDetails.responseJson : null, actorId, apiSource.id] [new Date(), pullError || null, responseDetails ? responseDetails.responseStatus : null, responseDetails ? responseDetails.responseContentType : null, responseDetails ? responseDetails.responseJson : null, actorId, apiSource.id]
); );
await connection.commit(); await connection.commit();
if (!pullError) {
try {
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'source_id', apiSource.id);
} catch (notifyError) {
console.warn('[data-source-refresh] Unable to notify players after API source refresh ' + apiSource.id + ':', notifyError);
}
}
} catch (error) { } catch (error) {
try { try {
await connection.rollback(); await connection.rollback();
@@ -37,7 +116,7 @@ async function refreshApiSource(pool, common, apiSourceOrId, actorId) {
} }
} }
async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId) { async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId, notifyPlayerScreens) {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
try { try {
let updatedItems = []; let updatedItems = [];
@@ -55,6 +134,14 @@ async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actor
} }
await connection.commit(); await connection.commit();
if (!pullError) {
try {
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'feed_id', rssFeedId);
} catch (notifyError) {
console.warn('[data-source-refresh] Unable to notify players after RSS feed refresh ' + rssFeedId + ':', notifyError);
}
}
if (pullError) { if (pullError) {
console.error('[data-source-refresh] RSS feed refresh completed with an error for feed ' + rssFeedId + ': ' + pullError); console.error('[data-source-refresh] RSS feed refresh completed with an error for feed ' + rssFeedId + ': ' + pullError);
} }
+1
View File
@@ -22,6 +22,7 @@ async function initializeWebServer(options) {
pool: pool, pool: pool,
common: common, common: common,
backgroundTaskQueue: backgroundTaskQueue, backgroundTaskQueue: backgroundTaskQueue,
notifyPlayerScreens: options && options.notifyPlayerScreens ? options.notifyPlayerScreens : null,
uploadSyncService: webBootstrap.uploadSyncService, uploadSyncService: webBootstrap.uploadSyncService,
captureSlideThumbnail: captureSlideThumbnail, captureSlideThumbnail: captureSlideThumbnail,
mediaDir: mediaDir, mediaDir: mediaDir,