Release 1.5.8
Publish Docker Image / build-and-push (push) Successful in 1m18s

This commit is contained in:
2026-07-25 18:24:12 +01:00
parent 04ddec50ad
commit d24c9c4035
6 changed files with 42 additions and 212 deletions
+7
View File
@@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file.
- No unreleased changes recorded yet.
## 1.5.8 - 2026-07-25
### Fixed
- Slide thumbnail refresh now uses the player-rendered preview again and signs the preview GET request in the same way the player verifies it.
- Background tasks settings page and client script were repaired after a syntax break so the web app can boot cleanly.
## 1.5.7 - 2026-07-25
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage",
"version": "1.5.7",
"version": "1.5.8",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+4 -121
View File
@@ -120,124 +120,6 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
return buildTextRegionMarkup(region, regionContent);
}
function buildSlideHtml(slide, baseUrl) {
const canvasSize = getCanvasSize(slide);
const template = slide && slide.template ? slide.template : null;
const backgroundColor = sanitizeTextColor(template && template.background_color ? template.background_color : '#111111', '#111111');
const backgroundImagePath = template && template.background_image_path ? resolveAssetUrl(baseUrl, template.background_image_path) : '';
if (!template) {
const mediaPath = String(slide && slide.media_path || '').trim();
const mediaUrl = resolveAssetUrl(baseUrl, mediaPath);
const kind = mediaKind(mediaPath || slide.media_type || '');
const mediaMarkup = kind === 'image' && mediaUrl
? '<img src="' + escapeHtml(mediaUrl) + '" alt="' + escapeHtml(slide.title || 'slide') + '" />'
: '';
return [
'<!doctype html>',
'<html>',
' <head>',
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=' + canvasSize.width + ', initial-scale=1" />',
' <link rel="stylesheet" href="' + escapeHtml(baseUrl + '/assets/css/player.css') + '" />',
' <style>',
' html, body { margin: 0; width: ' + canvasSize.width + 'px; height: ' + canvasSize.height + 'px; overflow: hidden; background: #111111; }',
' body { display: flex; align-items: stretch; justify-content: stretch; }',
' .thumbnail-stage { position: relative; width: ' + canvasSize.width + 'px; height: ' + canvasSize.height + 'px; overflow: hidden; background: ' + escapeHtml(backgroundColor) + '; }',
' .thumbnail-stage .template-region.text { color: #fff; display: block; padding: 0; text-align: left; white-space: normal; word-break: break-word; line-height: 1.35; }',
' .thumbnail-stage .template-region.text .template-region-text-scale { display: block; transform-origin: top left; width: 100%; height: 100%; white-space: normal; word-break: break-word; line-height: 1.35; text-align: left; overflow: hidden; }',
' .thumbnail-stage .template-region.text .template-region-text-scale > * { margin: 0; }',
' .thumbnail-stage .template-region.text .template-region-text-scale > * + * { margin-top: 0.5em; }',
' .thumbnail-stage .template-region.text .template-region-text-scale ul,',
' .thumbnail-stage .template-region.text .template-region-text-scale ol { padding-left: 1.2em; }',
' .thumbnail-stage .template-region.image img { width: 100%; height: 100%; object-fit: contain; display: block; }',
' .thumbnail-stage .template-region.webpage iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; overflow: hidden; }',
' .thumbnail-stage .template-region.html iframe { width: 100%; height: 100%; border: 0; display: block; background: transparent; overflow: hidden; }',
' .thumbnail-stage .template-region.rtmp { background: #000; }',
' .thumbnail-stage .template-region.rtmp video { width: 100%; height: 100%; object-fit: contain; display: block; pointer-events: none; }',
' .thumbnail-stage .template-region-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.06); font-size: 0.9rem; }',
' .thumbnail-stage .template-region-rtmp-placeholder { position: absolute; inset: 0; }',
' </style>',
' </head>',
' <body>',
' <div class="thumbnail-stage">',
mediaMarkup ? '<div class="slide"><div class="slide-canvas slide-media" style="width:' + canvasSize.width + 'px;height:' + canvasSize.height + 'px;">' + mediaMarkup + '</div></div>' : '',
' </div>',
' </body>',
'</html>'
].join('\n');
}
const regions = Array.isArray(template && template.regions) ? template.regions.slice().sort(function (left, right) {
return Number(left.z_index || 0) - Number(right.z_index || 0) || Number(left.id || 0) - Number(right.id || 0);
}) : [];
const regionsHtml = regions.map(function (region) {
const content = getRegionContent(slide, region);
const left = Math.max(0, Number(region.x || 0));
const top = Math.max(0, Number(region.y || 0));
const width = Math.max(1, Number(region.width || 1));
const height = Math.max(1, Number(region.height || 1));
const regionType = String(region.region_type || 'text').toLowerCase();
const pixelWidth = Math.max(1, Math.round(Number(region.width || 0) || 1));
const pixelHeight = Math.max(1, Math.round(Number(region.height || 0) || 1));
const innerHtml = regionType === 'text'
? buildTextRegionMarkup(Object.assign({}, region, { baseStyle: 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region.z_index || 0) + ';', pixelWidth: pixelWidth, pixelHeight: pixelHeight }), content)
: buildRegionInnerHtml(Object.assign({}, region, { baseStyle: 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region.z_index || 0) + ';', pixelWidth: pixelWidth, pixelHeight: pixelHeight }), content, baseUrl);
if (!innerHtml) {
return '';
}
return [
'<div class="template-region ' + escapeHtml(regionType) + '" style="left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region.z_index || 0) + ';">',
innerHtml,
'</div>'
].join('');
}).join('');
return [
'<!doctype html>',
'<html>',
' <head>',
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=' + canvasSize.width + ', initial-scale=1" />',
' <link rel="stylesheet" href="' + escapeHtml(baseUrl + '/assets/css/player.css') + '" />',
' <style>',
' html, body { margin: 0; width: ' + canvasSize.width + 'px; height: ' + canvasSize.height + 'px; overflow: hidden; background: #111111; }',
' body { display: flex; align-items: stretch; justify-content: stretch; }',
' .thumbnail-stage { position: relative; width: ' + canvasSize.width + 'px; height: ' + canvasSize.height + 'px; overflow: hidden; background: ' + escapeHtml(backgroundColor) + '; }',
' .thumbnail-stage .template-stage { position: relative; width: 100%; height: 100%; }',
' .thumbnail-stage .template-stage .template-background { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; display: block; z-index: 0; }',
' .thumbnail-stage .template-region { position: absolute; overflow: hidden; box-sizing: border-box; }',
' .thumbnail-stage .template-region.image img { width: 100%; height: 100%; object-fit: contain; display: block; }',
' .thumbnail-stage .template-region.webpage iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; overflow: hidden; }',
' .thumbnail-stage .template-region.html iframe { width: 100%; height: 100%; border: 0; display: block; background: transparent; overflow: hidden; }',
' .thumbnail-stage .template-region.rtmp { background: #000; }',
' .thumbnail-stage .template-region.rtmp video { width: 100%; height: 100%; object-fit: contain; display: block; pointer-events: none; }',
' .thumbnail-stage .template-region.text { color: #fff; display: block; padding: 0; text-align: left; white-space: normal; word-break: break-word; line-height: 1.35; }',
' .thumbnail-stage .template-region.text .template-region-text-scale { display: block; transform-origin: top left; width: 100%; height: 100%; white-space: normal; word-break: break-word; line-height: 1.35; text-align: left; overflow: hidden; }',
' .thumbnail-stage .template-region.text .template-region-text-scale > * { margin: 0; }',
' .thumbnail-stage .template-region.text .template-region-text-scale > * + * { margin-top: 0.5em; }',
' .thumbnail-stage .template-region.text .template-region-text-scale ul,',
' .thumbnail-stage .template-region.text .template-region-text-scale ol { padding-left: 1.2em; }',
' .thumbnail-stage .template-region-rtmp-placeholder { position: absolute; inset: 0; }',
' .thumbnail-stage .template-region-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.06); font-size: 0.9rem; }',
' </style>',
' </head>',
' <body>',
' <div class="thumbnail-stage">',
' <div class="template-stage" style="background-color:' + escapeHtml(backgroundColor) + ';">',
backgroundImagePath ? ' <img class="template-background" src="' + escapeHtml(backgroundImagePath) + '" alt="" />' : '',
regionsHtml,
' </div>',
' </div>',
' </body>',
'</html>'
].join('\n');
}
async function launchBrowser() {
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
return fs.existsSync(candidate);
@@ -312,12 +194,13 @@ async function captureSlideThumbnail(options) {
const previewUrl = baseUrl + previewPath;
await page.setExtraHTTPHeaders(createRequestAuthHeaders({
method: 'GET',
pathname: previewPath,
body: {}
pathname: previewPath
}));
await page.setViewport(PLAYER_VIEWPORT);
await page.goto(previewUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('.slide-canvas', { timeout: 30000, visible: true });
await page.waitForFunction(function () {
return Boolean(document.querySelector('.slide-canvas'));
}, { timeout: 30000 });
const canvas = await page.$('.slide-canvas');
if (!canvas) {
throw new Error('Player render did not produce a slide canvas.');
@@ -174,26 +174,6 @@
};
}
function findFilterLink(target) {
if (!target || !target.closest) {
return null;
}
var link = target.closest('a[data-background-tasks-nav]');
if (!link) {
return null;
}
var href = String(link.getAttribute('href') || '').trim();
if (!href || href === '#') {
return null;
}
return {
href: href
};
}
function initPaginationNavigation() {
document.addEventListener('click', function (event) {
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
@@ -217,24 +197,6 @@
loadPage(nextUrl.toString(), { sectionNames: [paginationLink.sectionName] });
return;
}
var filterLink = findFilterLink(event.target);
if (!filterLink) {
return;
}
var filterUrl = getPageUrl(filterLink.href);
if (!filterUrl) {
return;
}
if (filterUrl.pathname === window.location.pathname && filterUrl.search === window.location.search && filterUrl.hash === window.location.hash) {
event.preventDefault();
return;
}
event.preventDefault();
loadPage(filterUrl.toString(), { sectionNames: ['tasks'] });
}, true);
window.addEventListener('popstate', function () {
@@ -2,7 +2,6 @@ const { renderView } = require('../../view');
const { hasAnyPermission } = require('../../../rbac');
const TASKS_PER_PAGE = 10;
const TASK_STATUSES = new Set(['queued', 'running', 'failed', 'completed', 'canceled']);
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
@@ -22,11 +21,6 @@ function parsePageNumber(value) {
return Math.max(1, pageNumber);
}
function normalizeStatusFilter(value) {
const status = String(value || '').trim().toLowerCase();
return TASK_STATUSES.has(status) ? status : '';
}
function normalizeSourceFilter(sourceType, sourceId) {
const normalizedSourceType = String(sourceType || '').trim();
const normalizedSourceId = Math.floor(Number(sourceId) || 0);
@@ -92,11 +86,7 @@ function buildPagination(totalItems, currentPage, pageParam, queryState) {
};
}
function taskMatchesFilters(task, statusFilter, sourceFilter) {
if (statusFilter && task.status !== statusFilter) {
return false;
}
function taskMatchesFilters(task, sourceFilter) {
if (!sourceFilter || !sourceFilter.sourceType || !sourceFilter.sourceId) {
return true;
}
@@ -106,12 +96,26 @@ function taskMatchesFilters(task, statusFilter, sourceFilter) {
}
function formatStatusLabel(status) {
const normalizedStatus = String(status || '').trim().toLowerCase();
if (!normalizedStatus) {
return 'All tasks';
const normalized = String(status || '').trim().toLowerCase();
if (!normalized) {
return '';
}
return normalizedStatus.charAt(0).toUpperCase() + normalizedStatus.slice(1);
if (normalized === 'queued') {
return 'Queued';
}
if (normalized === 'running') {
return 'Running';
}
if (normalized === 'completed') {
return 'Completed';
}
if (normalized === 'failed') {
return 'Failed';
}
if (normalized === 'canceled') {
return 'Canceled';
}
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
@@ -133,17 +137,6 @@ function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
return buildQueryString(nextQuery);
}
function buildStatusFilterUrl(queryState, statusFilter) {
const nextQuery = Object.assign({}, queryState, {
page: 1,
status: statusFilter && statusFilter.status === statusFilter.targetStatus ? '' : statusFilter.targetStatus,
sourceType: statusFilter && statusFilter.sourceType ? statusFilter.sourceType : '',
sourceId: statusFilter && statusFilter.sourceId ? statusFilter.sourceId : ''
});
return buildQueryString(nextQuery);
}
module.exports = function renderBackgroundTasksPage(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const recurringTasks = (data && data.recurringTasks) || [];
@@ -152,16 +145,14 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
const queryState = {
page: parsePageNumber(data && data.page),
recurringPage: parsePageNumber(data && data.recurringPage),
status: normalizeStatusFilter(data && data.status),
sourceType: '',
sourceId: ''
};
const sourceFilter = normalizeSourceFilter(data && data.sourceType, data && data.sourceId);
queryState.status = normalizeStatusFilter(data && data.status);
queryState.sourceType = sourceFilter.sourceType;
queryState.sourceId = sourceFilter.sourceId;
const filteredTasks = tasks.filter(function (task) {
return taskMatchesFilters(task, queryState.status, sourceFilter);
return taskMatchesFilters(task, sourceFilter);
});
const pagination = buildPagination(filteredTasks.length, queryState.page, 'page', queryState);
const recurringPagination = buildPagination(recurringTasks.length, queryState.recurringPage, 'recurringPage', queryState);
@@ -193,18 +184,8 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
variant: 'success'
}
].map(function (card) {
const isActive = queryState.status === card.status;
const statusFilter = {
status: queryState.status,
sourceType: queryState.sourceType,
sourceId: queryState.sourceId,
targetStatus: card.status
};
return Object.assign({}, card, {
active: isActive,
href: buildStatusFilterUrl(queryState, statusFilter),
ariaLabel: isActive ? `Clear ${card.label.toLowerCase()} filter` : `Filter tasks by ${card.label.toLowerCase()}`
active: false
});
});
@@ -216,7 +197,7 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : ''
});
});
const hasActiveFilters = Boolean(queryState.status || queryState.sourceType || queryState.sourceId);
const hasActiveFilters = Boolean(queryState.sourceType || queryState.sourceId);
return renderView('settings/background-tasks/index', {
title: 'Background tasks',
@@ -233,7 +214,6 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
}),
summary: summary,
statusCards: statusCards,
activeStatusLabel: formatStatusLabel(queryState.status),
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
clearFiltersUrl: buildQueryString({ recurringPage: queryState.recurringPage }),
hasActiveFilters: hasActiveFilters,
@@ -3,7 +3,7 @@
<div class="page-header">
<div>
<h2>Background tasks</h2>
<p>Queued data refreshes run in the web process. Finished items stay here until you clear them. Click a status box or source name to filter the results.</p>
<p>Queued data refreshes run in the web process. Finished items stay here until you clear them.</p>
</div>
</div>
@@ -11,7 +11,7 @@
{{#if hasActiveFilters}}
<div class="alert alert-light border d-flex flex-wrap align-items-center justify-content-between gap-2 py-2 mb-3">
<div class="small text-muted">
Showing results for {{#if activeStatusLabel}}{{activeStatusLabel}}{{else}}all tasks{{/if}}{{#if activeSourceLabel}} from {{activeSourceLabel}}{{/if}}.
{{#if activeSourceLabel}}Showing results from {{activeSourceLabel}}.{{/if}}
</div>
<a class="btn btn-outline-secondary btn-sm" href="{{clearFiltersUrl}}" data-background-tasks-nav="clear">Clear filters</a>
</div>
@@ -21,14 +21,12 @@
<div id="background-tasks-summary" class="row g-3 mb-4">
{{#each statusCards}}
<div class="col-6 col-xl-3">
<a href="{{href}}" class="d-block h-100 text-decoration-none text-body" data-background-tasks-nav="status" aria-label="{{ariaLabel}}"{{#if active}} aria-current="page"{{/if}}>
<div class="card card-outline card-{{variant}} h-100{{#if active}} border-2 border-{{variant}}{{/if}}">
<div class="card-body">
<div class="text-muted small text-uppercase">{{label}}</div>
<div class="fs-3 fw-semibold">{{count}}</div>
</div>
<div class="card card-outline card-{{variant}} h-100{{#if active}} border-2 border-{{variant}}{{/if}}">
<div class="card-body">
<div class="text-muted small text-uppercase">{{label}}</div>
<div class="fs-3 fw-semibold">{{count}}</div>
</div>
</a>
</div>
</div>
{{/each}}
</div>