diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c2ef3..dc4cfe0 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.26 - 2026-08-10 + +### Changed + +- API sources and RSS feeds now accept hours as an update interval unit, and the list and background task scheduling paths now format and convert that unit correctly. + ## 2.6.25 - 2026-08-10 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index bf2612f..87e08b3 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.6.25", + "version": "2.6.26", "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 0c7f88c..ea35f29 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.6.25", + "version": "2.6.26", "private": false, "description": "Pulse Signage web and bridge application bundle", "main": "src/common.js", diff --git a/package.json b/package.json index 33a6e24..242e7cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.6.25", + "version": "2.6.26", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/data/api-sources.js b/src/data/api-sources.js index c3ac3ef..5873cab 100644 --- a/src/data/api-sources.js +++ b/src/data/api-sources.js @@ -11,7 +11,10 @@ const ITEMS_PATH_MAX_LENGTH = 255; function normalizeUpdateIntervalUnit(value) { const unit = String(value || '').trim().toLowerCase(); - return unit === 'seconds' ? 'seconds' : 'minutes'; + if (unit === 'seconds' || unit === 'minutes' || unit === 'hours') { + return unit; + } + return 'minutes'; } function normalizeAuthMethod(value) { diff --git a/src/data/rss-feeds.js b/src/data/rss-feeds.js index 490056b..c1ea303 100644 --- a/src/data/rss-feeds.js +++ b/src/data/rss-feeds.js @@ -9,7 +9,10 @@ const URL_MAX_LENGTH = 1024; function normalizeUpdateIntervalUnit(value) { const unit = String(value || '').trim().toLowerCase(); - return unit === 'seconds' ? 'seconds' : 'minutes'; + if (unit === 'seconds' || unit === 'minutes' || unit === 'hours') { + return unit; + } + return 'minutes'; } async function fetchRssFeedsData(pool) { diff --git a/src/web/lib/background-tasks/queue.js b/src/web/lib/background-tasks/queue.js index 851722a..4d9397b 100644 --- a/src/web/lib/background-tasks/queue.js +++ b/src/web/lib/background-tasks/queue.js @@ -19,6 +19,9 @@ function normalizeIntervalMs(value, unit) { if (normalizedUnit === 'seconds') { return numericValue * 1000; } + if (normalizedUnit === 'hours') { + return numericValue * 60 * 60 * 1000; + } return numericValue * 60 * 1000; } diff --git a/src/web/routes/data-sources/api-sources/list.js b/src/web/routes/data-sources/api-sources/list.js index 18e7be9..46052e2 100644 --- a/src/web/routes/data-sources/api-sources/list.js +++ b/src/web/routes/data-sources/api-sources/list.js @@ -13,10 +13,13 @@ function toIsoTimestamp(value) { function formatIntervalLabel(interval, unit) { const value = Math.max(1, Number(interval) || 0); - const normalizedUnit = String(unit || 'minutes').trim().toLowerCase() === 'seconds' ? 'seconds' : 'minutes'; + const normalizedUnit = String(unit || 'minutes').trim().toLowerCase(); if (normalizedUnit === 'seconds') { return value === 1 ? 'Every second' : `Every ${value} seconds`; } + if (normalizedUnit === 'hours') { + return value === 1 ? 'Every hour' : `Every ${value} hours`; + } return value === 1 ? 'Every minute' : `Every ${value} minutes`; } diff --git a/src/web/routes/data-sources/api-sources/routes.js b/src/web/routes/data-sources/api-sources/routes.js index ae1ae8c..7909698 100644 --- a/src/web/routes/data-sources/api-sources/routes.js +++ b/src/web/routes/data-sources/api-sources/routes.js @@ -105,7 +105,9 @@ module.exports = function registerApiSourceRoutes(app, deps) { itemsPath: apiSource.items_path || '', intervalLabel: apiSource.update_interval_unit === 'seconds' ? (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every second' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} seconds`) - : (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every minute' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} minutes`), + : apiSource.update_interval_unit === 'hours' + ? (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every hour' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} hours`) + : (Math.max(1, Number(apiSource.update_interval_value) || 0) === 1 ? 'Every minute' : `Every ${Math.max(1, Number(apiSource.update_interval_value) || 0)} minutes`), lastPullLabel: apiSource.last_pulled_at ? formatDashboardDate(apiSource.last_pulled_at) : 'Never', lastPulledAtValue: apiSource.last_pulled_at ? new Date(apiSource.last_pulled_at).toISOString() : '', inUse: usageIds.has(Number(apiSource.id)) diff --git a/src/web/routes/data-sources/rss-feeds/list.js b/src/web/routes/data-sources/rss-feeds/list.js index 7d95780..3326bb9 100644 --- a/src/web/routes/data-sources/rss-feeds/list.js +++ b/src/web/routes/data-sources/rss-feeds/list.js @@ -4,10 +4,13 @@ const { renderView } = require('../../../view'); function formatIntervalLabel(interval, unit) { const value = Math.max(1, Number(interval) || 0); - const normalizedUnit = String(unit || 'minutes').trim().toLowerCase() === 'seconds' ? 'seconds' : 'minutes'; + const normalizedUnit = String(unit || 'minutes').trim().toLowerCase(); if (normalizedUnit === 'seconds') { return value === 1 ? 'Every second' : `Every ${value} seconds`; } + if (normalizedUnit === 'hours') { + return value === 1 ? 'Every hour' : `Every ${value} hours`; + } return value === 1 ? 'Every minute' : `Every ${value} minutes`; } diff --git a/src/web/views/data-sources/api-sources/form.hbs b/src/web/views/data-sources/api-sources/form.hbs index 5e1eaf3..8546b95 100644 --- a/src/web/views/data-sources/api-sources/form.hbs +++ b/src/web/views/data-sources/api-sources/form.hbs @@ -90,6 +90,7 @@ diff --git a/src/web/views/data-sources/rss-feeds/form.hbs b/src/web/views/data-sources/rss-feeds/form.hbs index 654acfd..167def7 100644 --- a/src/web/views/data-sources/rss-feeds/form.hbs +++ b/src/web/views/data-sources/rss-feeds/form.hbs @@ -29,6 +29,7 @@
diff --git a/test/data-source-interval-units.test.js b/test/data-source-interval-units.test.js new file mode 100644 index 0000000..e7d2de4 --- /dev/null +++ b/test/data-source-interval-units.test.js @@ -0,0 +1,46 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { buildApiSourcePayload } = require('../src/data/api-sources'); +const { buildRssFeedPayload } = require('../src/data/rss-feeds'); +const { normalizeIntervalMs } = require('../src/web/lib/background-tasks/queue'); + +test('data source payloads accept hours as an update interval unit', () => { + const apiPayload = buildApiSourcePayload({ + body: { + name: 'API source', + api_url: 'https://example.com/api', + update_interval_value: '2', + update_interval_unit: 'hours' + } + }, null); + + const rssPayload = buildRssFeedPayload({ + body: { + name: 'RSS feed', + feed_url: 'https://example.com/feed.xml', + update_interval_value: '3', + update_interval_unit: 'hours', + item_limit: '5' + } + }, null); + + assert.equal(apiPayload.updateIntervalUnit, 'hours'); + assert.equal(apiPayload.updateIntervalValue, 2); + assert.equal(rssPayload.updateIntervalUnit, 'hours'); + assert.equal(rssPayload.updateIntervalValue, 3); +}); + +test('background task interval conversion supports hours', () => { + assert.equal(normalizeIntervalMs(2, 'hours'), 7_200_000); +}); + +test('data source forms expose hours as an interval option', () => { + const apiTemplate = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'data-sources', 'api-sources', 'form.hbs'), 'utf8'); + const rssTemplate = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'data-sources', 'rss-feeds', 'form.hbs'), 'utf8'); + + assert.match(apiTemplate, /value="hours"[\s\S]*>Hours<\/option>/); + assert.match(rssTemplate, /value="hours"[\s\S]*>Hours<\/option>/); +});