// Admin manage routes for screens and commands. const { buildAuditChanges } = require('#src/data/audit-log'); module.exports = function registerManageRoutes(app, deps) { const pool = deps.pool; const common = deps.common; const pages = deps.pages; const getAuditUserId = deps.getAuditUserId; const recordRequestAuditEvent = deps.recordRequestAuditEvent; const redirectAfterSave = deps.redirectAfterSave; const notifyPlayerScreens = deps.notifyPlayerScreens; const broadcastDashboardState = deps.broadcastDashboardState; const getScreenDeleteBlockMessage = deps.getScreenDeleteBlockMessage; const getScreenConnections = deps.getScreenConnections; const forwardPlayerCommand = deps.forwardPlayerCommand; const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl; const requirePermission = deps.requirePermission; const SCREEN_NAME_MAX_LENGTH = 255; const SCREEN_SLUG_MAX_LENGTH = 255; function readLimitedText(value, maxLength) { const text = String(value || '').trim(); const limit = Number(maxLength); if (!Number.isFinite(limit) || limit < 1 || text.length <= limit) { return text; } return text.slice(0, limit); } function normalizeExplicitPlayerBaseUrl(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 []; } } 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; } async function fetchLiveConnectionsForScreen(slug) { if (typeof getScreenConnections !== 'function') { return []; } try { const response = await getScreenConnections(slug); return Array.isArray(response && response.connections) ? response.connections : []; } catch (_error) { return []; } } 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 = normalizeExplicitPlayerBaseUrl(connection && connection.playerPublicBaseUrl); 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 forwardPlayerCommandForConnections(slug, connections, commandPayload) { const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections); if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') { const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) { return forwardPlayerCommandToBaseUrl(targetBaseUrl, slug, commandPayload); })); return { ok: true, sent: results.filter(function (result) { return result.status === 'fulfilled'; }).length, results: results }; } return forwardPlayerCommand(slug, commandPayload); } async function fetchAllScreenTargets() { const [rows] = await pool.query( `SELECT s.slug FROM d_screens s WHERE s.slug IS NOT NULL ORDER BY s.slug ASC` ); return (rows || []) .map(function (row) { return { slug: String(row && row.slug ? row.slug : '').trim(), playerId: '', publicBaseUrl: '', internalBaseUrl: '' }; }) .filter(function (row) { return Boolean(row.slug); }); } app.post('/commands', requirePermission('dashboard.allow'), async function (req, res, next) { try { const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase(); const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout') ? req.body.blackout : req.query.blackout; const pausedValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'paused') ? req.body.paused : req.query.paused; if (!command) { return res.status(400).json({ error: 'Command is required' }); } if (command !== 'reload' && command !== 'blackout' && command !== 'pause') { return res.status(400).json({ error: 'Unsupported command' }); } const targets = await fetchAllScreenTargets(); if (!targets.length) { await broadcastDashboardState(); return res.json({ ok: true, command: command, sent: 0 }); } const payload = command === 'blackout' ? { command: 'blackout', blackout: blackoutValue === true || blackoutValue === 'true' || blackoutValue === '1' ? true : false } : command === 'pause' ? { command: 'pause', paused: pausedValue === true || pausedValue === 'true' || pausedValue === '1' ? true : false } : 'reload'; const results = await Promise.allSettled(targets.map(async function (target) { const liveConnections = await fetchLiveConnectionsForScreen(target.slug); if (liveConnections.length) { return forwardPlayerCommandForConnections(target.slug, liveConnections, payload); } return forwardPlayerCommand(target.slug, payload); })); 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); await broadcastDashboardState(); return res.json({ ok: true, command: command, sent: sentCount, blackout: command === 'blackout' ? Boolean(payload.blackout) : undefined, paused: command === 'pause' ? Boolean(payload.paused) : undefined }); } catch (error) { next(error); } }); app.get('/screens/new', requirePermission('screens.create'), async function (req, res, next) { try { const data = await common.fetchScreenEditData(pool); res.send(pages.renderScreenFormPage(data, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.post('/screens', requirePermission('screens.create'), async function (req, res, next) { try { const name = typeof common.validateMaxLength === 'function' ? common.validateMaxLength(req.body.name || '', SCREEN_NAME_MAX_LENGTH, 'Screen name') : readLimitedText(req.body.name || '', 255); if (!name) { return res.status(400).send('Screen name is required.'); } if (await common.fetchDuplicateName(pool, 'd_screens', name)) { return res.status(400).send('A screen with that name already exists.'); } const slugInput = typeof common.validateMaxLength === 'function' ? common.validateMaxLength(req.body.slug || '', SCREEN_SLUG_MAX_LENGTH, 'Screen URL') : readLimitedText(req.body.slug || '', 255); const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null; const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name)); const actorId = getAuditUserId(req); const [result] = await pool.query('INSERT INTO d_screens (name, slug, playlist_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, slug, playlistId, actorId, actorId]); if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'screens', eventType: 'screen.created', actorUserId: actorId, targetType: 'screen', targetId: result.insertId, targetLabel: name, details: { slug: slug, playlistId: playlistId } }); redirectAfterSave(req, res, '/screens?edit=' + result.insertId, { closeUrl: '/screens', newUrl: '/screens/new', message: 'Screen created.' }); } catch (error) { next(error); } }); app.post('/screens/:id', requirePermission('screens.update'), async function (req, res, next) { try { const name = typeof common.validateMaxLength === 'function' ? common.validateMaxLength(req.body.name || '', SCREEN_NAME_MAX_LENGTH, 'Screen name') : readLimitedText(req.body.name || '', 255); if (!name) { return res.status(400).send('Screen name is required.'); } const screen = await common.fetchScreenById(pool, Number(req.params.id)); if (!screen) { return res.status(404).send('Screen not found'); } if (await common.fetchDuplicateName(pool, 'd_screens', name, screen.id)) { return res.status(400).send('A screen with that name already exists.'); } const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null; const previousPlaylistId = screen.playlist_id; const slug = String(screen.slug || '').trim(); const previousSlug = String(screen.slug || '').trim(); const changes = buildAuditChanges({ name: screen.name, slug: previousSlug, playlistId: previousPlaylistId === null ? null : Number(previousPlaylistId) }, { name: name, slug: slug, playlistId: playlistId }); await pool.query('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, getAuditUserId(req), screen.id]); if (previousPlaylistId !== playlistId && previousSlug) { await notifyPlayerScreens([previousSlug], 'refresh'); } if (previousSlug && previousSlug !== slug) { const previousConnections = await fetchLiveConnectionsForScreen(previousSlug); const redirectPayload = { command: 'redirect', url: `/screen/${encodeURIComponent(slug)}` }; if (previousConnections.length) { await forwardPlayerCommandForConnections(previousSlug, previousConnections, redirectPayload); } else { await forwardPlayerCommand(previousSlug, redirectPayload); } } if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'screens', eventType: 'screen.updated', actorUserId: getAuditUserId(req), targetType: 'screen', targetId: screen.id, targetLabel: name, details: { changes: changes } }); redirectAfterSave(req, res, '/screens?edit=' + screen.id, { closeUrl: '/screens', newUrl: '/screens/new', message: 'Screen updated.' }); } catch (error) { next(error); } }); app.post('/screens/:id/delete', requirePermission('screens.delete'), async function (req, res, next) { try { const screen = await common.fetchScreenById(pool, Number(req.params.id)); if (!screen) { return res.status(404).send('Screen not found'); } const blockMessage = await getScreenDeleteBlockMessage(pool, screen, getScreenConnections); if (blockMessage) { return res.redirect('/screens?message=' + encodeURIComponent(blockMessage)); } await pool.query('DELETE FROM d_screens WHERE id = ?', [screen.id]); if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'screens', eventType: 'screen.deleted', actorUserId: getAuditUserId(req), targetType: 'screen', targetId: screen.id, targetLabel: screen.name }); res.redirect('/screens?message=' + encodeURIComponent('Screen deleted.')); } catch (error) { next(error); } }); };