diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 387e36a..c3faed0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,6 +3,7 @@ ## Versioning and releases - 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. - 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc3db5..45f5eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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 ### Changed diff --git a/build/package.player.json b/build/package.player.json index d7e2e84..d685ba3 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.6.22", + "version": "2.6.23", "private": false, "description": "Pulse Signage player application bundle", "main": "src/common.js", diff --git a/build/package.web.json b/build/package.web.json index 49a6bb5..96542ed 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.6.22", + "version": "2.6.23", "private": false, "description": "Pulse Signage web and bridge application bundle", "main": "src/common.js", diff --git a/package.json b/package.json index 37bfc9c..38406de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.6.22", + "version": "2.6.23", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/web.js b/src/web.js index 98cc103..599123b 100644 --- a/src/web.js +++ b/src/web.js @@ -207,6 +207,7 @@ async function start() { mediaDir: webConfig.mediaDir, backgroundTaskQueue: backgroundTaskQueue, webBootstrap: webBootstrap, + notifyPlayerScreens: notifyPlayerScreens, loadCurrentUser: loadCurrentUser, initializeBackgroundTasks: initializeBackgroundTasks, captureSlideThumbnail: captureSlideThumbnail, diff --git a/src/web/lib/background-tasks/tasks-adhoc/data-source-refresh.js b/src/web/lib/background-tasks/tasks-adhoc/data-source-refresh.js index 6fba4d7..f38c207 100644 --- a/src/web/lib/background-tasks/tasks-adhoc/data-source-refresh.js +++ b/src/web/lib/background-tasks/tasks-adhoc/data-source-refresh.js @@ -24,7 +24,7 @@ function registerDataSourceRefreshTask(options) { if (!apiSource) { 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') { @@ -32,7 +32,7 @@ function registerDataSourceRefreshTask(options) { if (!rssFeed) { 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.'); diff --git a/src/web/lib/background-tasks/tasks-scheduled/data-source-refresh.js b/src/web/lib/background-tasks/tasks-scheduled/data-source-refresh.js index 372f659..47237d9 100644 --- a/src/web/lib/background-tasks/tasks-scheduled/data-source-refresh.js +++ b/src/web/lib/background-tasks/tasks-scheduled/data-source-refresh.js @@ -34,7 +34,7 @@ function registerRecurringDataSourceRefreshes(options) { sourceName: apiSource.name }, 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 }, 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) { - return refreshApiSource(pool, common, apiSourceId, actorId); + return refreshApiSource(pool, common, apiSourceId, actorId, options.notifyPlayerScreens); } 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 { diff --git a/src/web/lib/background-tasks/tasks-startup/data-source-refresh.js b/src/web/lib/background-tasks/tasks-startup/data-source-refresh.js index 3bcc82d..b205298 100644 --- a/src/web/lib/background-tasks/tasks-startup/data-source-refresh.js +++ b/src/web/lib/background-tasks/tasks-startup/data-source-refresh.js @@ -33,13 +33,13 @@ function scheduleStartupDataSourceRefreshes(options) { (apiSourcesData.apiSources || []).forEach(function (apiSource) { 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) { 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); })); }); diff --git a/src/web/lib/data-source-refresh.js b/src/web/lib/data-source-refresh.js index ad8ad22..7556d99 100644 --- a/src/web/lib/data-source-refresh.js +++ b/src/web/lib/data-source-refresh.js @@ -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(); try { 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] ); 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) { try { 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(); try { let updatedItems = []; @@ -55,6 +134,14 @@ async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actor } 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) { console.error('[data-source-refresh] RSS feed refresh completed with an error for feed ' + rssFeedId + ': ' + pullError); } diff --git a/src/web/lib/web-startup.js b/src/web/lib/web-startup.js index c5b6e7f..2e5d0ae 100644 --- a/src/web/lib/web-startup.js +++ b/src/web/lib/web-startup.js @@ -22,6 +22,7 @@ async function initializeWebServer(options) { pool: pool, common: common, backgroundTaskQueue: backgroundTaskQueue, + notifyPlayerScreens: options && options.notifyPlayerScreens ? options.notifyPlayerScreens : null, uploadSyncService: webBootstrap.uploadSyncService, captureSlideThumbnail: captureSlideThumbnail, mediaDir: mediaDir,