// Admin client command routes for connected screens. const { commitDeviceBinding } = require('#src/player/onboarding'); module.exports = function registerScreenCommandRoutes(app, deps) { const pool = deps.pool; const common = deps.common; const forwardPlayerCommand = deps.forwardPlayerCommand; const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl; const getScreenConnections = deps.getScreenConnections; const isClientNameAvailable = deps.isClientNameAvailable; const withClientNameReservation = deps.withClientNameReservation; const broadcastDashboardState = deps.broadcastDashboardState; const requirePermission = deps.requirePermission; function normalizeExplicitPlayerBaseUrl(value) { return String(value || '').trim().replace(/\/$/, ''); } function normalizeConnectionBaseUrl(connection) { return normalizeExplicitPlayerBaseUrl(connection && connection.playerPublicBaseUrl); } function normalizeBaseUrl(value) { return String(value || '').trim().replace(/\/$/, ''); } async function fetchPlayerRegistrations() { if (typeof common.fetchPlayerRegistrations !== 'function') { return []; } try { const registrations = await common.fetchPlayerRegistrations(pool); return Array.isArray(registrations) ? registrations : []; } catch (_error) { return []; } } async function fetchScreenConnections(screenSlug) { if (typeof getScreenConnections !== 'function') { return []; } try { const response = await getScreenConnections(screenSlug); return Array.isArray(response && response.connections) ? response.connections : []; } catch (_error) { return []; } } async function fetchAllScreenTargets() { const [rows] = await pool.query( `SELECT slug FROM d_screens WHERE slug IS NOT NULL ORDER BY slug ASC` ); return (rows || []) .map(function (row) { return { slug: String(row && row.slug ? row.slug : '').trim() }; }) .filter(function (row) { return Boolean(row.slug); }); } async function resolvePlayerBaseUrlsForConnections(connections) { const connectionList = Array.isArray(connections) ? connections : []; const seen = new Set(); const registrations = await fetchPlayerRegistrations(); return connectionList.map(function (connection) { const selectedPublicBaseUrl = normalizeConnectionBaseUrl(connection); if (!selectedPublicBaseUrl || seen.has(selectedPublicBaseUrl)) { return ''; } seen.add(selectedPublicBaseUrl); const matchedPlayer = registrations.find(function (player) { return normalizeExplicitPlayerBaseUrl(player && player.public_base_url) === selectedPublicBaseUrl; }) || null; return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || selectedPublicBaseUrl; }).filter(Boolean); } async function resolvePlayerBaseUrlForExplicitPublicBaseUrl(playerBaseUrl) { const normalizedPublicBaseUrl = normalizeExplicitPlayerBaseUrl(playerBaseUrl); if (!normalizedPublicBaseUrl) { return ''; } const registrations = await fetchPlayerRegistrations(); const matchedPlayer = registrations.find(function (player) { return normalizeExplicitPlayerBaseUrl(player && player.public_base_url) === normalizedPublicBaseUrl; }) || null; return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || normalizedPublicBaseUrl; } async function forwardPlayerCommandForConnections(screenSlug, connections, commandPayload, connectionId, deviceId) { const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections); if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') { const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) { return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined); })); return { ok: true, sent: results.filter(function (result) { return result.status === 'fulfilled'; }).length, results: results }; } return forwardPlayerCommand(screenSlug, commandPayload, connectionId || deviceId || undefined); } async function forwardPlayerCommandForPlayerBaseUrl(screenSlug, playerBaseUrl, commandPayload, connectionId, deviceId) { const targetBaseUrl = await resolvePlayerBaseUrlForExplicitPublicBaseUrl(playerBaseUrl); if (targetBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function') { return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined); } return forwardPlayerCommand(screenSlug, commandPayload, connectionId || deviceId || undefined); } 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; } app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) { try { const slug = String(req.params.slug || '').trim(); const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase(); const connectionId = String((req.body && (req.body.connectionId || req.body.clientId)) || req.query.connectionId || req.query.clientId || '').trim(); const playerBaseUrl = String((req.body && req.body.playerBaseUrl) || req.query.playerBaseUrl || '').trim(); const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout') ? req.body.blackout : req.query.blackout; if (!slug) { return res.status(400).json({ error: 'Screen slug is required' }); } if (!command) { return res.status(400).json({ error: 'Command is required' }); } if (slug === '__all__') { if (command !== 'reload' && command !== 'pause' && command !== 'blackout') { return res.status(400).json({ error: 'This command requires a specific screen.' }); } const targets = await fetchAllScreenTargets(); if (!targets.length) { if (typeof broadcastDashboardState === 'function') { await broadcastDashboardState(); } return res.json({ ok: true, allScreens: true, command: command, targetScreenCount: 0, targetPlayerCount: 0, sent: 0 }); } 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; } if (command === 'pause' && req.body && Object.prototype.hasOwnProperty.call(req.body, 'paused') && commandPayload && typeof commandPayload === 'object') { commandPayload.paused = req.body.paused; } const results = await Promise.allSettled(targets.map(async function (target) { const liveConnections = await fetchScreenConnections(target.slug); if (liveConnections.length) { return forwardPlayerCommandForConnections(target.slug, liveConnections, commandPayload); } return forwardPlayerCommand(target.slug, commandPayload); })); const sentCount = results.reduce(function (total, result) { if (result.status !== 'fulfilled') { return total; } const sentValue = Number(result.value && typeof result.value.sent === 'number' ? result.value.sent : 1); return total + (Number.isFinite(sentValue) && sentValue > 0 ? sentValue : 1); }, 0); if (typeof broadcastDashboardState === 'function') { await broadcastDashboardState(); } return res.json({ ok: true, allScreens: true, command: command, targetScreenCount: targets.length, targetPlayerCount: sentCount, sent: sentCount, blackout: command === 'blackout' ? Boolean(commandPayload.blackout) : undefined, paused: command === 'pause' ? Boolean(commandPayload.paused) : undefined }); } 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' }); } if (command === 'setclientname') { const deviceId = String((req.body && (req.body.deviceId || req.body.clientId || req.body.connectionId)) || req.query.deviceId || req.query.clientId || req.query.connectionId || '').trim(); const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim(); if (!deviceId) { return res.status(400).json({ error: 'Device ID is required' }); } if (!clientName) { return res.status(400).json({ error: 'Client name is required' }); } const [currentRows] = await pool.query( `SELECT client_name FROM d_onboarding_devices WHERE device_id = ? LIMIT 1`, [deviceId] ); const onboardingRow = currentRows[0] || null; const currentName = String(onboardingRow && onboardingRow.client_name ? onboardingRow.client_name : '').trim(); if (currentName && currentName.toLowerCase() === clientName.toLowerCase()) { return res.json({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null, deviceId: deviceId, clientName: currentName, ok: true, unchanged: true }); } if (typeof withClientNameReservation !== 'function') { return res.status(500).json({ error: 'Client name reservation is unavailable.' }); } return withClientNameReservation(pool, clientName, async function () { let liveConnections = []; try { const [screenSlugs] = await pool.query('SELECT slug FROM d_screens ORDER BY slug ASC'); const liveResults = await Promise.all((screenSlugs || []).map(async function (row) { const screenSlug = String(row && row.slug ? row.slug : '').trim(); if (!screenSlug || typeof getScreenConnections !== 'function') { return []; } try { const liveResponse = await getScreenConnections(screenSlug); return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : []; } catch (_error) { return []; } })); liveConnections = liveResults.flat(); } catch (_error) { liveConnections = []; } const available = await isClientNameAvailable(pool, clientName, deviceId, liveConnections); if (!available) { return res.status(409).json({ error: 'Client name already exists.' }); } if (!onboardingRow) { if (playerBaseUrl) { await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, { command: command, clientName: clientName, clientId: connectionId || deviceId || null, deviceId: deviceId || null }, connectionId || deviceId || undefined, deviceId || null); } else { await forwardPlayerCommandForConnections(slug, liveConnections, { command: command, clientName: clientName, clientId: connectionId || deviceId || null, deviceId: deviceId || null }, connectionId || deviceId || undefined, deviceId || null); } if (typeof broadcastDashboardState === 'function') { await broadcastDashboardState(); } return res.json({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null, deviceId: deviceId, clientName: clientName, ok: true, liveOnly: true }); } const [updateResult] = await pool.query( `UPDATE d_onboarding_devices pod JOIN d_screens s ON s.id = pod.screen_id SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP WHERE s.slug = ? AND pod.device_id = ?`, [clientName, slug, deviceId] ); if (!updateResult.affectedRows) { return res.status(404).json({ error: 'Client not found' }); } return res.json({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null, deviceId: deviceId, clientName: clientName, ok: true }); }); } if (command === 'moveclient') { const deviceId = String((req.body && (req.body.deviceId || req.body.clientId || req.body.connectionId)) || req.query.deviceId || req.query.clientId || req.query.connectionId || '').trim(); const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim(); const targetScreenSlug = String((req.body && (req.body.targetScreenSlug || req.body.screenSlug)) || req.query.targetScreenSlug || req.query.screenSlug || '').trim(); if (!deviceId) { return res.status(400).json({ error: 'Device ID is required' }); } if (!targetScreenSlug) { return res.status(400).json({ error: 'Target screen is required' }); } const [currentRows] = await pool.query( `SELECT d.client_name, s.slug AS current_screen_slug FROM d_onboarding_devices d LEFT JOIN d_screens s ON s.id = d.screen_id WHERE d.device_id = ? LIMIT 1`, [deviceId] ); const onboardingRow = currentRows[0] || null; const resolvedClientName = String(clientName || onboardingRow && onboardingRow.client_name || '').trim(); const currentScreenSlug = String(onboardingRow && onboardingRow.current_screen_slug || '').trim(); if (!resolvedClientName) { return res.status(400).json({ error: 'Client name is required' }); } if (currentScreenSlug && currentScreenSlug === targetScreenSlug) { return res.json({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null, deviceId: deviceId, clientName: resolvedClientName, targetScreenSlug: targetScreenSlug, ok: true, unchanged: true }); } if (typeof commitDeviceBinding !== 'function') { return res.status(500).json({ error: 'Client binding is unavailable.' }); } const liveConnections = await fetchScreenConnections(slug); const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections); let targetPlayerUrl = ''; 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; }) || liveConnections[0] || null : null; const sourcePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(playerBaseUrl || (liveConnection && liveConnection.playerPublicBaseUrl) || ''); if (sourcePlayerBaseUrl) { targetPlayerUrl = `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}`; } if (!targetPlayerUrl) { targetPlayerUrl = `/screen/${encodeURIComponent(targetScreenSlug)}`; } if (playerBaseUrl) { await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, { command: 'redirect', url: targetPlayerUrl }, connectionId || deviceId || undefined, deviceId || null); } else { await forwardPlayerCommandForConnections(slug, liveConnections, { command: 'redirect', url: targetPlayerUrl }, connectionId || deviceId || undefined, deviceId || null); } if (typeof broadcastDashboardState === 'function') { await broadcastDashboardState(); } return res.json({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null, deviceId: deviceId, clientName: status ? status.client_name : resolvedClientName, targetScreenSlug: targetScreenSlug, playerUrl: targetPlayerUrl, ok: true }); } 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; } const liveConnections = await fetchScreenConnections(slug); const result = playerBaseUrl ? await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, commandPayload, connectionId, null) : await forwardPlayerCommandForConnections(slug, liveConnections, commandPayload, connectionId, null); if (typeof broadcastDashboardState === 'function') { await broadcastDashboardState(); } return res.json(Object.assign({ screen: screenRows[0], screenSlug: slug, command: command, connectionId: connectionId || null }, result && typeof result === 'object' ? result : {})); } catch (error) { next(error); } }); };