Release v2.6.21

This commit is contained in:
2026-08-09 12:30:22 +01:00
parent 39f2098ffe
commit c36b9122c9
11 changed files with 58 additions and 36 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+20 -10
View File
@@ -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
```
+1 -1
View File
@@ -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": {
+2 -2
View File
@@ -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')
};
+7 -7
View File
@@ -84,8 +84,8 @@
}
return {
tz: timeZone,
tz_short: shortName
tz: shortName,
tz_long: timeZone
};
}
@@ -298,7 +298,7 @@
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
'<div class="api-region-placeholder-title">Available placeholders</div>' +
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
'<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{start.format("MMM D, YYYY h:mm A")}}</code>, <code>{{start.tz()}}</code> or <code>{{start.tz_short()}}</code>.</div>' +
'<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{start.format("MMM D, YYYY h:mm A")}}</code>, <code>{{start.tz()}}</code> for the short zone name, or <code>{{start.tz_long()}}</code> for the full IANA zone name.</div>' +
'<details class="mt-2">' +
'<summary class="small text-body-secondary">Supported date format tokens</summary>' +
'<div class="mt-2">' + renderScheduleFormatTokenTable() + '</div>' +
@@ -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
+3 -3
View File
@@ -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') {
@@ -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;
@@ -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);
+6 -6
View File
@@ -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: '<p>{{title}} {{start.tz()}} {{start.tz_short()}}</p>',
value: '<p>{{title}} {{start.tz()}} {{start.tz_long()}}</p>',
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, /<code>\{\{start\.tz\(\)\}\}<\/code>/);
assert.match(html, /<code>\{\{start\.tz_short\(\)\}\}<\/code>/);
assert.match(html, /<code>\{\{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, /<td><code>yyyy<\/code><\/td>/);
assert.doesNotMatch(html, /<td><code>yy<\/code><\/td>/);
assert.doesNotMatch(html, /<td><code>tz<\/code><\/td>/);
assert.doesNotMatch(html, /<td><code>tz_short<\/code><\/td>/);
assert.match(html, /start\.tz_long\(\)/);
assert.match(html, /Use these tokens inside <code>\.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: '<p>{{start.tz("UTC")}} {{start.tz_short("UTC")}}</p>',
value: '<p>{{start.tz("UTC")}} {{start.tz_long("UTC")}}</p>',
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: '<p>{{start.tz_short("Europe/London")}}</p>',
value: '<p>{{start.tz("Europe/London")}}</p>',
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: '<p>{{start.tz_short()}}</p>',
value: '<p>{{start.tz_long()}}</p>',
timetable_group_id: 1,
display_mode: 'both',
max_items: 3