Refine player control-plane flow

This commit is contained in:
2026-08-07 13:10:16 +01:00
parent 74318eb34e
commit 433be06bc7
35 changed files with 1604 additions and 233 deletions
+49
View File
@@ -11,6 +11,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
const withClientNameReservation = deps.withClientNameReservation;
const broadcastDashboardState = deps.broadcastDashboardState;
const requirePermission = deps.requirePermission;
const ALL_SCREENS_SLUG = '__all__';
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
try {
@@ -28,6 +29,46 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
return res.status(400).json({ error: 'Command is required' });
}
if (slug === ALL_SCREENS_SLUG) {
if (command === 'setclientname' || command === 'moveclient') {
return res.status(400).json({ error: 'This command requires a specific screen.' });
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug IS NOT NULL ORDER BY slug ASC');
if (!screenRows.length) {
return res.status(404).json({ error: 'No screens found' });
}
const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
? Object.assign({}, req.body, { command: command })
: { command: command };
if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') {
commandPayload.blackout = blackoutValue;
}
await Promise.all(screenRows.map(function (screenRow) {
return forwardPlayerCommand(String(screenRow && screenRow.slug || '').trim(), commandPayload);
}));
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json({
screen: {
id: null,
name: 'All screens',
slug: ALL_SCREENS_SLUG
},
screenSlug: slug,
command: command,
connectionId: connectionId || null,
targetScreenCount: screenRows.length,
ok: true,
allScreens: true
});
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug = ?', [slug]);
if (!screenRows.length) {
return res.status(404).json({ error: 'Screen not found' });
@@ -208,7 +249,15 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
}
const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
const liveConnection = Array.isArray(liveConnections)
? liveConnections.find(function (connection) {
const candidateConnectionId = String(connection && (connection.id || connection.clientId) || '').trim();
const candidateDeviceId = String(connection && connection.deviceId || '').trim();
return candidateConnectionId === connectionId || candidateConnectionId === deviceId || candidateDeviceId === deviceId;
})
: null;
const targetBaseUrl = String(
(liveConnection && liveConnection.playerPublicBaseUrl) ||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
''
).trim().replace(/\/$/, '');
@@ -143,6 +143,30 @@ function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
return buildQueryString(nextQuery);
}
function buildTaskPlayerLabel(metadata) {
const playerLabel = String(metadata && metadata.playerLabel || '').trim();
const playerIdentifier = String(metadata && metadata.playerIdentifier || '').trim();
const playerPublicBaseUrl = String(metadata && metadata.playerPublicBaseUrl || '').trim();
if (playerLabel) {
return playerLabel;
}
if (playerIdentifier && playerPublicBaseUrl) {
return `${playerIdentifier} (${playerPublicBaseUrl})`;
}
if (playerIdentifier) {
return playerIdentifier;
}
if (playerPublicBaseUrl) {
return playerPublicBaseUrl;
}
return '';
}
function buildQueuePageViewModel(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0 };
@@ -171,7 +195,10 @@ function buildQueuePageViewModel(data, message, currentUser) {
'finishedAt',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
function (item) { return item && item.metadata && item.metadata.sourceId; },
function (item) { return item && item.metadata && item.metadata.playerLabel; },
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
])(task);
});
const sortedTasks = sortRows(filteredTasks, function (task) {
@@ -194,7 +221,7 @@ function buildQueuePageViewModel(data, message, currentUser) {
return task && task.finishedAt;
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
@@ -234,11 +261,16 @@ function buildQueuePageViewModel(data, message, currentUser) {
});
const visibleTasksWithSources = visibleTasks.map(function (task) {
const playerLabel = buildTaskPlayerLabel(task.metadata);
const taskDescription = [playerLabel, String(task && task.key || '').trim()].filter(Boolean).join(':');
return Object.assign({}, task, {
sourceUrl: buildTaskSourceFilterUrl(queryState, task, sourceFilter),
sourceLabel: task.metadata && task.metadata.sourceName ? String(task.metadata.sourceName).trim() : '',
sourceTypeLabel: task.metadata && task.metadata.sourceType ? String(task.metadata.sourceType).trim() : '',
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : ''
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : '',
playerLabel: playerLabel,
taskDescription: taskDescription
});
});
const hasActiveFilters = Boolean(queryState.sourceType || queryState.sourceId);
@@ -287,7 +319,10 @@ function buildScheduledPageViewModel(data, message, currentUser) {
'lastError',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
function (item) { return item && item.metadata && item.metadata.sourceId; },
function (item) { return item && item.metadata && item.metadata.playerLabel; },
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
])(task);
});
const sortedRecurringTasks = sortRows(filteredRecurringTasks, function (task) {
@@ -309,7 +344,7 @@ function buildScheduledPageViewModel(data, message, currentUser) {
}).join(' ');
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
@@ -325,7 +360,8 @@ function buildScheduledPageViewModel(data, message, currentUser) {
backgroundTasksMenuOpen: true,
recurringTasks: sortedRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
intervalLabel: formatIntervalLabel(task.intervalMs),
playerLabel: buildTaskPlayerLabel(task.metadata)
});
}),
summary: summary,