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.
|
||||
|
||||
## 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,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,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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user