Release 2.8.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m16s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
2026-08-16 22:08:32 +01:00
parent 203d0bfc01
commit a5bf8e6f7f
21 changed files with 227 additions and 60 deletions
+11 -3
View File
@@ -165,11 +165,19 @@ async function saveAppSettings(pool, settings, modifiedBy) {
if (!Object.prototype.hasOwnProperty.call(inputSettings, definition.key)) {
continue;
}
await connection.query(
`UPDATE o_app_settings
SET setting_value = ?, modified_by = ?
WHERE setting_key = ?`,
[JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, definition.key]
);
await connection.query(
`INSERT INTO o_app_settings (setting_key, setting_value, created_by, modified_by)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), modified_by = VALUES(modified_by)`,
[definition.key, JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, modifiedBy || null]
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM o_app_settings WHERE setting_key = ?
)`,
[definition.key, JSON.stringify(normalizedSettings[definition.key]), modifiedBy || null, modifiedBy || null, definition.key]
);
}
if (typeof connection.commit === 'function') {
+25 -14
View File
@@ -92,15 +92,19 @@ async function upsertPlayerRegistration(pool, options) {
return null;
}
await pool.query(
`UPDATE d_players
SET public_base_url = ?, internal_base_url = ?, last_seen_at = CURRENT_TIMESTAMP, modified_at = CURRENT_TIMESTAMP
WHERE identifier = ?`,
[publicBaseUrl || null, internalBaseUrl || null, identifier]
);
await pool.query(
`INSERT INTO d_players (identifier, public_base_url, internal_base_url, last_seen_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
public_base_url = VALUES(public_base_url),
internal_base_url = VALUES(internal_base_url),
last_seen_at = CURRENT_TIMESTAMP,
modified_at = CURRENT_TIMESTAMP`,
[identifier, publicBaseUrl || null, internalBaseUrl || null]
SELECT ?, ?, ?, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM d_players WHERE identifier = ?
)`,
[identifier, publicBaseUrl || null, internalBaseUrl || null, identifier]
);
return resolvePlayerRegistration(pool, identifier);
@@ -115,15 +119,22 @@ async function recordPlayerHeartbeat(pool, options) {
return null;
}
await pool.query(
`UPDATE d_players
SET public_base_url = COALESCE(?, public_base_url),
internal_base_url = COALESCE(?, internal_base_url),
last_seen_at = CURRENT_TIMESTAMP,
modified_at = CURRENT_TIMESTAMP
WHERE identifier = ?`,
[publicBaseUrl || null, internalBaseUrl || null, identifier]
);
await pool.query(
`INSERT INTO d_players (identifier, public_base_url, internal_base_url, last_seen_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
public_base_url = COALESCE(VALUES(public_base_url), public_base_url),
internal_base_url = COALESCE(VALUES(internal_base_url), internal_base_url),
last_seen_at = CURRENT_TIMESTAMP,
modified_at = CURRENT_TIMESTAMP`,
[identifier, publicBaseUrl || null, internalBaseUrl || null]
SELECT ?, ?, ?, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM d_players WHERE identifier = ?
)`,
[identifier, publicBaseUrl || null, internalBaseUrl || null, identifier]
);
return resolvePlayerRegistration(pool, identifier);
+34 -9
View File
@@ -15,11 +15,19 @@ async function bootstrapDatabase(pool) {
}
for (const permission of PERMISSIONS) {
await pool.query(
`UPDATE a_permissions
SET name = ?, section_name = ?, description = ?, modified_by = ?
WHERE permission_key = ?`,
[permission.name, permission.sectionName, permission.description || null, null, permission.key]
);
await pool.query(
`INSERT INTO a_permissions (permission_key, name, section_name, description, created_by, modified_by)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE name = VALUES(name), section_name = VALUES(section_name), description = VALUES(description), modified_by = VALUES(modified_by)` ,
[permission.key, permission.name, permission.sectionName, permission.description || null, null, null]
SELECT ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM a_permissions WHERE permission_key = ?
)`,
[permission.key, permission.name, permission.sectionName, permission.description || null, null, null, permission.key]
);
}
@@ -35,20 +43,37 @@ async function bootstrapDatabase(pool) {
);
}
const [legacyRoleRows] = await pool.query('SELECT id FROM a_roles WHERE role_key = ? LIMIT 1', ['administrators']);
const [currentRoleRows] = await pool.query('SELECT id FROM a_roles WHERE role_key = ? LIMIT 1', [DEFAULT_ROLE.key]);
if (legacyRoleRows.length && !currentRoleRows.length) {
await pool.query(
`UPDATE a_roles
SET role_key = ?, name = ?, description = ?, modified_by = ?
WHERE role_key = ?`,
[DEFAULT_ROLE.key, DEFAULT_ROLE.name, DEFAULT_ROLE.description || null, null, 'administrators']
);
}
await pool.query(
`INSERT INTO a_roles (role_key, name, description, created_by, modified_by)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description), modified_by = VALUES(modified_by)`,
[DEFAULT_ROLE.key, DEFAULT_ROLE.name, DEFAULT_ROLE.description || null, null, null]
SELECT ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM a_roles WHERE role_key = ?
)`,
[DEFAULT_ROLE.key, DEFAULT_ROLE.name, DEFAULT_ROLE.description || null, null, null, DEFAULT_ROLE.key]
);
const [defaultRoleRows] = await pool.query('SELECT id FROM a_roles WHERE role_key = ? LIMIT 1', [DEFAULT_ROLE.key]);
const defaultRoleId = defaultRoleRows.length ? Number(defaultRoleRows[0].id) : null;
if (defaultRoleId) {
await pool.query(
`INSERT IGNORE INTO a_role_permissions (role_id, permission_id, created_by, modified_by)
SELECT ?, id, NULL, NULL FROM a_permissions`,
[defaultRoleId]
`INSERT INTO a_role_permissions (role_id, permission_id, created_by, modified_by)
SELECT ?, permissions.id, NULL, NULL
FROM a_permissions permissions
LEFT JOIN a_role_permissions existing
ON existing.role_id = ? AND existing.permission_id = permissions.id
WHERE existing.id IS NULL`,
[defaultRoleId, defaultRoleId]
);
}
+8 -3
View File
@@ -538,7 +538,7 @@ async function detectSchemaVersion(pool) {
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
const scheduleEntriesExists = await tableExists(pool, 'i_schedule_entries');
if ((timetableGroupsExists || timetableEntriesExists) && !scheduleGroupsExists && !scheduleEntriesExists) {
if (timetableGroupsExists && timetableEntriesExists && !scheduleGroupsExists && !scheduleEntriesExists) {
return '2.6.18';
}
@@ -556,9 +556,14 @@ async function recordSchemaVersion(pool, version) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
);
const stateValue = String(version || appVersion || '0.0.0').trim();
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()]
'UPDATE ' + APP_STATE_TABLE + ' SET state_value = ? WHERE state_key = ?',
[stateValue, APP_STATE_SCHEMA_VERSION_KEY]
);
await pool.query(
'INSERT INTO ' + APP_STATE_TABLE + ' (state_key, state_value) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM ' + APP_STATE_TABLE + ' WHERE state_key = ?)',
[APP_STATE_SCHEMA_VERSION_KEY, stateValue, APP_STATE_SCHEMA_VERSION_KEY]
);
}
+12 -2
View File
@@ -128,8 +128,18 @@ async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNam
}
await pool.query(
'INSERT INTO d_onboarding_devices (device_id, client_name, screen_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE client_name = VALUES(client_name), screen_id = VALUES(screen_id), modified_at = CURRENT_TIMESTAMP',
[normalizedDeviceId, normalizedClientName, screen.id]
`UPDATE d_onboarding_devices
SET client_name = ?, screen_id = ?, modified_at = CURRENT_TIMESTAMP
WHERE device_id = ?`,
[normalizedClientName, screen.id, normalizedDeviceId]
);
await pool.query(
`INSERT INTO d_onboarding_devices (device_id, client_name, screen_id)
SELECT ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM d_onboarding_devices WHERE device_id = ?
)`,
[normalizedDeviceId, normalizedClientName, screen.id, normalizedDeviceId]
);
return getOnboardingStatus(pool, normalizedDeviceId);
+2 -2
View File
@@ -236,8 +236,8 @@ const PERMISSIONS = PERMISSION_SECTIONS.flatMap(function (section, sectionIndex)
});
const DEFAULT_ROLE = {
key: 'administrators',
name: 'Administrators',
key: 'super-admin',
name: 'Super Admin',
description: 'Full access to the admin interface.'
};
+25 -3
View File
@@ -236,7 +236,14 @@ async function syncUserRoles(pool, userId, roleIds) {
await pool.query('DELETE FROM a_user_roles WHERE user_id = ?', [userId]);
for (const roleId of uniqueRoleIds) {
await pool.query('INSERT IGNORE INTO a_user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
await pool.query(
`INSERT INTO a_user_roles (user_id, role_id, created_by, modified_by)
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM a_user_roles WHERE user_id = ? AND role_id = ?
)`,
[userId, roleId, null, null, userId, roleId]
);
}
}
@@ -249,7 +256,14 @@ async function syncRoleUsers(pool, roleId, userIds) {
await pool.query('DELETE FROM a_user_roles WHERE role_id = ?', [roleId]);
for (const userId of uniqueUserIds) {
await pool.query('INSERT IGNORE INTO a_user_roles (user_id, role_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [userId, roleId, null, null]);
await pool.query(
`INSERT INTO a_user_roles (user_id, role_id, created_by, modified_by)
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM a_user_roles WHERE user_id = ? AND role_id = ?
)`,
[userId, roleId, null, null, userId, roleId]
);
}
}
@@ -268,7 +282,15 @@ async function syncRolePermissions(pool, roleId, permissionKeys) {
await pool.query('DELETE FROM a_role_permissions WHERE role_id = ?', [roleId]);
for (const permissionRow of permissionRows) {
await pool.query('INSERT IGNORE INTO a_role_permissions (role_id, permission_id, created_by, modified_by) VALUES (?, ?, ?, ?)', [roleId, Number(permissionRow.id), null, null]);
const permissionId = Number(permissionRow.id);
await pool.query(
`INSERT INTO a_role_permissions (role_id, permission_id, created_by, modified_by)
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM a_role_permissions WHERE role_id = ? AND permission_id = ?
)`,
[roleId, permissionId, null, null, roleId, permissionId]
);
}
}
+4 -3
View File
@@ -1,4 +1,5 @@
// Admin RBAC route registration and permission management.
const { DEFAULT_ROLE } = require('#src/rbac');
module.exports = function registerRbacRoutes(app, deps) {
const pool = deps.pool;
@@ -359,7 +360,7 @@
excludeUserId: currentUserId
});
const users = mapUsersForView(data.users, viewModel.selectedUserIds);
viewModel.role.inUse = String(viewModel.role.role_key || '') === 'administrators' || Number(viewModel.role.user_count) > 0;
viewModel.role.inUse = String(viewModel.role.role_key || '') === DEFAULT_ROLE.key || String(viewModel.role.role_key || '') === 'administrators' || Number(viewModel.role.user_count) > 0;
res.send(pages.renderRbacEditPage(viewModel.role, req.query.message ? String(req.query.message) : '', req.currentUser, viewModel.permissionGroups, users, buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'users', 'User pages')));
} catch (error) {
@@ -580,8 +581,8 @@
if (!role) {
return res.status(404).send('Role not found.');
}
if (String(role.role_key || '') === 'administrators') {
return res.redirect('/settings/roles?message=' + encodeURIComponent('The built-in Administrators role cannot be deleted.'));
if (String(role.role_key || '') === DEFAULT_ROLE.key || String(role.role_key || '') === 'administrators') {
return res.redirect('/settings/roles?message=' + encodeURIComponent('The built-in Super Admin role cannot be deleted.'));
}
if (Number(role.user_count) > 0) {
return res.redirect('/settings/roles?message=' + encodeURIComponent('Remove all users from this role before deleting it.'));
@@ -124,6 +124,9 @@ function buildRbacAddViewModel(message, currentUser, formValues, permissionGroup
}
function buildRbacEditViewModel(role, message, currentUser, permissionGroups, users, pagination) {
const roleKey = String(role && role.role_key || '').trim();
const isProtectedRole = roleKey === 'super-admin' || roleKey === 'administrators';
const deleteDisabled = isProtectedRole || Boolean(role && role.inUse);
return {
title: 'Edit role',
active: 'rbac',
@@ -135,13 +138,16 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
footerCancelUrl: '/settings/roles',
cancelConfirmMessage: "You've made changes. Are you sure you want to leave this page?",
footerDeleteUrl: '/settings/roles/' + Number(role && role.id) + '/delete',
footerDeleteDisabled: Boolean(role && role.inUse),
footerDeleteTitle: role && role.inUse ? 'Delete is disabled while this role is assigned to users.' : '',
footerDeleteDisabled: deleteDisabled,
footerDeleteConfirmMessage: 'Delete ' + String(role && role.name || 'this role') + '?',
footerDeleteTitle: isProtectedRole
? 'The built-in Super Admin role cannot be deleted.'
: (role && role.inUse ? 'Delete is disabled while this role is assigned to users.' : ''),
footerShowDelete: true,
message: message,
currentUser: currentUser || null,
role: role,
inUse: Boolean(role && role.inUse),
role: Object.assign({}, role, { isProtected: isProtectedRole }),
inUse: deleteDisabled,
permissionGroups: permissionGroups || [],
permissionSections: buildPermissionSections(permissionGroups),
users: users || [],
+17 -6
View File
@@ -159,12 +159,23 @@ module.exports = function registerAnnouncementRoutes(app, deps) {
}
if (selectedIds.length) {
await pool.query(
'INSERT INTO d_announcement_screens (announcement_id, screen_id, created_by, modified_by) VALUES ? ON DUPLICATE KEY UPDATE modified_at = CURRENT_TIMESTAMP, modified_by = VALUES(modified_by)',
[selectedIds.map(function (screenId) {
return [announcementId, screenId, actorId || null, actorId || null];
})]
);
for (const screenId of selectedIds) {
await pool.query(
`UPDATE d_announcement_screens
SET modified_at = CURRENT_TIMESTAMP, modified_by = ?
WHERE announcement_id = ? AND screen_id = ?`,
[actorId || null, announcementId, screenId]
);
await pool.query(
`INSERT INTO d_announcement_screens (announcement_id, screen_id, created_by, modified_by)
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM d_announcement_screens
WHERE announcement_id = ? AND screen_id = ?
)`,
[announcementId, screenId, actorId || null, actorId || null, announcementId, screenId]
);
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@
</div>
<div class="card-footer d-flex justify-content-end flex-wrap">
<div class="btn-group" role="group" aria-label="Role actions">
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl cancelConfirmMessage=cancelConfirmMessage deleteUrl=footerDeleteUrl deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showDelete=footerShowDelete}}}
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl cancelConfirmMessage=cancelConfirmMessage deleteUrl=footerDeleteUrl deleteConfirmMessage=footerDeleteConfirmMessage deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showDelete=footerShowDelete}}}
</div>
</div>
</div>