From 72284069299b3549cced532e332d859cf4af9449 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sun, 25 Oct 2026 20:33:59 +0000 Subject: [PATCH] Release v2.20.3 --- CHANGELOG.md | 10 ++++ build/package.player.json | 2 +- build/package.web.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/web/lib/playlist-schedule.js | 16 ++++++- src/web/public/css/theme-custom.css | 8 ++++ .../public/js/playlists/playlist-schedule.js | 16 ++++++- test/playlist-form-view-model.test.js | 48 +++++++++++++++++++ 9 files changed, 101 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0533a78..5347da2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## 2.20.3 - 2026-10-25 + +### Added + +- Playlist schedule summaries show up to two numbered rules on separate lines, with a 60-character description limit per rule. Longer summaries and schedules with three or more rules show the rule count instead. Time windows without selected weekdays explicitly show "Any day". + +### Changed + +- Standardized the Screen groups navigation label. + ## 2.20.2 - 2026-10-25 ### Changed diff --git a/build/package.player.json b/build/package.player.json index f57af25..957fd72 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.20.2", + "version": "2.20.3", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index ea6fe98..b5bb4b2 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.20.2", + "version": "2.20.3", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index ac5f805..7101478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.20.2", + "version": "2.20.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.20.2", + "version": "2.20.3", "dependencies": { "@better-auth/passkey": "^1.7.13", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index d4ddb37..01ba563 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.20.2", + "version": "2.20.3", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/lib/playlist-schedule.js b/src/web/lib/playlist-schedule.js index c3c8a0b..b4c13ec 100644 --- a/src/web/lib/playlist-schedule.js +++ b/src/web/lib/playlist-schedule.js @@ -182,6 +182,8 @@ function formatScheduleRuleSummary(rule) { const days = formatDays(normalized.days || []); if (days) { rightParts.push(days); + } else if (normalized.start_time && normalized.end_time) { + rightParts.push('Any day'); } if (normalized.start_time && normalized.end_time) { rightParts.push(formatTime(normalized.start_time) + '-' + formatTime(normalized.end_time)); @@ -200,7 +202,19 @@ function formatScheduleRulesSummary(value) { return 'Always visible'; } - return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules'; + if (rules.length >= 3) { + return String(rules.length) + ' rules'; + } + + const summaries = rules.map(function (rule) { + return formatScheduleRuleSummary(rule).replace(/^Any date: /, ''); + }); + if (summaries.some(function (summary) { return summary.length > 60; })) { + return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules'; + } + return summaries.map(function (summary, index) { + return 'Rule ' + String(index + 1) + ': ' + summary; + }).join('\n'); } module.exports = { diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 33405a9..da415af 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -2396,6 +2396,14 @@ white-space: nowrap; } +.playlist-schedule-summary .chip { + display: inline-block; + max-width: min(30rem, 100%); + white-space: pre-line; + overflow-wrap: anywhere; + border-radius: 0.375rem; +} + .empty { color: var(--bs-secondary-color); font-style: italic; diff --git a/src/web/public/js/playlists/playlist-schedule.js b/src/web/public/js/playlists/playlist-schedule.js index 63a3f6a..d7138c6 100644 --- a/src/web/public/js/playlists/playlist-schedule.js +++ b/src/web/public/js/playlists/playlist-schedule.js @@ -146,6 +146,8 @@ var days = formatDays(normalized.days || []); if (days) { right.push(days); + } else if (normalized.start_time && normalized.end_time) { + right.push('Any day'); } if (normalized.start_time && normalized.end_time) { right.push(normalized.start_time.slice(0, 5) + '-' + normalized.end_time.slice(0, 5)); @@ -165,7 +167,19 @@ return 'Always visible'; } - return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules'; + if (rules.length >= 3) { + return String(rules.length) + ' rules'; + } + + var summaries = rules.map(function (rule) { + return formatScheduleRuleSummary(rule).replace(/^Any date: /, ''); + }); + if (summaries.some(function (summary) { return summary.length > 60; })) { + return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules'; + } + return summaries.map(function (summary, index) { + return 'Rule ' + String(index + 1) + ': ' + summary; + }).join('\n'); } function parseScheduleBoundary(value) { diff --git a/test/playlist-form-view-model.test.js b/test/playlist-form-view-model.test.js index 960140b..0eed636 100644 --- a/test/playlist-form-view-model.test.js +++ b/test/playlist-form-view-model.test.js @@ -5,6 +5,54 @@ require('../src/common'); const { buildPlaylistFormViewModel } = require('../src/web/routes/signage/playlists/form-view-model'); +test('playlist schedule summaries show up to two short rules on separate lines and count longer summaries', () => { + const fs = require('node:fs'); + const path = require('node:path'); + const vm = require('node:vm'); + const { formatScheduleRulesSummary } = require('../src/web/lib/playlist-schedule'); + const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/playlists/playlist-schedule.js'), 'utf8'); + const context = {}; + vm.runInNewContext(source.slice(0, source.indexOf(' function parseScheduleBoundary(')) + + 'globalThis.formatSummary = formatScheduleRulesSummary; })();', context); + + const cases = [ + [[], 'Always visible'], + [[{ days: [1, 3], start_time: '09:00', end_time: '17:00' }], 'Rule 1: Mon, Wed 09:00-17:00'], + [[{ start_time: '09:00', end_time: '17:00' }], 'Rule 1: Any day 09:00-17:00'], + [[{ start_time: '09:00', end_time: '17:00' }, { days: [5] }], + 'Rule 1: Any day 09:00-17:00\nRule 2: Fri'], + [[{ days: [1] }, { days: [5] }], 'Rule 1: Mon\nRule 2: Fri'], + [[{ days: [1] }, { days: [3] }, { days: [5] }], '3 rules'], + [[{ days: [1] }, { days: [2] }, { days: [3] }, { days: [4] }], '4 rules'], + [[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', days: [1] }], + 'Rule 1: Dates 2026-10-25 09:00 to 2026-10-26 17:00: Mon'], + [[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', + days: [1, 2], start_time: '09:00', end_time: '17:00' }], + '1 rule'], + [[{ days: [0, 1, 2, 3, 4, 5, 6], start_time: '09:00', end_time: '17:00' }, + { days: [1, 2, 3, 4, 5], start_time: '18:00', end_time: '19:00' }], + 'Rule 1: Sun, Mon, Tue, Wed, Thu, Fri, Sat 09:00-17:00\nRule 2: Mon, Tue, Wed, Thu, Fri 18:00-19:00'], + [[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', + days: [1, 2, 3] }], 'Rule 1: Dates 2026-10-25 09:00 to 2026-10-26 17:00: Mon, Tue, Wed'], + [[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', + days: [1, 2, 3, 4, 5] }], '1 rule'], + [[{ days: [1] }, { start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', + days: [1, 2, 3, 4, 5] }], '2 rules'], + [[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', + days: [1, 2], start_time: '09:00', end_time: '17:00' }, { days: [5] }], '2 rules'] + ]; + + for (const [rules, expected] of cases) { + assert.equal(formatScheduleRulesSummary(rules), expected); + assert.equal(context.formatSummary(rules), expected); + assert.equal(formatScheduleRulesSummary(JSON.stringify(rules)), expected); + assert.equal(context.formatSummary(JSON.stringify(rules)), expected); + } + + const css = fs.readFileSync(path.join(__dirname, '../src/web/public/css/theme-custom.css'), 'utf8'); + assert.match(css, /\.playlist-schedule-summary \.chip\s*\{[^}]*display:\s*inline-block;[^}]*white-space:\s*pre-line;[^}]*overflow-wrap:\s*anywhere;/); +}); + test('playlist rows show mute controls for RTMP slides', () => { const model = buildPlaylistFormViewModel( { id: 22, name: 'Playlist 22' },