Files
pulse-signage/src/web/public/js/dashboard/dashboard-page.js
T
2026-08-03 12:45:27 +01:00

788 lines
36 KiB
JavaScript

(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 latestDashboardState = null;
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]')
};
}
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 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;
}
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 [
'<div class="actions justify-content-end">',
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></form>',
'<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" data-bs-toggle="modal" data-bs-target="#client-move-screen-modal" aria-label="Move client to another screen" title="Move client to another screen"><i class="bi bi-display" aria-hidden="true"></i></button>',
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="previous" aria-label="Previous slide" title="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form>',
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="next" aria-label="Next slide" title="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form>',
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause" aria-label="' + pauseButtonLabel + '" title="' + pauseButtonLabel + '"><i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonText + '</button></form>',
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + (blackout ? 'false' : 'true') + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout" aria-label="' + blackoutButtonLabel + '" title="' + blackoutButtonLabel + '"><i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonText + '</button></form>',
'</div>'
].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 = '<i class="bi ' + (paused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (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 || '';
}
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 = '<i class="bi bi-arrow-repeat" aria-hidden="true"></i>';
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 || '';
}
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 = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + (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 || '';
}
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 || '';
}
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 || '';
}
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 ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
var clientIpValue = normalizeDisplayIp(client.clientIp);
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
var clientNameValue = getClientDisplayName(client);
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
var screenName = client.screen_name ? escapeHtml(client.screen_name) : '<span class="empty">Unknown</span>';
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : '<span class="empty">No slide currently showing</span>';
var actionCell = hasActionsColumn ? '<td data-label="Actions">' + renderClientActionCell(client) + '</td>' : '';
return [
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '" data-client-id="' + escapeHtml(client.clientId || '') + '" data-client-device-id="' + escapeHtml(client.deviceId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '">',
'<td data-label="Client"><div>' + clientName + '</div></td>',
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
'<td data-label="Current Slide">' + currentSlide + '</td>',
'<td data-label="IP">' + clientIp + '</td>',
'<td data-label="Viewport">' + viewport + '</td>',
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
actionCell,
'</tr>'
].join('');
}
function renderScreenTile(screen) {
var clientCount = Number(screen.player_connection_count || 0);
var playerUrl = String(screen.player_url || '').trim();
var connectionLabel = clientCount ? clientCount + ' live' : 'No clients';
var connectionStateClass = clientCount ? 'is-live' : 'is-idle';
var playlistLabel = screen.playlist_name ? escapeHtml(screen.playlist_name) : 'Unassigned';
var connectionsLabel = clientCount ? clientCount + ' connected' : 'No clients connected';
return [
'<article class="dashboard-screen-tile" data-screen-key="' + escapeHtml(screen.id || '') + '">',
'<div class="dashboard-screen-tile-top">',
'<div class="dashboard-screen-tile-text">',
'<h4 class="dashboard-screen-name">' + escapeHtml(screen.name || '') + '</h4>',
'<a class="dashboard-screen-link" href="' + escapeHtml(playerUrl) + '" target="_blank">' + escapeHtml(playerUrl) + '</a>',
'</div>',
'<span class="dashboard-screen-pill ' + connectionStateClass + '">' + escapeHtml(connectionLabel) + '</span>',
'</div>',
'<dl class="dashboard-screen-meta">',
'<div><dt>Playlist</dt><dd>' + playlistLabel + '</dd></div>',
'<div><dt>Connections</dt><dd>' + escapeHtml(connectionsLabel) + '</dd></div>',
'</dl>',
'</article>'
].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 clientCount = document.getElementById('dashboard-client-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 (clientCount) {
clientCount.textContent = String(Number(state.connectedClientsCount || 0));
}
}
function updateClientTable(state) {
var tbody = document.getElementById('dashboard-clients-table-body');
if (!tbody || !Array.isArray(state.clients)) {
return;
}
var table = document.getElementById('dashboard-clients-table');
var hasActionsColumn = Boolean(table && String(table.getAttribute('data-has-actions-column') || '').toLowerCase() === 'true');
if (!state.clients.length) {
tbody.innerHTML = '<tr><td colspan="' + (hasActionsColumn ? '7' : '6') + '" class="empty">No connected clients yet.</td></tr>';
return;
}
var existingRows = {};
Array.prototype.slice.call(tbody.querySelectorAll('tr[data-client-key]')).forEach(function (row) {
existingRows[row.getAttribute('data-client-key')] = row;
});
Array.prototype.slice.call(tbody.querySelectorAll('tr')).forEach(function (row) {
if (!row.hasAttribute('data-client-key')) {
row.parentNode.removeChild(row);
}
});
state.clients.forEach(function (client, index) {
var rowKey = getClientRowKey(client);
var row = existingRows[rowKey];
if (!row) {
var tempBody = document.createElement('tbody');
tempBody.innerHTML = renderClientRow(client, hasActionsColumn);
row = tempBody.firstElementChild;
}
if (!row) {
return;
}
row.setAttribute('data-client-key', rowKey);
row.setAttribute('data-client-id', client.clientId || '');
row.setAttribute('data-client-device-id', client.deviceId || '');
row.setAttribute('data-client-screen-slug', client.screen_slug || '');
if (row.cells && row.cells.length >= 6) {
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
var clientIpValue = normalizeDisplayIp(client.clientIp);
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
var clientNameValue = getClientDisplayName(client);
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
var screenName = client.screen_name ? escapeHtml(client.screen_name) : '<span class="empty">Unknown</span>';
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : '<span class="empty">No slide currently showing</span>';
row.cells[0].innerHTML = '<div>' + clientName + '</div>';
row.cells[1].innerHTML = '<div>' + screenName + '</div>';
row.cells[2].innerHTML = currentSlide;
row.cells[3].innerHTML = clientIp;
row.cells[4].innerHTML = viewport;
row.cells[5].innerHTML = connectedAt;
syncClientActionCell(row, client, hasActionsColumn);
}
var referenceNode = tbody.children[index] || null;
if (referenceNode !== row) {
tbody.insertBefore(row, referenceNode);
}
});
while (tbody.children.length > state.clients.length) {
tbody.removeChild(tbody.lastElementChild);
}
}
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 = '<div class="empty dashboard-screen-empty">No screens yet.</div>';
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;
if (!select.value || !screenBySlug[select.value]) {
select.value = screens[0].slug || '';
}
var selectedScreen = screenBySlug[select.value] || screens[0];
var selectedSlug = String(selectedScreen && selectedScreen.slug || '').trim();
var selectedClients = Array.isArray(state && state.clients) ? state.clients.filter(function (client) {
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 = String(selectedScreen && selectedScreen.name || 'Selected screen');
}
if (metaNode) {
metaNode.textContent = 'Commands sent here target every client currently using this screen.';
}
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 = '<i class="bi ' + (allPaused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (allPaused ? 'Resume screen' : '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 ? 'Resume all connected clients on this screen?' : '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 = '<i class="bi ' + (allBlackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (allBlackout ? 'Restore screen' : '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 ? 'Restore all connected clients on this screen?' : 'Blackout all connected clients on this screen?');
} else {
commandInput.value = command || commandInput.value || '';
}
}
form.action = selectedSlug ? '/clients/' + encodeURIComponent(selectedSlug) + '/commands' : '#';
if (command === 'reload') {
form.setAttribute('data-confirm-message', 'Reload selected screen?');
if (button) {
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen';
}
}
Array.prototype.slice.call(form.querySelectorAll('button, input')).forEach(function (control) {
control.disabled = !selectedSlug;
});
});
}
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 = '<i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + 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 = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + 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;
updateStats(state);
updateScreenGrid(state);
updateScreenCommandControls(state);
updateClientTable(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 = '<div>' + escapeHtml(nextName) + '</div>';
}
}).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;
});
});
}
window.webHandleDashboardState = handleDashboardState;
initClientRenameHandler();
initClientMoveHandler();
var screenCommandSelect = document.getElementById('screen-command-select');
if (screenCommandSelect) {
updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom());
screenCommandSelect.addEventListener('change', function () {
updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom());
});
}
}());