From c36b9122c934fbdcd4074ef53080788a2d877b75 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sun, 9 Aug 2026 12:30:22 +0100 Subject: [PATCH] Release v2.6.21 --- CHANGELOG.md | 8 +++++ build/package.player.json | 2 +- build/package.web.json | 2 +- docs/schema.md | 30 ++++++++++++------- package.json | 2 +- src/player/regions/time-date.js | 4 +-- src/web/public/js/regions/type/schedule.js | 14 ++++----- src/web/public/js/regions/type/time-date.js | 6 ++-- src/web/public/js/shared/placeholder-utils.js | 10 +++++-- .../js/shared/time-date-placeholders.js | 4 +-- test/timetable-region-timezone.test.js | 12 ++++---- 11 files changed, 58 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78aa5bc..071fe7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## 2.6.21 - 2026-08-09 + +### Changed + +- Startup now logs the previously detected schema version, the current app version, and whether migrations are required. +- The schema documentation now reflects the timetable table rename, the new `o_app_state` table, and startup version tracking. +- Timetable timezone placeholders now use `tz` for the short zone name and `tz_long` for the full IANA zone. + ## 2.6.20 - 2026-08-09 ### Changed diff --git a/build/package.player.json b/build/package.player.json index 513fbb0..cb33a65 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.6.20", + "version": "2.6.21", "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 421bf29..61494d3 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.6.20", + "version": "2.6.21", "private": false, "description": "Pulse Signage web and bridge application bundle", "main": "src/common.js", diff --git a/docs/schema.md b/docs/schema.md index ed53229..4970c2f 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -165,8 +165,8 @@ Primary keys are `id` unless noted otherwise. Timestamps are stored as `created_ - `i_rss_feeds` - RSS feed definitions and refresh cadence. - `i_rss_feed_items` - cached RSS feed items. - `i_api_sources` - API source definitions and last response snapshot. -- `i_schedule_groups` - grouped schedule definitions used by the schedule region. -- `i_schedule_entries` - dated entries that belong to a schedule group. +- `i_timetable_groups` - grouped timetable definitions used by the timetable region. +- `i_timetable_entries` - dated entries that belong to a timetable group. ### `i_rss_feeds` @@ -184,32 +184,40 @@ Primary keys are `id` unless noted otherwise. Timestamps are stored as `created_ - `id`, `name`, `api_url`, `auth_method`, `auth_username`, `auth_password`, `auth_bearer_token`, `auth_header_name`, `auth_header_value`, `items_path`, `update_interval_value`, `update_interval_unit`, `last_pulled_at`, `last_pull_error`, `last_response_status`, `last_response_content_type`, `last_response_json`, `created_at`, `created_by`, `modified_at`, `modified_by` -### `i_schedule_groups` +### `i_timetable_groups` -- `id`, `name`, `short_description`, `created_at`, `created_by`, `modified_at`, `modified_by` +- `id`, `name`, `short_description`, `timezone`, `created_at`, `created_by`, `modified_at`, `modified_by` +- `timezone` defaults to `Europe/London`. -### `i_schedule_entries` +### `i_timetable_entries` - `id`, `schedule_group_id`, `title`, `short_description`, `start_datetime`, `end_datetime`, `created_at`, `created_by`, `modified_at`, `modified_by` - Foreign key: - - `schedule_group_id` -> `i_schedule_groups.id` with `ON DELETE CASCADE` + - `schedule_group_id` -> `i_timetable_groups.id` with `ON DELETE CASCADE` - Index: - `(schedule_group_id, start_datetime)` ## Operations - `o_background_tasks` - queue and history for background jobs. +- `o_app_state` - generic app state and version markers stored as key/value pairs. ### `o_background_tasks` - `id`, `task_key`, `task_type`, `title`, `category`, `status`, `payload_json`, `metadata_json`, `attempts`, `created_at`, `created_by`, `started_at`, `finished_at`, `error_message` - Indexed by `status`, `task_key`, and `task_type`. +### `o_app_state` + +- `state_key`, `state_value`, `created_at`, `modified_at` +- `state_key` is the primary key. +- `schema_version` is stored here so startup can detect the previously recorded schema version before deciding whether migrations need to run. + ## Notes - The schema is initialized with `CREATE TABLE IF NOT EXISTS`, so new installs can start from an empty database. - `src/db/bootstrap.js` seeds the canvas size defaults, default permissions, and the default administrator role. -- The migration module stays in place for future releases, but this version treats the current schema as the install baseline. +- `src/db/migrations.js` records the current schema version in `o_app_state` during startup so later launches can tell whether an update is happening. ```mermaid erDiagram @@ -255,9 +263,11 @@ erDiagram } I_API_SOURCES { } - I_SCHEDULE_GROUPS { + I_TIMETABLE_GROUPS { } - I_SCHEDULE_ENTRIES { + I_TIMETABLE_ENTRIES { + } + O_APP_STATE { } O_BACKGROUND_TASKS { } @@ -282,5 +292,5 @@ erDiagram D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives I_RSS_FEEDS ||--o{ I_RSS_FEED_ITEMS : caches - I_SCHEDULE_GROUPS ||--o{ I_SCHEDULE_ENTRIES : contains + I_TIMETABLE_GROUPS ||--o{ I_TIMETABLE_ENTRIES : contains ``` \ No newline at end of file diff --git a/package.json b/package.json index 348ab2b..5f7aca8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.6.20", + "version": "2.6.21", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/player/regions/time-date.js b/src/player/regions/time-date.js index 62df679..8cc0a7e 100644 --- a/src/player/regions/time-date.js +++ b/src/player/regions/time-date.js @@ -213,8 +213,8 @@ function getFormattedParts(timeZone, date) { MMM: toTitleCase(getPart(monthShort, 'month')), MMMM: toTitleCase(getPart(monthLong, 'month')), a: toTitleCase(getPart(ampm, 'dayPeriod')), - tz: resolvedTimeZone, - tz_short: getPart(timezoneShort, 'timeZoneName'), + tz: getPart(timezoneShort, 'timeZoneName'), + tz_long: resolvedTimeZone, date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'), time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second') }; diff --git a/src/web/public/js/regions/type/schedule.js b/src/web/public/js/regions/type/schedule.js index d460ecf..3fb588c 100644 --- a/src/web/public/js/regions/type/schedule.js +++ b/src/web/public/js/regions/type/schedule.js @@ -84,8 +84,8 @@ } return { - tz: timeZone, - tz_short: shortName + tz: shortName, + tz_long: timeZone }; } @@ -298,7 +298,7 @@ '
' + '
Available placeholders
' + '
' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '
' + - '
Placeholder values support transforms, for example {{title.upper()}}, {{title.title()}}, {{title.lower()}}, {{start.format("MMM D, YYYY h:mm A")}}, {{start.tz()}} or {{start.tz_short()}}.
' + + '
Placeholder values support transforms, for example {{title.upper()}}, {{title.title()}}, {{title.lower()}}, {{start.format("MMM D, YYYY h:mm A")}}, {{start.tz()}} for the short zone name, or {{start.tz_long()}} for the full IANA zone name.
' + '
' + 'Supported date format tokens' + '
' + renderScheduleFormatTokenTable() + '
' + @@ -392,8 +392,8 @@ timetable_group_id: timetableGroupInput ? timetableGroupInput.value || current.timetable_group_id : current.timetable_group_id, display_mode: timetableDisplayModeInput ? timetableDisplayModeInput.value || current.display_mode : current.display_mode, tz: timezoneValues.tz, - tz_short: timezoneValues.tz_short, - timeZone: timezoneValues.tz, + tz_long: timezoneValues.tz_long, + timeZone: timezoneValues.tz_long, max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items, existingContent: existingContent || {}, timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : [] @@ -421,8 +421,8 @@ start: entry && entry.start_datetime !== undefined ? entry.start_datetime : '', end: entry && entry.end_datetime !== undefined ? entry.end_datetime : '', tz: timezoneValues.tz, - tz_short: timezoneValues.tz_short, - timeZone: timezoneValues.tz, + tz_long: timezoneValues.tz_long, + timeZone: timezoneValues.tz_long, group: group || {}, entries: entries, index: index + 1 diff --git a/src/web/public/js/regions/type/time-date.js b/src/web/public/js/regions/type/time-date.js index e48cc5b..9d782de 100644 --- a/src/web/public/js/regions/type/time-date.js +++ b/src/web/public/js/regions/type/time-date.js @@ -156,11 +156,11 @@ MMM: toTitleCase(getPart(monthShort, 'month')), MMMM: toTitleCase(getPart(monthLong, 'month')), a: toTitleCase(getPart(ampm, 'dayPeriod')), - tz: resolvedTimeZone, - tz_short: getPart(getFormatter('tz-short:' + resolvedTimeZone, { + tz: getPart(getFormatter('tz-short:' + resolvedTimeZone, { timeZone: resolvedTimeZone, timeZoneName: 'short' }).formatToParts(targetDate), 'timeZoneName'), + tz_long: resolvedTimeZone, date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'), time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second') }; @@ -200,7 +200,7 @@ 'MMMM', 'a', 'tz', - 'tz_short' + 'tz_long' ]; if (typeof placeholderChips.renderChips === 'function') { diff --git a/src/web/public/js/shared/placeholder-utils.js b/src/web/public/js/shared/placeholder-utils.js index e3f7581..7e8272d 100644 --- a/src/web/public/js/shared/placeholder-utils.js +++ b/src/web/public/js/shared/placeholder-utils.js @@ -245,6 +245,10 @@ } } + function getTimeZoneLongName(timeZone) { + return getTransformTimeZone([], { timeZone: timeZone }); + } + function getTransformTimeZone(args, options) { return resolveTimeZone((args && args[0]) || (options && options.timeZone) || ''); } @@ -273,11 +277,11 @@ } if (name === 'tz') { - return getTransformTimeZone(args, options); + return getTimeZoneShortName(value, getTransformTimeZone(args, options)); } - if (name === 'tz_short') { - return getTimeZoneShortName(value, getTransformTimeZone(args, options)); + if (name === 'tz_long') { + return getTimeZoneLongName(getTransformTimeZone(args, options)); } return text; diff --git a/src/web/public/js/shared/time-date-placeholders.js b/src/web/public/js/shared/time-date-placeholders.js index e98f268..75a57af 100644 --- a/src/web/public/js/shared/time-date-placeholders.js +++ b/src/web/public/js/shared/time-date-placeholders.js @@ -24,8 +24,8 @@ ]; var HELPER_PLACEHOLDERS = [ - { token: 'tz', output: 'Europe/London', description: 'The resolved time zone name' }, - { token: 'tz_short', output: 'BST/GMT', description: 'The time zone abbreviation for the selected date' } + { token: 'tz', output: 'BST/GMT', description: 'The time zone abbreviation for the selected date' }, + { token: 'tz_long', output: 'Europe/London', description: 'The resolved time zone name' } ]; var PLACEHOLDERS = FORMAT_PLACEHOLDERS.concat(HELPER_PLACEHOLDERS); diff --git a/test/timetable-region-timezone.test.js b/test/timetable-region-timezone.test.js index d754d09..e2b76c5 100644 --- a/test/timetable-region-timezone.test.js +++ b/test/timetable-region-timezone.test.js @@ -86,7 +86,7 @@ test('timetable region editor lists timezone transforms in helper text', () => { const html = module.renderEditorCard({ region: { id: 47, label: 'Timetable' }, current: { - value: '

