Release v2.6.21
This commit is contained in:
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
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.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
|
## 2.6.20 - 2026-08-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.6.20",
|
"version": "2.6.21",
|
||||||
"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.20",
|
"version": "2.6.21",
|
||||||
"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",
|
||||||
|
|||||||
+20
-10
@@ -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_feeds` - RSS feed definitions and refresh cadence.
|
||||||
- `i_rss_feed_items` - cached RSS feed items.
|
- `i_rss_feed_items` - cached RSS feed items.
|
||||||
- `i_api_sources` - API source definitions and last response snapshot.
|
- `i_api_sources` - API source definitions and last response snapshot.
|
||||||
- `i_schedule_groups` - grouped schedule definitions used by the schedule region.
|
- `i_timetable_groups` - grouped timetable definitions used by the timetable region.
|
||||||
- `i_schedule_entries` - dated entries that belong to a schedule group.
|
- `i_timetable_entries` - dated entries that belong to a timetable group.
|
||||||
|
|
||||||
### `i_rss_feeds`
|
### `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`
|
- `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`
|
- `id`, `schedule_group_id`, `title`, `short_description`, `start_datetime`, `end_datetime`, `created_at`, `created_by`, `modified_at`, `modified_by`
|
||||||
- Foreign key:
|
- 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:
|
- Index:
|
||||||
- `(schedule_group_id, start_datetime)`
|
- `(schedule_group_id, start_datetime)`
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
- `o_background_tasks` - queue and history for background jobs.
|
- `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`
|
### `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`
|
- `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`.
|
- 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
|
## Notes
|
||||||
|
|
||||||
- The schema is initialized with `CREATE TABLE IF NOT EXISTS`, so new installs can start from an empty database.
|
- 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.
|
- `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
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
@@ -255,9 +263,11 @@ erDiagram
|
|||||||
}
|
}
|
||||||
I_API_SOURCES {
|
I_API_SOURCES {
|
||||||
}
|
}
|
||||||
I_SCHEDULE_GROUPS {
|
I_TIMETABLE_GROUPS {
|
||||||
}
|
}
|
||||||
I_SCHEDULE_ENTRIES {
|
I_TIMETABLE_ENTRIES {
|
||||||
|
}
|
||||||
|
O_APP_STATE {
|
||||||
}
|
}
|
||||||
O_BACKGROUND_TASKS {
|
O_BACKGROUND_TASKS {
|
||||||
}
|
}
|
||||||
@@ -282,5 +292,5 @@ erDiagram
|
|||||||
D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives
|
D_SCREENS ||--o{ D_ANNOUNCEMENT_SCREENS : receives
|
||||||
|
|
||||||
I_RSS_FEEDS ||--o{ I_RSS_FEED_ITEMS : caches
|
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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.6.20",
|
"version": "2.6.21",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -213,8 +213,8 @@ function getFormattedParts(timeZone, date) {
|
|||||||
MMM: toTitleCase(getPart(monthShort, 'month')),
|
MMM: toTitleCase(getPart(monthShort, 'month')),
|
||||||
MMMM: toTitleCase(getPart(monthLong, 'month')),
|
MMMM: toTitleCase(getPart(monthLong, 'month')),
|
||||||
a: toTitleCase(getPart(ampm, 'dayPeriod')),
|
a: toTitleCase(getPart(ampm, 'dayPeriod')),
|
||||||
tz: resolvedTimeZone,
|
tz: getPart(timezoneShort, 'timeZoneName'),
|
||||||
tz_short: getPart(timezoneShort, 'timeZoneName'),
|
tz_long: resolvedTimeZone,
|
||||||
date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'),
|
date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'),
|
||||||
time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second')
|
time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second')
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,8 +84,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tz: timeZone,
|
tz: shortName,
|
||||||
tz_short: 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-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
|
||||||
'<div class="api-region-placeholder-title">Available placeholders</div>' +
|
'<div class="api-region-placeholder-title">Available placeholders</div>' +
|
||||||
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</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">' +
|
'<details class="mt-2">' +
|
||||||
'<summary class="small text-body-secondary">Supported date format tokens</summary>' +
|
'<summary class="small text-body-secondary">Supported date format tokens</summary>' +
|
||||||
'<div class="mt-2">' + renderScheduleFormatTokenTable() + '</div>' +
|
'<div class="mt-2">' + renderScheduleFormatTokenTable() + '</div>' +
|
||||||
@@ -392,8 +392,8 @@
|
|||||||
timetable_group_id: timetableGroupInput ? timetableGroupInput.value || current.timetable_group_id : current.timetable_group_id,
|
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,
|
display_mode: timetableDisplayModeInput ? timetableDisplayModeInput.value || current.display_mode : current.display_mode,
|
||||||
tz: timezoneValues.tz,
|
tz: timezoneValues.tz,
|
||||||
tz_short: timezoneValues.tz_short,
|
tz_long: timezoneValues.tz_long,
|
||||||
timeZone: timezoneValues.tz,
|
timeZone: timezoneValues.tz_long,
|
||||||
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
|
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
|
||||||
existingContent: existingContent || {},
|
existingContent: existingContent || {},
|
||||||
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
|
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
|
||||||
@@ -421,8 +421,8 @@
|
|||||||
start: entry && entry.start_datetime !== undefined ? entry.start_datetime : '',
|
start: entry && entry.start_datetime !== undefined ? entry.start_datetime : '',
|
||||||
end: entry && entry.end_datetime !== undefined ? entry.end_datetime : '',
|
end: entry && entry.end_datetime !== undefined ? entry.end_datetime : '',
|
||||||
tz: timezoneValues.tz,
|
tz: timezoneValues.tz,
|
||||||
tz_short: timezoneValues.tz_short,
|
tz_long: timezoneValues.tz_long,
|
||||||
timeZone: timezoneValues.tz,
|
timeZone: timezoneValues.tz_long,
|
||||||
group: group || {},
|
group: group || {},
|
||||||
entries: entries,
|
entries: entries,
|
||||||
index: index + 1
|
index: index + 1
|
||||||
|
|||||||
@@ -156,11 +156,11 @@
|
|||||||
MMM: toTitleCase(getPart(monthShort, 'month')),
|
MMM: toTitleCase(getPart(monthShort, 'month')),
|
||||||
MMMM: toTitleCase(getPart(monthLong, 'month')),
|
MMMM: toTitleCase(getPart(monthLong, 'month')),
|
||||||
a: toTitleCase(getPart(ampm, 'dayPeriod')),
|
a: toTitleCase(getPart(ampm, 'dayPeriod')),
|
||||||
tz: resolvedTimeZone,
|
tz: getPart(getFormatter('tz-short:' + resolvedTimeZone, {
|
||||||
tz_short: getPart(getFormatter('tz-short:' + resolvedTimeZone, {
|
|
||||||
timeZone: resolvedTimeZone,
|
timeZone: resolvedTimeZone,
|
||||||
timeZoneName: 'short'
|
timeZoneName: 'short'
|
||||||
}).formatToParts(targetDate), 'timeZoneName'),
|
}).formatToParts(targetDate), 'timeZoneName'),
|
||||||
|
tz_long: resolvedTimeZone,
|
||||||
date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'),
|
date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'),
|
||||||
time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second')
|
time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second')
|
||||||
};
|
};
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
'MMMM',
|
'MMMM',
|
||||||
'a',
|
'a',
|
||||||
'tz',
|
'tz',
|
||||||
'tz_short'
|
'tz_long'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (typeof placeholderChips.renderChips === 'function') {
|
if (typeof placeholderChips.renderChips === 'function') {
|
||||||
|
|||||||
@@ -245,6 +245,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTimeZoneLongName(timeZone) {
|
||||||
|
return getTransformTimeZone([], { timeZone: timeZone });
|
||||||
|
}
|
||||||
|
|
||||||
function getTransformTimeZone(args, options) {
|
function getTransformTimeZone(args, options) {
|
||||||
return resolveTimeZone((args && args[0]) || (options && options.timeZone) || '');
|
return resolveTimeZone((args && args[0]) || (options && options.timeZone) || '');
|
||||||
}
|
}
|
||||||
@@ -273,11 +277,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'tz') {
|
if (name === 'tz') {
|
||||||
return getTransformTimeZone(args, options);
|
return getTimeZoneShortName(value, getTransformTimeZone(args, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'tz_short') {
|
if (name === 'tz_long') {
|
||||||
return getTimeZoneShortName(value, getTransformTimeZone(args, options));
|
return getTimeZoneLongName(getTransformTimeZone(args, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
var HELPER_PLACEHOLDERS = [
|
var HELPER_PLACEHOLDERS = [
|
||||||
{ token: 'tz', output: 'Europe/London', description: 'The resolved time zone name' },
|
{ token: 'tz', output: 'BST/GMT', description: 'The time zone abbreviation for the selected date' },
|
||||||
{ token: 'tz_short', 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);
|
var PLACEHOLDERS = FORMAT_PLACEHOLDERS.concat(HELPER_PLACEHOLDERS);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ test('timetable region editor lists timezone transforms in helper text', () => {
|
|||||||
const html = module.renderEditorCard({
|
const html = module.renderEditorCard({
|
||||||
region: { id: 47, label: 'Timetable' },
|
region: { id: 47, label: 'Timetable' },
|
||||||
current: {
|
current: {
|
||||||
value: '<p>{{title}} {{start.tz()}} {{start.tz_short()}}</p>',
|
value: '<p>{{title}} {{start.tz()}} {{start.tz_long()}}</p>',
|
||||||
timetable_group_id: 1,
|
timetable_group_id: 1,
|
||||||
display_mode: 'current',
|
display_mode: 'current',
|
||||||
max_items: 3
|
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, /Placeholder values support transforms/);
|
||||||
assert.match(html, /<code>\{\{start\.tz\(\)\}\}<\/code>/);
|
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>/);
|
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>yyyy<\/code><\/td>/);
|
||||||
assert.doesNotMatch(html, /<td><code>yy<\/code><\/td>/);
|
assert.doesNotMatch(html, /<td><code>yy<\/code><\/td>/);
|
||||||
assert.doesNotMatch(html, /<td><code>tz<\/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>/);
|
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(
|
const preview = module.renderPreview(
|
||||||
{ id: 47, width: 500, height: 280 },
|
{ 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,
|
timetable_group_id: 1,
|
||||||
display_mode: 'both',
|
display_mode: 'both',
|
||||||
max_items: 3
|
max_items: 3
|
||||||
@@ -210,7 +210,7 @@ test('timetable timezone abbreviation changes across daylight saving boundaries'
|
|||||||
const preview = module.renderPreview(
|
const preview = module.renderPreview(
|
||||||
{ id: 47, width: 500, height: 280 },
|
{ 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,
|
timetable_group_id: 1,
|
||||||
display_mode: 'both',
|
display_mode: 'both',
|
||||||
max_items: 10
|
max_items: 10
|
||||||
@@ -265,7 +265,7 @@ test('timetable preview does not force the group timezone into placeholder trans
|
|||||||
const preview = module.renderPreview(
|
const preview = module.renderPreview(
|
||||||
{ id: 47, width: 500, height: 280 },
|
{ id: 47, width: 500, height: 280 },
|
||||||
{
|
{
|
||||||
value: '<p>{{start.tz_short()}}</p>',
|
value: '<p>{{start.tz_long()}}</p>',
|
||||||
timetable_group_id: 1,
|
timetable_group_id: 1,
|
||||||
display_mode: 'both',
|
display_mode: 'both',
|
||||||
max_items: 3
|
max_items: 3
|
||||||
|
|||||||
Reference in New Issue
Block a user