(function () { var webUiHelpers = window.webUiHelpers || {}; var escapeHtml = webUiHelpers.escapeHtml; var formatDashboardDate = webUiHelpers.formatDashboardDate; var getClientRowKey = webUiHelpers.getClientRowKey; var getClientDisplayName = webUiHelpers.getClientDisplayName; var setButtonVariant = webUiHelpers.setButtonVariant; var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp; var LIST_PAGE_SIZE = 25; var latestDashboardState = null; var ALL_SCREENS_SLUG = '__all__'; var ALL_SCREENS_LABEL = 'All screens'; function getClientSearchInput() { var table = document.getElementById('dashboard-clients-table'); var container = table && typeof table.closest === 'function' ? (table.closest('[data-table-search-container]') || table.closest('.card') || null) : null; return container && container.querySelector ? container.querySelector('[data-table-search]') : document.querySelector('[data-table-search]'); } function getClientListQueryState() { var searchParams = new URLSearchParams(String(window.location && window.location.search || '')); var searchInput = getClientSearchInput(); var searchValue = searchInput ? String(searchInput.value || '').trim() : String(searchParams.get('search') || '').trim(); return { search: searchValue, sort: String(searchParams.get('sort') || '').trim(), direction: String(searchParams.get('direction') || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc', page: Math.max(1, Math.floor(Number(searchParams.get('page') || 1) || 1)) }; } function isClientSearchLoading() { return Boolean(document.querySelector('[data-table-search-loading="true"]')); } function getComparableClientSortValue(rawValue) { var value = String(rawValue || '').trim(); if (!value) { return { type: 'empty', value: '' }; } var numericValue = Number(value.replace(/,/g, '')); if (!Number.isNaN(numericValue)) { return { type: 'number', value: numericValue }; } var dateValue = Date.parse(value); if (!Number.isNaN(dateValue)) { return { type: 'date', value: dateValue }; } return { type: 'string', value: value.toLowerCase() }; } function compareClientSortValues(leftValue, rightValue) { var left = getComparableClientSortValue(leftValue); var right = getComparableClientSortValue(rightValue); if (left.type === 'empty' && right.type === 'empty') { return 0; } if (left.type === 'empty') { return 1; } if (right.type === 'empty') { return -1; } if (left.type === right.type) { if (left.value < right.value) { return -1; } if (left.value > right.value) { return 1; } return 0; } return String(left.value).localeCompare(String(right.value), undefined, { numeric: true, sensitivity: 'base' }); } function createClientSearchMatcher(searchTerm) { var query = String(searchTerm || '').trim().toLowerCase(); if (!query) { return function () { return true; }; } return function (client) { var searchableValues = [ client && client.name, client && client.client_name, client && client.clientId, client && client.deviceId, client && client.slug, client && client.screen_slug, client && client.screen_name, client && client.ipAddress, client && client.clientIp, client && client.status, client && client.currentSlideTitle ]; return searchableValues.map(function (value) { return String(value || '').trim().toLowerCase(); }).join(' ').indexOf(query) !== -1; }; } function sortClientsForTable(clients, sortKey, sortDirection) { var normalizedSortKey = String(sortKey || '').trim(); var normalizedDirection = String(sortDirection || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc'; var accessors = { client: function (client) { return String(client && (getClientDisplayName(client) || client.client_name || client.name || client.clientId) || '').trim(); }, screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); }, slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); }, ip: function (client) { return String(client && client.clientIp || '').trim(); }, viewport: function (client) { var viewport = client && client.viewport; if (!viewport || !viewport.width || !viewport.height) { return ''; } return String(Number(viewport.width) || 0) + 'x' + String(Number(viewport.height) || 0); }, connected: function (client) { return String(client && (client.connectedAt || client.lastSeenAt) || '').trim(); } }; function compareValues(leftValue, rightValue) { return compareClientSortValues(leftValue, rightValue); } var sortKeys = accessors[normalizedSortKey] ? [normalizedSortKey] : ['client']; if (sortKeys[0] === 'client') { sortKeys.push('ip'); } else if (sortKeys[0] === 'ip') { sortKeys.push('client'); } return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) { for (var index = 0; index < sortKeys.length; index += 1) { var sortKeyName = sortKeys[index]; var comparison = compareValues(accessors[sortKeyName](leftClient), accessors[sortKeyName](rightClient)); if (comparison !== 0) { return index === 0 && normalizedDirection === 'desc' ? comparison * -1 : comparison; } } return 0; }); } function getVisibleClients(state) { var query = getClientListQueryState(); var clients = Array.isArray(state && state.clients) ? state.clients.slice() : []; var searchMatcher = createClientSearchMatcher(query.search); clients = clients.filter(searchMatcher); clients = sortClientsForTable(clients, query.sort, query.direction); return clients.slice((query.page - 1) * LIST_PAGE_SIZE, ((query.page - 1) * LIST_PAGE_SIZE) + LIST_PAGE_SIZE); } function getClientMoveModalElements() { return { modal: document.getElementById('client-move-screen-modal'), form: document.getElementById('client-move-screen-form'), targetSelect: document.getElementById('client-move-screen-target'), connectionInput: document.querySelector('[data-client-move-connection-id]'), deviceInput: document.querySelector('[data-client-move-device-id]'), clientNameInput: document.querySelector('[data-client-move-client-name]'), playerBaseUrlInput: document.querySelector('[data-client-move-player-base-url]') }; } function getClientMoveScreens() { if (latestDashboardState && Array.isArray(latestDashboardState.screens) && latestDashboardState.screens.length) { return latestDashboardState.screens.slice().sort(compareScreensByConnectedClients); } var select = document.getElementById('client-move-screen-target'); if (!select || !select.options) { return []; } return Array.prototype.slice.call(select.options).map(function (option) { return { slug: String(option.value || '').trim(), name: String(option.textContent || option.value || '').trim() }; }).filter(function (screen) { return Boolean(screen && screen.slug); }); } function updateClientMoveModalFromRow(row) { var elements = getClientMoveModalElements(); if (!elements.form || !elements.targetSelect || !row) { return; } var currentScreenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim(); var connectionId = String(row.getAttribute('data-client-key') || '').trim(); var deviceId = String(row.getAttribute('data-client-device-id') || '').trim(); var playerBaseUrl = String(row.getAttribute('data-client-player-base-url') || '').trim(); var clientNameCell = row.querySelector('td[data-label="Client"] > div'); var clientName = String(clientNameCell && clientNameCell.textContent || '').trim(); var options = Array.prototype.slice.call(elements.targetSelect.options || []); options.forEach(function (option) { option.disabled = false; if (String(option.value || '').trim() === currentScreenSlug) { option.disabled = true; } }); elements.form.action = currentScreenSlug ? '/clients/' + encodeURIComponent(currentScreenSlug) + '/commands' : '#'; if (elements.connectionInput) { elements.connectionInput.value = connectionId; } if (elements.deviceInput) { elements.deviceInput.value = deviceId; } if (elements.clientNameInput) { elements.clientNameInput.value = clientName; } if (elements.playerBaseUrlInput) { elements.playerBaseUrlInput.value = playerBaseUrl; } elements.targetSelect.value = ''; if (elements.form.querySelector('button[type="submit"]')) { elements.form.querySelector('button[type="submit"]').disabled = false; } } function renderClientActionCell(client) { var paused = Boolean(client.paused); var pauseButtonClass = 'btn btn-sm btn-info'; var pauseButtonIcon = paused ? 'bi-play-fill' : 'bi-pause-fill'; var pauseButtonLabel = paused ? 'Resume client' : 'Pause client'; var pauseButtonText = paused ? 'Resume' : 'Pause'; var blackout = Boolean(client.blackout); var blackoutButtonClass = 'btn btn-sm ' + (blackout ? 'btn-success' : 'btn-secondary'); var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash'; var blackoutButtonLabel = blackout ? 'Restore client' : 'Blackout client'; var blackoutButtonText = blackout ? 'Restore' : 'Blackout'; var reloadConfirmMessage = 'Reloading will restart the player page. Continue?'; return [ '
', '
', '', '
', '
', '
', '
', '
' ].join(''); } function updateClientActionCell(cell, client) { if (!cell) { return; } var pauseButton = cell.querySelector('button[data-action="pause"]'); if (!pauseButton) { cell.innerHTML = renderClientActionCell(client); return; } var paused = Boolean(client.paused); setButtonVariant(pauseButton, ['btn-secondary', 'btn-outline-primary'], 'btn-info'); pauseButton.setAttribute('aria-label', paused ? 'Resume client' : 'Pause client'); pauseButton.setAttribute('title', paused ? 'Resume client' : 'Pause client'); pauseButton.innerHTML = '' + (paused ? 'Resume' : 'Pause'); var pauseForm = pauseButton.form; if (pauseForm) { var commandInput = pauseForm.querySelector('input[name="command"]'); if (commandInput) { commandInput.value = 'pause'; } var connectionInput = pauseForm.querySelector('input[name="connectionId"]'); if (connectionInput) { connectionInput.value = client.id || ''; } var playerBaseUrlInput = pauseForm.querySelector('input[name="playerBaseUrl"]'); if (playerBaseUrlInput) { playerBaseUrlInput.value = client.player_url || ''; } pauseForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands'; } var reloadButton = cell.querySelector('button[data-action="reload"]'); if (reloadButton) { reloadButton.setAttribute('aria-label', 'Reload client'); reloadButton.setAttribute('title', 'Reload client'); reloadButton.innerHTML = ''; setButtonVariant(reloadButton, ['btn-danger', 'btn-success', 'btn-outline-secondary', 'btn-outline-dark', 'btn-outline-primary', 'btn-secondary'], 'btn-danger'); var reloadForm = reloadButton.form; if (reloadForm) { var reloadInput = reloadForm.querySelector('input[name="connectionId"]'); if (reloadInput) { reloadInput.value = client.id || ''; } var reloadPlayerBaseUrlInput = reloadForm.querySelector('input[name="playerBaseUrl"]'); if (reloadPlayerBaseUrlInput) { reloadPlayerBaseUrlInput.value = client.player_url || ''; } reloadForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands'; reloadForm.setAttribute('data-confirm-message', 'Reloading will restart the player page. Continue?'); } } var blackoutButton = cell.querySelector('button[data-action="blackout"]'); if (!blackoutButton) { cell.innerHTML = renderClientActionCell(client); return; } var blackout = Boolean(client.blackout); var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash'; setButtonVariant(blackoutButton, ['btn-success', 'btn-secondary'], blackout ? 'btn-success' : 'btn-secondary'); blackoutButton.setAttribute('aria-label', blackout ? 'Restore client' : 'Blackout client'); blackoutButton.setAttribute('title', blackout ? 'Restore client' : 'Blackout client'); blackoutButton.innerHTML = '' + (blackout ? 'Restore' : 'Blackout'); var blackoutForm = blackoutButton.form; if (blackoutForm) { var blackoutCommandInput = blackoutForm.querySelector('input[name="command"]'); if (blackoutCommandInput) { blackoutCommandInput.value = 'blackout'; } var blackoutStateInput = blackoutForm.querySelector('input[name="blackout"]'); if (blackoutStateInput) { blackoutStateInput.value = blackout ? 'false' : 'true'; } var blackoutConnectionInput = blackoutForm.querySelector('input[name="connectionId"]'); if (blackoutConnectionInput) { blackoutConnectionInput.value = client.id || ''; } var blackoutPlayerBaseUrlInput = blackoutForm.querySelector('input[name="playerBaseUrl"]'); if (blackoutPlayerBaseUrlInput) { blackoutPlayerBaseUrlInput.value = client.player_url || ''; } blackoutForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands'; } var previousButton = cell.querySelector('button[data-action="previous"]'); if (!previousButton) { cell.innerHTML = renderClientActionCell(client); return; } setButtonVariant(previousButton, ['btn-outline-secondary', 'btn-success', 'btn-danger', 'btn-primary', 'btn-secondary', 'btn-warning'], 'btn-warning'); previousButton.setAttribute('aria-label', 'Previous slide'); previousButton.setAttribute('title', 'Previous slide'); var previousForm = previousButton.form; if (previousForm) { var previousCommandInput = previousForm.querySelector('input[name="command"]'); if (previousCommandInput) { previousCommandInput.value = 'previous'; } var previousConnectionInput = previousForm.querySelector('input[name="connectionId"]'); if (previousConnectionInput) { previousConnectionInput.value = client.id || ''; } var previousPlayerBaseUrlInput = previousForm.querySelector('input[name="playerBaseUrl"]'); if (previousPlayerBaseUrlInput) { previousPlayerBaseUrlInput.value = client.player_url || ''; } previousForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands'; } var nextButton = cell.querySelector('button[data-action="next"]'); if (!nextButton) { cell.innerHTML = renderClientActionCell(client); return; } setButtonVariant(nextButton, ['btn-outline-secondary', 'btn-success', 'btn-danger', 'btn-primary', 'btn-secondary', 'btn-warning'], 'btn-warning'); nextButton.setAttribute('aria-label', 'Next slide'); nextButton.setAttribute('title', 'Next slide'); var nextForm = nextButton.form; if (nextForm) { var nextCommandInput = nextForm.querySelector('input[name="command"]'); if (nextCommandInput) { nextCommandInput.value = 'next'; } var nextConnectionInput = nextForm.querySelector('input[name="connectionId"]'); if (nextConnectionInput) { nextConnectionInput.value = client.id || ''; } var nextPlayerBaseUrlInput = nextForm.querySelector('input[name="playerBaseUrl"]'); if (nextPlayerBaseUrlInput) { nextPlayerBaseUrlInput.value = client.player_url || ''; } nextForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands'; } } function syncClientActionCell(row, client, hasActionsColumn) { if (!row || !row.cells) { return; } if (!hasActionsColumn) { if (row.cells.length > 6) { row.deleteCell(row.cells.length - 1); } return; } var actionCell = row.cells.length > 6 ? row.cells[6] : null; if (!actionCell) { actionCell = row.insertCell(-1); actionCell.setAttribute('data-label', 'Actions'); actionCell.className = 'text-end'; } updateClientActionCell(actionCell, client); } function renderClientRow(client, hasActionsColumn) { var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; var clientIpValue = normalizeDisplayIp(client.clientIp); var clientIp = clientIpValue ? escapeHtml(clientIpValue) : 'Unknown'; var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : 'Unknown'; var clientNameValue = getClientDisplayName(client); var clientName = clientNameValue ? escapeHtml(clientNameValue) : 'Unknown'; var screenName = client.screen_name ? escapeHtml(client.screen_name) : 'Unknown'; var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : 'No slide currently showing'; var actionCell = hasActionsColumn ? '' + renderClientActionCell(client) + '' : ''; return [ '', '
' + clientName + '
', '
' + screenName + '
', '' + currentSlide + '', '' + clientIp + '', '' + viewport + '', '' + connectedAt + '', actionCell, '' ].join(''); } function updateClientRowCells(row, client, hasActionsColumn) { if (!row || !row.cells || row.cells.length < 6) { return; } var connectedAt = client.connectedAt ? '
' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '
' + (client.lastSeenAt ? '
' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '
' : '') : 'Unknown'; var clientIpValue = normalizeDisplayIp(client.clientIp); var clientIp = clientIpValue ? escapeHtml(clientIpValue) : 'Unknown'; var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : 'Unknown'; var clientNameValue = getClientDisplayName(client); var clientName = clientNameValue ? escapeHtml(clientNameValue) : 'Unknown'; var screenName = client.screen_name ? escapeHtml(client.screen_name) : 'Unknown'; var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : 'No slide currently showing'; row.setAttribute('data-client-key', escapeHtml(getClientRowKey(client))); row.setAttribute('data-client-id', escapeHtml(client.clientId || '')); row.setAttribute('data-client-device-id', escapeHtml(client.deviceId || '')); row.setAttribute('data-client-screen-slug', escapeHtml(client.screen_slug || '')); row.setAttribute('data-client-player-base-url', escapeHtml(client.player_url || '')); setCellHtml(row.cells[0], '
' + clientName + '
'); setCellHtml(row.cells[1], '
' + screenName + '
'); setCellHtml(row.cells[2], currentSlide); setCellHtml(row.cells[3], clientIp); setCellHtml(row.cells[4], viewport); setCellHtml(row.cells[5], connectedAt); syncClientActionCell(row, client, hasActionsColumn); } function createClientRowFromTemplate(client, hasActionsColumn) { var template = document.createElement('tbody'); template.innerHTML = renderClientRow(client, hasActionsColumn); return template.firstElementChild || null; } function setCellHtml(cell, html) { if (!cell || String(cell.innerHTML || '') === String(html || '')) { return false; } cell.innerHTML = html; return true; } function renderScreenTile(screen) { var clientCount = Number(screen.player_connection_count || 0); var connectionLabel = clientCount ? clientCount + ' live' : 'No clients'; var connectionStateClass = clientCount ? 'is-live' : 'is-idle'; var playlistLabel = screen.playlist_name ? escapeHtml(screen.playlist_name) : 'Unassigned'; return [ '
', '
', '
', '

' + escapeHtml(screen.name || '') + '

', '
', '' + escapeHtml(connectionLabel) + '', '
', '
', '
Playlist
' + playlistLabel + '
', '
', '
' ].join(''); } function compareScreensByConnectedClients(left, right) { var leftCount = Number(left && left.player_connection_count || 0); var rightCount = Number(right && right.player_connection_count || 0); if (leftCount !== rightCount) { return rightCount - leftCount; } var leftName = String(left && left.name || '').trim(); var rightName = String(right && right.name || '').trim(); var nameCompare = leftName.localeCompare(rightName, undefined, { sensitivity: 'base', numeric: true }); if (nameCompare !== 0) { return nameCompare; } return String(left && left.slug || '').localeCompare(String(right && right.slug || ''), undefined, { sensitivity: 'base', numeric: true }); } function updateStats(state) { var playerCount = document.getElementById('dashboard-player-count'); var screenCount = document.getElementById('dashboard-screen-count'); var slideCount = document.getElementById('dashboard-slide-count'); var playlistCount = document.getElementById('dashboard-playlist-count'); if (playlistCount && Array.isArray(state.playlists)) { playlistCount.textContent = String(state.playlists.length); } if (slideCount && Array.isArray(state.slides)) { slideCount.textContent = String(state.slides.length); } if (screenCount && Array.isArray(state.screens)) { screenCount.textContent = String(state.screens.length); } if (playerCount) { playerCount.textContent = String(Number(state.connectedPlayersCount || 0)); } } function updateClientTable(state, forceRender) { var tbody = document.getElementById('dashboard-clients-table-body'); if (!tbody || !Array.isArray(state.clients)) { return; } if (!forceRender && isClientSearchLoading()) { return; } var table = document.getElementById('dashboard-clients-table'); var hasActionsColumn = Boolean(table && String(table.getAttribute('data-has-actions-column') || '').toLowerCase() === 'true'); var visibleClients = getVisibleClients(state); var canPatchRows = typeof tbody.querySelectorAll === 'function' && typeof tbody.insertBefore === 'function' && typeof tbody.removeChild === 'function' && typeof document.createElement === 'function'; if (!visibleClients.length) { tbody.innerHTML = 'No connected clients yet.'; return; } if (!canPatchRows) { tbody.innerHTML = visibleClients.map(function (client) { return renderClientRow(client, hasActionsColumn); }).join(''); return; } var existingRows = {}; Array.prototype.slice.call(tbody.querySelectorAll('tr[data-client-key]')).forEach(function (row) { existingRows[String(row.getAttribute('data-client-key') || '').trim()] = row; }); var nextRows = visibleClients.map(function (client) { var rowKey = String(getClientRowKey(client) || '').trim(); var row = existingRows[rowKey] || null; if (!row) { row = createClientRowFromTemplate(client, hasActionsColumn); } else { updateClientRowCells(row, client, hasActionsColumn); } return row; }).filter(function (row) { return Boolean(row); }); nextRows.forEach(function (row, index) { var referenceNode = tbody.children[index] || null; if (referenceNode !== row) { tbody.insertBefore(row, referenceNode); } }); while (tbody.children.length > nextRows.length) { tbody.removeChild(tbody.lastElementChild); } } function refreshClientTableFromLatestState() { if (!latestDashboardState) { return; } updateClientTable(latestDashboardState, true); } function updateKioskLauncherModal(state) { var modal = document.getElementById('dashboard-kiosk-launcher-modal'); if (!modal) { return; } var checkbox = modal.querySelector('[data-kiosk-launcher-confirm]'); var select = modal.querySelector('[data-kiosk-launcher-player-select]'); var downloadLinks = Array.prototype.slice.call(modal.querySelectorAll('[data-kiosk-launcher-download]')); var playersState = state && Array.isArray(state.kioskPlayers) ? state.kioskPlayers : (state && Array.isArray(state.clients) ? state.clients : []); var players = playersState.filter(function (client) { return Boolean(client && String(client.player_url || '').trim()); }); if (select && playersState.length) { var currentValue = String(select.value || '').trim(); var options = ['']; players.forEach(function (player) { var playerUrl = String(player.player_url || '').trim(); var playerIdentifier = String(player.player_identifier || 'Connected player').trim(); if (!playerUrl) { return; } options.push(''); }); select.innerHTML = options.join(''); if (currentValue) { select.value = currentValue; } } var selectedUrl = select ? String(select.value || '').trim() : ''; var canEnableDownloads = Boolean(checkbox && checkbox.checked && selectedUrl); downloadLinks.forEach(function (link) { if (!link) { return; } var baseHref = String(link.getAttribute('data-kiosk-launcher-download-base') || link.getAttribute('href') || '').trim(); if (canEnableDownloads) { link.setAttribute('href', baseHref + '?playerUrl=' + encodeURIComponent(selectedUrl)); link.classList.remove('disabled'); link.setAttribute('aria-disabled', 'false'); link.removeAttribute('tabindex'); } else { link.removeAttribute('href'); link.classList.add('disabled'); link.setAttribute('aria-disabled', 'true'); link.setAttribute('tabindex', '-1'); } }); } function updateScreenGrid(state) { var grid = document.getElementById('dashboard-screens-grid'); if (!grid || !Array.isArray(state.screens)) { return; } var screens = state.screens.slice().sort(compareScreensByConnectedClients); if (!screens.length) { grid.innerHTML = '
No screens yet.
'; return; } grid.innerHTML = screens.map(renderScreenTile).join(''); } function updateScreenCommandControls(state) { var select = document.getElementById('screen-command-select'); if (!select) { return; } var forms = Array.prototype.slice.call(document.querySelectorAll('[data-screen-command-form]')); var pill = document.querySelector('[data-screen-command-pill]'); var nameNode = document.querySelector('[data-screen-command-name]'); var metaNode = document.querySelector('[data-screen-command-meta]'); var screens = Array.isArray(state && state.screens) ? state.screens : []; var screenBySlug = {}; screens.forEach(function (screen) { if (screen && screen.slug) { screenBySlug[String(screen.slug)] = screen; } }); if (!screens.length) { select.value = ''; select.disabled = true; forms.forEach(function (form) { form.querySelectorAll('button, input').forEach(function (control) { control.disabled = true; }); }); if (pill) { pill.classList.remove('is-live'); pill.classList.add('is-idle'); pill.textContent = 'No screens'; } if (nameNode) { nameNode.textContent = 'No target available'; } if (metaNode) { metaNode.textContent = 'Create a screen before using screen-level commands.'; } return; } select.disabled = false; var selectedOption = select.options && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null; var isAllSelected = Boolean(selectedOption && String(selectedOption.getAttribute('data-screen-target-all') || '').toLowerCase() === 'true') || select.value === ALL_SCREENS_SLUG; var selectedScreen = isAllSelected ? { slug: ALL_SCREENS_SLUG, name: String(selectedOption && selectedOption.textContent || ALL_SCREENS_LABEL).trim() || ALL_SCREENS_LABEL } : screenBySlug[select.value] || null; var selectedSlug = isAllSelected ? ALL_SCREENS_SLUG : String(selectedScreen && selectedScreen.slug || '').trim(); var selectedClients = Array.isArray(state && state.clients) ? state.clients.filter(function (client) { if (isAllSelected) { return true; } return String(client && client.screen_slug || '').trim() === selectedSlug; }) : []; var connectionCount = selectedClients.length; var hasClients = connectionCount > 0; var allPaused = hasClients && selectedClients.every(function (client) { return Boolean(client && client.paused); }); var allBlackout = hasClients && selectedClients.every(function (client) { return Boolean(client && client.blackout); }); var connectionLabel = hasClients ? connectionCount + ' connected client' + (connectionCount === 1 ? '' : 's') : 'No clients connected'; if (pill) { pill.classList.toggle('is-live', hasClients); pill.classList.toggle('is-idle', !hasClients); pill.textContent = connectionLabel; } if (nameNode) { nameNode.textContent = selectedScreen ? String(selectedScreen.name || 'Selected screen') : 'Select a target screen group'; } if (metaNode) { metaNode.textContent = !selectedSlug ? 'Choose a screen group before sending commands.' : isAllSelected ? 'Commands sent here target every client across every screen group.' : 'Commands sent here target every client currently using this screen.'; } var commandTargetSlug = selectedSlug || ''; forms.forEach(function (form) { var command = String(form.getAttribute('data-screen-command-action') || '').trim().toLowerCase(); var commandInput = form.querySelector('input[name="command"]'); var button = form.querySelector('button[type="submit"]'); if (commandInput) { if (command === 'pause') { commandInput.value = allPaused ? 'pause' : 'pause'; var pauseStateInput = form.querySelector('input[name="paused"]'); if (pauseStateInput) { pauseStateInput.value = allPaused ? 'false' : 'true'; } if (button) { button.innerHTML = '' + (allPaused ? (isAllSelected ? 'Resume all screens' : 'Resume screen') : (isAllSelected ? 'Pause all screens' : 'Pause screen')); } setButtonVariant(button, ['btn-success', 'btn-info', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info'); form.setAttribute('data-confirm-message', allPaused ? (isAllSelected ? 'Resume all connected clients on all screens?' : 'Resume all connected clients on this screen?') : (isAllSelected ? 'Pause all connected clients on all screens?' : 'Pause all connected clients on this screen?')); } else if (command === 'blackout') { commandInput.value = 'blackout'; var blackoutStateInput = form.querySelector('input[name="blackout"]'); if (blackoutStateInput) { blackoutStateInput.value = allBlackout ? 'false' : 'true'; } if (button) { button.innerHTML = '' + (allBlackout ? (isAllSelected ? 'Restore all screens' : 'Restore screen') : (isAllSelected ? 'Blackout all screens' : 'Blackout screen')); } setButtonVariant(button, ['btn-success', 'btn-secondary', 'btn-danger', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary'); form.setAttribute('data-confirm-message', allBlackout ? (isAllSelected ? 'Restore all connected clients on all screens?' : 'Restore all connected clients on this screen?') : (isAllSelected ? 'Blackout all connected clients on all screens?' : 'Blackout all connected clients on this screen?')); } else { commandInput.value = command || commandInput.value || ''; } } form.action = commandTargetSlug ? '/clients/' + encodeURIComponent(commandTargetSlug) + '/commands' : '#'; if (command === 'reload') { form.setAttribute('data-confirm-message', isAllSelected ? 'Reload all screens?' : 'Reload selected screen?'); if (button) { button.innerHTML = '' + (isAllSelected ? 'Reload all screens' : 'Reload screen'); } } Array.prototype.slice.call(form.querySelectorAll('button, input')).forEach(function (control) { control.disabled = !commandTargetSlug; }); }); } function readScreenCommandStateFromDom() { var select = document.getElementById('screen-command-select'); if (!select) { return { screens: [] }; } return { screens: Array.prototype.slice.call(select.options || []).map(function (option) { return { slug: String(option.value || '').trim(), name: String(option.getAttribute('data-screen-name') || option.textContent || option.value || '').trim(), player_connection_count: Number(option.getAttribute('data-player-connection-count') || 0), playlist_name: String(option.getAttribute('data-playlist-name') || '').trim() }; }) }; } function updateDashboardQuickActions(state) { var pauseButton = document.getElementById('dashboard-pause-all-button'); var blackoutButton = document.getElementById('dashboard-blackout-all-button'); if ((!pauseButton && !blackoutButton) || !state || !Array.isArray(state.clients)) { return; } var hasClients = state.clients.length > 0; var allPaused = hasClients && state.clients.every(function (client) { return Boolean(client && client.paused); }); var allBlackout = hasClients && state.clients.every(function (client) { return Boolean(client && client.blackout); }); if (pauseButton) { var pauseForm = pauseButton.form; var pauseInput = pauseForm ? pauseForm.querySelector('input[name="paused"]') : null; var pauseLabel = allPaused ? 'Resume all clients' : 'Pause all clients'; var pauseButtonIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill'; pauseButton.innerHTML = '' + escapeHtml(pauseLabel); setButtonVariant(pauseButton, ['btn-success', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info'); if (pauseInput) { pauseInput.value = allPaused ? 'false' : 'true'; } if (pauseForm) { pauseForm.setAttribute('data-confirm-message', allPaused ? 'Resume all connected clients?' : 'Pause all connected clients?'); } pauseButton.setAttribute('aria-label', pauseLabel); } var label = allBlackout ? 'Restore all clients' : 'Blackout all clients'; var blackoutForm = blackoutButton.form; var blackoutInput = blackoutForm ? blackoutForm.querySelector('input[name="blackout"]') : null; var blackoutButtonIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash'; blackoutButton.innerHTML = '' + escapeHtml(label); setButtonVariant(blackoutButton, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], 'btn-secondary'); if (blackoutInput) { blackoutInput.value = allBlackout ? 'false' : 'true'; } if (blackoutForm) { blackoutForm.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients?' : 'Blackout all connected clients?'); } blackoutButton.setAttribute('aria-label', label); } function handleDashboardState(state) { if (!state) { return; } latestDashboardState = state; window.webLatestDashboardState = latestDashboardState; updateStats(state); updateScreenGrid(state); updateScreenCommandControls(state); updateClientTable(state); updateKioskLauncherModal(state); updateDashboardQuickActions(state); } function sendClientRename(screenSlug, connectionId, clientId, deviceId, clientName) { var body = new URLSearchParams(); body.append('command', 'setClientName'); body.append('connectionId', String(connectionId || '').trim()); body.append('clientId', String(clientId || '').trim()); body.append('deviceId', String(deviceId || '').trim()); body.append('clientName', String(clientName || '').trim()); return fetch('/clients/' + encodeURIComponent(String(screenSlug || '').trim()) + '/commands', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*' }, body: body.toString(), credentials: 'same-origin' }).then(function (response) { if (!response.ok) { return response.text().then(function (text) { var message = text || 'Unable to rename client.'; try { var payload = JSON.parse(text); message = payload && (payload.error || payload.message) ? String(payload.error || payload.message) : message; } catch (_error) { // fall back to the raw text body } throw new Error(message); }); } return response.json().catch(function () { return { ok: true }; }); }); } function initClientRenameHandler() { var table = document.getElementById('dashboard-clients-table'); var tbody = document.getElementById('dashboard-clients-table-body'); if (!table || !tbody) { return; } tbody.addEventListener('dblclick', function (event) { var cell = event.target && event.target.closest ? event.target.closest('td[data-label="Client"]') : null; if (!cell || !tbody.contains(cell)) { return; } var row = cell.parentElement; if (!row) { return; } var connectionId = String(row.getAttribute('data-client-key') || '').trim(); var clientId = String(row.getAttribute('data-client-id') || '').trim(); var deviceId = String(row.getAttribute('data-client-device-id') || '').trim(); var screenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim(); var currentName = String(cell.textContent || '').trim(); var nextName = window.prompt('Rename connected client', currentName && currentName !== 'Unknown' ? currentName : ''); if (nextName === null) { return; } nextName = String(nextName || '').trim(); if (!nextName) { window.alert('Client name is required.'); return; } if (!connectionId || !screenSlug) { window.alert('Unable to rename this client right now.'); return; } sendClientRename(screenSlug, connectionId, clientId, deviceId, nextName).then(function () { if (row && row.cells && row.cells[0]) { row.cells[0].innerHTML = '
' + escapeHtml(nextName) + '
'; } }).catch(function (error) { window.alert(error && error.message ? error.message : 'Unable to rename client.'); }); }); } function initClientMoveHandler() { var elements = getClientMoveModalElements(); if (!elements.modal || !elements.form || !elements.targetSelect) { return; } document.addEventListener('click', function (event) { var moveButton = event.target && event.target.closest ? event.target.closest('button[data-action="move-screen"]') : null; if (!moveButton) { return; } var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null; if (!row) { return; } updateClientMoveModalFromRow(row); }); elements.form.addEventListener('submit', function (event) { if (elements.form.dataset && elements.form.dataset.busy === 'true') { event.preventDefault(); return; } var targetScreenSlug = String(elements.targetSelect.value || '').trim(); if (!targetScreenSlug) { event.preventDefault(); window.alert('Choose a target screen.'); return; } event.preventDefault(); elements.form.dataset.busy = 'true'; var formData = new FormData(elements.form); var body = new URLSearchParams(); formData.forEach(function (value, key) { body.append(key, value); }); fetch(elements.form.action, { method: (elements.form.method || 'POST').toUpperCase(), headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*' }, body: body.toString(), credentials: 'same-origin' }).then(function (response) { if (!response.ok) { return response.text().then(function (text) { var error = new Error(text || 'Unable to move client.'); try { var payload = JSON.parse(text); if (payload && (payload.error || payload.message)) { error = new Error(String(payload.error || payload.message)); } } catch (_error) { // fall back to the raw text body } throw error; }); } if (window.pulseModal && typeof window.pulseModal.hide === 'function') { window.pulseModal.hide(elements.modal); } else if (window.bootstrap && window.bootstrap.Modal) { window.bootstrap.Modal.getOrCreateInstance(elements.modal).hide(); } return response.json().catch(function () { return { ok: true }; }); }).catch(function (error) { window.alert(error && error.message ? error.message : 'Unable to move client.'); }).finally(function () { delete elements.form.dataset.busy; }); }); } function initKioskLauncherModal() { var modal = document.getElementById('dashboard-kiosk-launcher-modal'); if (!modal) { return; } var checkbox = modal.querySelector('[data-kiosk-launcher-confirm]'); var select = modal.querySelector('[data-kiosk-launcher-player-select]'); var downloadLinks = Array.prototype.slice.call(modal.querySelectorAll('[data-kiosk-launcher-download]')); function resetModalState() { if (checkbox) { checkbox.checked = false; } if (select) { select.value = ''; } updateKioskLauncherModal(latestDashboardState); } if (checkbox) { checkbox.addEventListener('change', function () { updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom()); }); } if (select) { select.addEventListener('change', function () { updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom()); }); } modal.addEventListener('show.bs.modal', resetModalState); modal.addEventListener('hidden.bs.modal', resetModalState); modal.addEventListener('click', function (event) { var target = event.target && event.target.closest ? event.target.closest('[data-kiosk-launcher-download]') : null; if (target && target.classList.contains('disabled')) { event.preventDefault(); event.stopPropagation(); } }); resetModalState(); } window.webHandleDashboardState = handleDashboardState; window.webRefreshClientTableFromLatestState = refreshClientTableFromLatestState; initClientRenameHandler(); initClientMoveHandler(); initKioskLauncherModal(); var screenCommandSelect = document.getElementById('screen-command-select'); if (screenCommandSelect) { updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom()); screenCommandSelect.addEventListener('change', function () { updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom()); }); } }());