Add player control-plane and dashboard updates

This commit is contained in:
2026-08-07 22:35:27 +01:00
parent a4f8a807ff
commit 2bd47fb96a
36 changed files with 5081 additions and 263 deletions
+49 -20
View File
@@ -1,6 +1,7 @@
// Screen route registration and dashboard wiring.
const fs = require('fs');
const { screenPlayerUrl } = require('../../../routes/common');
module.exports = function registerScreensRoutes(app, deps) {
const pool = deps.pool;
@@ -11,6 +12,14 @@ module.exports = function registerScreensRoutes(app, deps) {
const { buildPagination } = require('../../../lib/pagination');
const requirePermission = deps.requirePermission;
function isRecentPlayerRegistration(player, staleSeconds) {
const lastSeenAt = player && player.last_seen_at;
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
const LIST_PAGE_SIZE = 25;
const batTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.bat');
const shTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.sh');
@@ -19,6 +28,17 @@ module.exports = function registerScreensRoutes(app, deps) {
linux: '/downloads/kiosk/pulse-signage-kiosk.sh'
};
function normalizeTargetPlayerUrl(value) {
const normalized = String(value || '').trim().replace(/\/$/, '');
if (!normalized) {
return null;
}
if (!/^https?:\/\//i.test(normalized)) {
return null;
}
return normalized;
}
function requireQueryPermission(readPermissionKey, editPermissionKey) {
return function (req, res, next) {
const permissionKey = req.query && req.query.edit ? editPermissionKey : readPermissionKey;
@@ -58,7 +78,6 @@ module.exports = function registerScreensRoutes(app, deps) {
return Object.assign({}, screen, {
player_connection_count: playerConnectionCount,
player_url: dashboardScreen && dashboardScreen.player_url ? dashboardScreen.player_url : screen.player_url || null
});
});
}
@@ -71,6 +90,26 @@ module.exports = function registerScreensRoutes(app, deps) {
return common.fetchPlayerPublicBaseUrl(pool);
}
async function buildScreenPlayerUrls(screen) {
const playerRegistrations = typeof common.fetchPlayerRegistrations === 'function'
? await common.fetchPlayerRegistrations(pool)
: [];
return (Array.isArray(playerRegistrations) ? playerRegistrations : []).filter(function (player) {
return isRecentPlayerRegistration(player, 60);
}).map(function (player) {
const baseUrl = String(player && player.public_base_url || '').trim().replace(/\/$/, '');
const playerUrl = screenPlayerUrl(screen && screen.slug ? screen.slug : '', baseUrl);
return {
identifier: String(player && player.identifier || '').trim(),
public_base_url: baseUrl || null,
player_url: playerUrl || null
};
}).sort(function (left, right) {
return String(left && left.identifier || '').localeCompare(String(right && right.identifier || ''), undefined, { sensitivity: 'base', numeric: true });
});
}
function buildLauncherContent(templatePath, playerUrl) {
const template = fs.readFileSync(templatePath, 'utf8');
const normalizedPlayerUrl = String(playerUrl || '').trim();
@@ -100,8 +139,8 @@ module.exports = function registerScreensRoutes(app, deps) {
if (!screen) {
return res.status(404).send('Screen not found');
}
screen.player_url = await buildPlayerUrl();
if (screen.player_url) {
screen.player_urls = await buildScreenPlayerUrls(screen);
if (screen.player_urls.length) {
screen.launcher_downloads = launcherDownloadPaths;
}
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
@@ -113,18 +152,9 @@ module.exports = function registerScreensRoutes(app, deps) {
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const dashboardState = await buildDashboardState(pool);
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
? await common.fetchScreenPlayerUrls(pool)
: {};
const data = await common.fetchScreensPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
res.send(pages.renderScreensPage({
screens: applyConnectionCounts(data.screens || [], dashboardState.screens || []).map(function (screen) {
const slug = String(screen && screen.slug || '').trim();
const registryUrl = slug && playerUrlsBySlug[slug] ? String(playerUrlsBySlug[slug]).trim() : '';
return Object.assign({}, screen, {
player_url: screen.player_url || registryUrl || null
});
}),
screens: applyConnectionCounts(data.screens || [], dashboardState.screens || []),
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'screens', 'Screen pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
@@ -138,11 +168,8 @@ module.exports = function registerScreensRoutes(app, deps) {
if (!screen) {
return res.status(404).send('Screen not found');
}
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
? await common.fetchScreenPlayerUrls(pool)
: {};
screen.player_url = String(playerUrlsBySlug[screen.slug] || '').trim() || null;
if (screen.player_url) {
screen.player_urls = await buildScreenPlayerUrls(screen);
if (screen.player_urls.length) {
screen.launcher_downloads = launcherDownloadPaths;
}
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
@@ -155,7 +182,8 @@ module.exports = function registerScreensRoutes(app, deps) {
app.get('/downloads/kiosk/pulse-signage-kiosk.bat', requirePermission('screens.update'), async function (_req, res, next) {
try {
const playerUrl = await buildPlayerUrl();
const playerUrl = normalizeTargetPlayerUrl(_req.query && _req.query.playerUrl)
|| await buildPlayerUrl();
if (!playerUrl) {
return res.status(404).send('Player URL is not available yet.');
}
@@ -173,7 +201,8 @@ module.exports = function registerScreensRoutes(app, deps) {
app.get('/downloads/kiosk/pulse-signage-kiosk.sh', requirePermission('screens.update'), async function (_req, res, next) {
try {
const playerUrl = await buildPlayerUrl();
const playerUrl = normalizeTargetPlayerUrl(_req.query && _req.query.playerUrl)
|| await buildPlayerUrl();
if (!playerUrl) {
return res.status(404).send('Player URL is not available yet.');
}