Release 2.6.23
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,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,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
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user