const { WebSocket } = require('ws'); function normalizeClientName(value) { return String(value || '').trim(); } function enrichScreensWithConnections(screens, connectionsBySlug, onboardingNameBySlug) { return (screens || []).map(function (screen) { const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] }; return Object.assign({}, screen, { client_name: onboardingNameBySlug[screen.slug] || null, player_connection_count: connectionState.count || 0, player_connections: Array.isArray(connectionState.connections) ? connectionState.connections : [] }); }); } function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerPublicBaseUrl, formatDashboardDate) { return (screens || []).flatMap(function (screen) { const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] }; return (connectionState.connections || []).map(function (connection) { const deviceId = String(connection.deviceId || '').trim(); return Object.assign({}, connection, { screen_slug: screen.slug, screen_name: screen.name, client_name: (deviceId && onboardingNameByDeviceId && onboardingNameByDeviceId[deviceId]) || connection.clientName || onboardingNameBySlug[screen.slug] || String(connection.clientId || '').trim() || null, playlist_name: screen.playlist_name || null, connectedAtLabel: formatDashboardDate(connection.connectedAt), lastSeenAtLabel: formatDashboardDate(connection.lastSeenAt), player_url: `${playerPublicBaseUrl}/screen/${encodeURIComponent(screen.slug)}` }); }); }); } function createDashboardStateService(options) { const pool = options && options.pool; const common = options && options.common; const playerSnapshotCache = options && options.playerSnapshotCache; const playerSnapshotSockets = options && options.playerSnapshotSockets; const ensurePlayerSnapshotSubscription = options && options.ensurePlayerSnapshotSubscription; const playerPublicBaseUrl = String(options && options.playerPublicBaseUrl || '').replace(/\/$/, ''); const formatDashboardDate = options && options.formatDashboardDate; if (!pool || !common || !playerSnapshotCache || !playerSnapshotSockets || typeof ensurePlayerSnapshotSubscription !== 'function' || typeof formatDashboardDate !== 'function') { throw new Error('createDashboardStateService requires the dashboard dependencies.'); } async function buildDashboardState() { const data = await common.fetchAdminData(pool); const screensData = data.screens || []; screensData.forEach(function (screen) { ensurePlayerSnapshotSubscription(screen.slug); }); const [onboardingRows] = await pool.query( `SELECT s.slug, pod.device_id, pod.client_name FROM player_onboarding_devices pod JOIN screens s ON s.id = pod.screen_id WHERE pod.client_name IS NOT NULL AND TRIM(pod.client_name) <> ''` ); const onboardingNameBySlug = {}; const onboardingNameByDeviceId = {}; onboardingRows.forEach(function (row) { const clientName = normalizeClientName(row.client_name); const slug = normalizeClientName(row.slug); const deviceId = normalizeClientName(row.device_id); if (slug) { onboardingNameBySlug[slug] = clientName; } if (deviceId) { onboardingNameByDeviceId[deviceId] = clientName; } }); const connectionsBySlug = {}; screensData.forEach(function (screen) { const cached = playerSnapshotCache.get(String(screen.slug || '').trim()); if (cached && Array.isArray(cached.connections)) { connectionsBySlug[screen.slug] = cached; } }); const screens = enrichScreensWithConnections(data.screens || [], connectionsBySlug, onboardingNameBySlug).map(function (screen) { return Object.assign({}, screen, { player_url: `${playerPublicBaseUrl}/screen/${encodeURIComponent(screen.slug)}` }); }); const clients = buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerPublicBaseUrl, formatDashboardDate); const playerServiceConnected = Array.from(playerSnapshotSockets.values()).some(function (socket) { return socket && socket.readyState === WebSocket.OPEN; }); return { playlists: data.playlists || [], screens: screens, clients: clients, slides: data.slides || [], playerServiceConnected: playerServiceConnected, connectedClientsCount: screens.reduce(function (total, screen) { return total + Number(screen.player_connection_count || 0); }, 0) }; } return { buildDashboardState: buildDashboardState }; } module.exports = { createDashboardStateService };