{{title}} {{start.tz()}} {{start.tz_short()}}

', + value: '

{{title}} {{start.tz()}} {{start.tz_long()}}

', timetable_group_id: 1, display_mode: 'current', max_items: 3 @@ -103,7 +103,7 @@ test('timetable region editor lists timezone transforms in helper text', () => { assert.match(html, /Placeholder values support transforms/); assert.match(html, /\{\{start\.tz\(\)\}\}<\/code>/); - assert.match(html, /\{\{start\.tz_short\(\)\}\}<\/code>/); + assert.match(html, /\{\{start\.tz_long\(\)\}\}<\/code>/); assert.doesNotMatch(html, /class="chip">\{\{start\.tz\(\)\}\}<\/span>/); }); @@ -136,7 +136,7 @@ test('timetable region editor shows supported date format tokens', () => { assert.doesNotMatch(html, /yyyy<\/code><\/td>/); assert.doesNotMatch(html, /yy<\/code><\/td>/); assert.doesNotMatch(html, /tz<\/code><\/td>/); - assert.doesNotMatch(html, /tz_short<\/code><\/td>/); + assert.match(html, /start\.tz_long\(\)/); assert.match(html, /Use these tokens inside \.format\(\.\.\.\)<\/code>/); }); @@ -145,7 +145,7 @@ test('timetable region preview resolves timezone placeholders from explicit time const preview = module.renderPreview( { id: 47, width: 500, height: 280 }, { - value: '

{{start.tz("UTC")}} {{start.tz_short("UTC")}}

', + value: '

{{start.tz("UTC")}} {{start.tz_long("UTC")}}

', timetable_group_id: 1, display_mode: 'both', max_items: 3 @@ -210,7 +210,7 @@ test('timetable timezone abbreviation changes across daylight saving boundaries' const preview = module.renderPreview( { id: 47, width: 500, height: 280 }, { - value: '

{{start.tz_short("Europe/London")}}

', + value: '

{{start.tz("Europe/London")}}

', timetable_group_id: 1, display_mode: 'both', max_items: 10 @@ -265,7 +265,7 @@ test('timetable preview does not force the group timezone into placeholder trans const preview = module.renderPreview( { id: 47, width: 500, height: 280 }, { - value: '

{{start.tz_short()}}

', + value: '

{{start.tz_long()}}

', timetable_group_id: 1, display_mode: 'both', max_items: 3