Release v2.10.3
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
2026-08-29 01:56:45 +01:00
parent 30814f3f46
commit 3f4f57020a
60 changed files with 1075 additions and 523 deletions
+41 -45
View File
@@ -1,3 +1,5 @@
// Thin-client bridge for player registration, snapshots, commands, and heartbeats.
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
@@ -8,7 +10,6 @@ const common = require('../common');
const { verifyRequestAuth, createRequestAuthHeaders } = require('#src/request-auth');
const { normalizeDeviceId, upsertPlayerRegistration, recordPlayerHeartbeat } = require('#src/data/player-registry');
const { createPlayerPlaylistService } = require('../player/playlist');
const { buildThumbnailPreviewData } = require('../player/thumbnail-preview');
const { commitDeviceBinding, bindPlayerToScreen, getOnboardingStatus, getPlayerPublicBaseUrl } = require('../player/onboarding');
const { createStyledQrCodeSvg } = require('../data/qr-code');
const { verifyPageAuthToken } = require('#src/request-auth');
@@ -458,7 +459,21 @@ async function start() {
function storeScreenSnapshot(slug, connections, deviceIds) {
const key = String(slug || '').trim();
const normalizedConnections = Array.isArray(connections) ? connections : [];
const normalizedConnections = [];
const connectionIndexes = new Map();
(Array.isArray(connections) ? connections : []).forEach(function (connection) {
const identity = connection && typeof connection === 'object'
? [String(connection.deviceId || '').trim(), String(connection.clientId || '').trim()].join('|')
: '';
if (!identity || !connectionIndexes.has(identity)) {
if (identity) {
connectionIndexes.set(identity, normalizedConnections.length);
}
normalizedConnections.push(connection);
return;
}
normalizedConnections[connectionIndexes.get(identity)] = connection;
});
const normalizedDeviceIds = Array.isArray(deviceIds) ? deviceIds.map(function (value) {
return normalizeDeviceId(value);
}).filter(Boolean) : [];
@@ -654,6 +669,16 @@ async function start() {
}, response && typeof response === 'object' ? response : {});
}));
logBridge('Screen command result', {
screenSlug: slug,
command: command,
connectionId: connectionId || null,
targets: targetPlayers.map(function (target) { return target.deviceId; }),
results: results.map(function (result) {
return { playerIdentifier: result.playerIdentifier, ok: result.ok, status: result.status, error: result.error || null };
})
});
res.json({
ok: true,
screenSlug: slug,
@@ -882,48 +907,6 @@ async function start() {
}
});
app.get('/api/internal/slide-thumbnails/:id/preview', requireRequestAuth, async function (req, res, next) {
try {
const slide = await common.fetchSlideById(pool, Number(req.params.id));
if (!slide) {
return res.status(404).send('Slide not found');
}
const data = buildThumbnailPreviewData(slide);
if (typeof common.fetchRssFeedsData === 'function' && typeof common.fetchRssFeedItemsByFeedId === 'function') {
const rssData = await common.fetchRssFeedsData(pool);
data.rssFeeds = await Promise.all((rssData.rssFeeds || []).map(async function (feed) {
const items = await common.fetchRssFeedItemsByFeedId(pool, feed.id);
return Object.assign({}, feed, {
items: items.map(function (item) {
return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item;
})
});
}));
}
if (typeof common.fetchApiSourcesData === 'function') {
const apiData = await common.fetchApiSourcesData(pool);
data.apiSources = (apiData.apiSources || []).map(function (source) {
return Object.assign({}, source, {
responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null
});
});
}
if (typeof common.fetchTimetablesData === 'function') {
const timetableData = await common.fetchTimetablesData(pool);
data.timetableGroups = Array.isArray(timetableData && timetableData.timetableGroups) ? timetableData.timetableGroups : [];
}
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.send(common.renderPlayerPage('slide-thumbnail-preview-' + slide.id, data));
} catch (error) {
next(error);
}
});
app.post('/api/players/:deviceId/commands', requireRequestAuth, async function (req, res, next) {
try {
const deviceId = normalizeDeviceId(req.params.deviceId);
@@ -989,7 +972,14 @@ async function start() {
return;
}
setScreenSnapshotSource(slug, deviceId, Array.isArray(payload.connections) ? payload.connections : []);
const snapshotPlayerPublicBaseUrl = String(payload.playerPublicBaseUrl || socket.publicBaseUrl || '').trim().replace(/\/$/, '');
const connections = Array.isArray(payload.connections) ? payload.connections.map(function (connection) {
if (!connection || typeof connection !== 'object' || !snapshotPlayerPublicBaseUrl) {
return connection;
}
return Object.assign({}, connection, { playerPublicBaseUrl: snapshotPlayerPublicBaseUrl });
}) : [];
setScreenSnapshotSource(slug, deviceId, connections);
return;
}
@@ -1021,6 +1011,12 @@ async function start() {
publicBaseUrl: payload.publicBaseUrl,
internalBaseUrl: payload.internalBaseUrl
});
if (typeof common.touchOnboardingDeviceLastSeen === 'function') {
const onboardingClientIds = (Array.isArray(payload.connections) ? payload.connections : []).map(function (connection) {
return connection && connection.clientId;
});
await common.touchOnboardingDeviceLastSeen(pool, onboardingClientIds);
}
socket.send(JSON.stringify({ type: 'heartbeat-ack', ok: true, player: player }));
return;