Compare commits

..
2 Commits
Author SHA1 Message Date
lzstealth c36b9122c9 Release v2.6.21 2026-08-09 12:30:22 +01:00
lzstealth 39f2098ffe Release v2.6.20 2026-08-09 12:18:44 +01:00
14 changed files with 294 additions and 45 deletions
+14
View File
@@ -2,6 +2,20 @@
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
- Startup now logs the previously detected schema version, the current app version, and whether migrations are required.
## 2.6.19 - 2026-08-09
### Changed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage-player",
"version": "2.6.19",
"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.19",
"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.19",
"version": "2.6.21",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+10 -2
View File
@@ -1,3 +1,6 @@
const { version: appVersion } = require('#root/package.json');
const { compareVersions, detectSchemaVersion, recordSchemaVersion, runMigrations } = require('./migrations');
// Snapshot only: keep this file aligned with the current schema state.
async function ensureSchema(pool, options) {
const schemaLockName = 'pulse_signage_schema_lock';
@@ -12,6 +15,11 @@ async function ensureSchema(pool, options) {
}
try {
const currentVersion = await detectSchemaVersion(pool);
const updateRequired = compareVersions(currentVersion, appVersion) < 0;
console.info('[schema] previous=' + currentVersion + ' current=' + appVersion + ' update=' + (updateRequired ? 'yes' : 'no'));
await pool.query(`
CREATE TABLE IF NOT EXISTS c_canvas_sizes (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -384,8 +392,8 @@ async function ensureSchema(pool, options) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
const { runMigrations } = require('./migrations');
await runMigrations(pool, options);
await runMigrations(pool, Object.assign({}, options, { currentVersion: currentVersion }));
await recordSchemaVersion(pool, appVersion);
} finally {
await pool.query('SELECT RELEASE_LOCK(?)', [schemaLockName]).catch(function () {
});
+75 -7
View File
@@ -1,5 +1,7 @@
const { version: appVersion } = require('#root/package.json');
const TIMETABLE_TIME_ZONE = 'Europe/London';
const APP_STATE_TABLE = 'o_app_state';
const APP_STATE_SCHEMA_VERSION_KEY = 'schema_version';
const VERSIONED_MIGRATIONS = [
{
@@ -301,23 +303,42 @@ const VERSIONED_MIGRATIONS = [
version: '2.6.16',
label: 'v2.6.16 timetable timezone schema',
run: async function (pool) {
await ensureColumn(pool, 'i_schedule_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
const tableName = timetableGroupsExists ? 'i_timetable_groups' : scheduleGroupsExists ? 'i_schedule_groups' : null;
if (!tableName) {
return;
}
await ensureColumn(pool, tableName, 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
}
},
{
version: '2.6.17',
label: 'v2.6.17 timetable europe/london conversion',
run: async function (pool) {
await ensureColumn(pool, 'i_schedule_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
const timetableEntriesExists = await tableExists(pool, 'i_timetable_entries');
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
const scheduleEntriesExists = await tableExists(pool, 'i_schedule_entries');
const groupTableName = timetableGroupsExists ? 'i_timetable_groups' : scheduleGroupsExists ? 'i_schedule_groups' : null;
const entryTableName = timetableEntriesExists ? 'i_timetable_entries' : scheduleEntriesExists ? 'i_schedule_entries' : null;
await pool.query('UPDATE i_schedule_groups SET timezone = ?', [TIMETABLE_TIME_ZONE]);
if (!groupTableName || !entryTableName) {
return;
}
const [rows] = await pool.query('SELECT id, start_datetime, end_datetime FROM i_schedule_entries ORDER BY id ASC');
await ensureColumn(pool, groupTableName, 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
await pool.query('UPDATE ' + groupTableName + ' SET timezone = ?', [TIMETABLE_TIME_ZONE]);
const [rows] = await pool.query('SELECT id, start_datetime, end_datetime FROM ' + entryTableName + ' ORDER BY id ASC');
for (const row of rows) {
const startDate = convertMigrationDateTimeFromTimeZone(row.start_datetime, TIMETABLE_TIME_ZONE);
const endDate = row.end_datetime ? convertMigrationDateTimeFromTimeZone(row.end_datetime, TIMETABLE_TIME_ZONE) : null;
await pool.query(
'UPDATE i_schedule_entries SET start_datetime = ?, end_datetime = ? WHERE id = ?',
'UPDATE ' + entryTableName + ' SET start_datetime = ?, end_datetime = ? WHERE id = ?',
[formatMigrationDateTimeUtc(startDate), endDate ? formatMigrationDateTimeUtc(endDate) : null, row.id]
);
}
@@ -333,12 +354,15 @@ const VERSIONED_MIGRATIONS = [
if (scheduleGroupsExists) {
if (!timetableGroupsExists) {
await pool.query('RENAME TABLE i_schedule_groups TO i_timetable_groups, i_schedule_entries TO i_timetable_entries');
await ensureColumn(pool, 'i_timetable_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
return;
}
await ensureColumn(pool, 'i_timetable_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
await pool.query(`
INSERT IGNORE INTO i_timetable_groups (id, name, short_description, timezone, created_at, created_by, modified_at, modified_by)
SELECT id, name, short_description, timezone, created_at, created_by, modified_at, modified_by
SELECT id, name, short_description, 'Europe/London' AS timezone, created_at, created_by, modified_at, modified_by
FROM i_schedule_groups
ORDER BY id ASC
`);
@@ -389,6 +413,47 @@ async function tableExists(pool, tableName) {
return Number(rows && rows[0] && rows[0].table_count) > 0;
}
async function detectSchemaVersion(pool) {
if (await tableExists(pool, APP_STATE_TABLE)) {
const [rows] = await pool.query(
'SELECT state_value FROM ' + APP_STATE_TABLE + ' WHERE state_key = ? LIMIT 1',
[APP_STATE_SCHEMA_VERSION_KEY]
);
const storedVersion = String(rows && rows[0] && rows[0].state_value || '').trim();
if (storedVersion) {
return storedVersion;
}
}
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
const timetableEntriesExists = await tableExists(pool, 'i_timetable_entries');
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
const scheduleEntriesExists = await tableExists(pool, 'i_schedule_entries');
if ((timetableGroupsExists || timetableEntriesExists) && !scheduleGroupsExists && !scheduleEntriesExists) {
return '2.6.18';
}
return '0.0.0';
}
async function recordSchemaVersion(pool, version) {
await pool.query(
`CREATE TABLE IF NOT EXISTS ${APP_STATE_TABLE} (
state_key VARCHAR(191) NOT NULL PRIMARY KEY,
state_value MEDIUMTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
);
await pool.query(
'INSERT INTO ' + APP_STATE_TABLE + ' (state_key, state_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE state_value = VALUES(state_value)',
[APP_STATE_SCHEMA_VERSION_KEY, String(version || appVersion || '0.0.0').trim()]
);
}
async function columnIsAutoIncrement(pool, tableName, columnName) {
const [rows] = await pool.query(
`SELECT COUNT(*) AS auto_increment_count
@@ -772,5 +837,8 @@ async function runMigrations(pool, options) {
module.exports = {
appVersion: appVersion,
runMigrations: runMigrations
runMigrations: runMigrations,
detectSchemaVersion: detectSchemaVersion,
recordSchemaVersion: recordSchemaVersion,
compareVersions: compareVersions
};
+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);
+145
View File
@@ -0,0 +1,145 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const { runMigrations } = require('../src/db/migrations');
function createPool(responses) {
const queries = [];
return {
queries,
async query(sql, params) {
queries.push([sql, params]);
const text = String(sql);
for (const response of responses) {
if (response.match(text, params)) {
return response.result;
}
}
return [[]];
}
};
}
test('timetable rename migration does not depend on legacy timezone column', async () => {
const pool = createPool([
{
match(sql) {
return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?');
},
result: [[{ column_count: 0 }]]
},
{
match(sql) {
return sql.includes('FROM information_schema.TABLES') && sql.includes('TABLE_NAME = ?');
},
result: [[{ table_count: 1 }]]
},
{
match(sql) {
return sql.includes('SELECT id, name, short_description,') && sql.includes("'Europe/London' AS timezone") && sql.includes('FROM i_schedule_groups');
},
result: [{}]
},
{
match(sql) {
return sql.includes('SELECT id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, created_by, modified_at, modified_by FROM i_schedule_entries');
},
result: [{}]
},
{
match(sql) {
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_groups');
},
result: [[{ max_id: 1 }]]
},
{
match(sql) {
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_entries');
},
result: [[{ max_id: 2 }]]
}
]);
await runMigrations(pool, { currentVersion: '2.6.17' });
assert.ok(pool.queries.some(function ([sql]) {
return String(sql).includes("SELECT id, name, short_description, 'Europe/London' AS timezone") && String(sql).includes('FROM i_schedule_groups');
}));
assert.ok(!pool.queries.some(function ([sql]) {
return String(sql).includes('SELECT id, name, short_description, timezone, created_at, created_by, modified_at, modified_by') && String(sql).includes('FROM i_schedule_groups');
}));
});
test('timetable timezone migrations use timetable tables when legacy tables are gone', async () => {
const pool = createPool([
{
match(sql) {
return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?');
},
result: [[{ column_count: 0 }]]
},
{
match(_sql, params) {
return Array.isArray(params) && params[0] === 'i_timetable_groups';
},
result: [[{ table_count: 1 }]]
},
{
match(_sql, params) {
return Array.isArray(params) && params[0] === 'i_timetable_entries';
},
result: [[{ table_count: 1 }]]
},
{
match(_sql, params) {
return Array.isArray(params) && params[0] === 'i_schedule_groups';
},
result: [[{ table_count: 0 }]]
},
{
match(_sql, params) {
return Array.isArray(params) && params[0] === 'i_schedule_entries';
},
result: [[{ table_count: 0 }]]
},
{
match(sql) {
return sql.includes('SELECT id, start_datetime, end_datetime FROM i_timetable_entries ORDER BY id ASC');
},
result: [[{ id: 1, start_datetime: '2026-08-09 09:00:00', end_datetime: '2026-08-09 10:00:00' }]]
},
{
match(sql) {
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_groups');
},
result: [[{ max_id: 1 }]]
},
{
match(sql) {
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_entries');
},
result: [[{ max_id: 1 }]]
}
]);
await runMigrations(pool, { currentVersion: '2.6.15' });
assert.ok(pool.queries.some(function ([sql]) {
return String(sql).includes('ALTER TABLE i_timetable_groups ADD COLUMN timezone');
}));
assert.ok(pool.queries.some(function ([sql]) {
return String(sql).includes('UPDATE i_timetable_groups SET timezone = ?');
}));
assert.ok(pool.queries.some(function ([sql]) {
return String(sql).includes('UPDATE i_timetable_entries SET start_datetime = ?, end_datetime = ? WHERE id = ?');
}));
assert.ok(!pool.queries.some(function ([sql]) {
return String(sql).includes('i_schedule_groups') || String(sql).includes('i_schedule_entries');
}));
});
+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