Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee3b1b51bf | ||
|
|
4e5a1bc393 |
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.6.9 - 2026-08-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The connected-clients view no longer exposes or sorts by client IP, so the table stays focused on the player identity, screen, and playback state.
|
||||||
|
- The connected-clients dashboard row renderer now keeps the actions column aligned after removing the IP column, so row updates no longer append a duplicate actions cell.
|
||||||
|
|
||||||
|
## 2.6.8 - 2026-08-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The screen-group command buttons stay disabled until a real target group is selected, so the placeholder "Select Screen Group" state cannot send commands.
|
||||||
|
|
||||||
## 2.6.7 - 2026-08-08
|
## 2.6.7 - 2026-08-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.6.7",
|
"version": "2.6.9",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ function normalizeRemoteAddress(value) {
|
|||||||
|
|
||||||
function formatPlayerConnectionLabel(deviceId, remoteAddress) {
|
function formatPlayerConnectionLabel(deviceId, remoteAddress) {
|
||||||
const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player';
|
const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player';
|
||||||
const normalizedRemoteAddress = normalizeRemoteAddress(remoteAddress);
|
return normalizedDeviceId;
|
||||||
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProxyBaseUrl(value) {
|
function normalizeProxyBaseUrl(value) {
|
||||||
@@ -359,7 +358,7 @@ async function start() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId, socket.bridgeRemoteAddress)} has disconnected`);
|
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId)} has disconnected`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMediaPath(fileName) {
|
function resolveMediaPath(fileName) {
|
||||||
@@ -828,7 +827,7 @@ async function start() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
playerSockets.set(deviceId, socket);
|
playerSockets.set(deviceId, socket);
|
||||||
logBridge(`Player ${formatPlayerConnectionLabel(deviceId, socket.bridgeRemoteAddress)} has connected`);
|
logBridge(`Player ${formatPlayerConnectionLabel(deviceId)} has connected`);
|
||||||
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
|
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -875,14 +874,12 @@ async function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyRequestAuth(request)) {
|
if (!verifyRequestAuth(request)) {
|
||||||
const remoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
|
logBridge('Player denied with wrong shared secret');
|
||||||
logBridge(remoteAddress ? `Player (ip ${remoteAddress}) denied with wrong shared secret` : 'Player denied with wrong shared secret');
|
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playersWs.handleUpgrade(request, socket, head, function (ws) {
|
playersWs.handleUpgrade(request, socket, head, function (ws) {
|
||||||
ws.bridgeRemoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
|
|
||||||
playersWs.emit('connection', ws, request);
|
playersWs.emit('connection', ws, request);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ async function start() {
|
|||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
}, authHeaders)
|
}, authHeaders)
|
||||||
});
|
});
|
||||||
|
|
||||||
return Boolean(response && response.ok);
|
return Boolean(response && response.ok);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+71
-14
@@ -45,6 +45,76 @@ function createPlayerRuntime(options) {
|
|||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstHeaderValue(value) {
|
||||||
|
return String(value || '').trim().split(',')[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateOrReservedIp(ip) {
|
||||||
|
const normalized = normalizeClientIp(ip);
|
||||||
|
if (!normalized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = normalized.toLowerCase();
|
||||||
|
if (lower === 'localhost' || lower === '::1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^10\./.test(lower) || /^192\.168\./.test(lower) || /^127\./.test(lower) || /^169\.254\./.test(lower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(lower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.startsWith('fc') || lower.startsWith('fd') || lower.startsWith('fe80:') || lower.startsWith('::ffff:127.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickForwardedIp(candidates) {
|
||||||
|
const normalizedCandidates = Array.isArray(candidates)
|
||||||
|
? candidates.map(function (candidate) {
|
||||||
|
return normalizeClientIp(candidate);
|
||||||
|
}).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const publicCandidate = normalizedCandidates.find(function (candidate) {
|
||||||
|
return !isPrivateOrReservedIp(candidate);
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicCandidate || normalizedCandidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRequestIp(request) {
|
||||||
|
const forwardedFor = String(request && request.headers && request.headers['x-forwarded-for'] || '').split(',');
|
||||||
|
const forwardedForIp = pickForwardedIp(forwardedFor);
|
||||||
|
if (forwardedForIp) {
|
||||||
|
return forwardedForIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realIp = pickForwardedIp([firstHeaderValue(request && request.headers && request.headers['x-real-ip'])]);
|
||||||
|
if (realIp) {
|
||||||
|
return realIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwarded = firstHeaderValue(request && request.headers && request.headers.forwarded);
|
||||||
|
if (forwarded) {
|
||||||
|
const forwardedMatches = Array.from(forwarded.matchAll(/(?:^|,\s*|;\s*)for=(?:"?\[?)([^"\];,\s]+)/gi)).map(function (match) {
|
||||||
|
return match[1];
|
||||||
|
});
|
||||||
|
const forwardedIp = pickForwardedIp(forwardedMatches);
|
||||||
|
if (forwardedIp) {
|
||||||
|
return forwardedIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeClientIp(request && request.socket && request.socket.remoteAddress);
|
||||||
|
}
|
||||||
|
|
||||||
function parseCookies(cookieHeader) {
|
function parseCookies(cookieHeader) {
|
||||||
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
|
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
|
||||||
if (!pair) {
|
if (!pair) {
|
||||||
@@ -145,7 +215,6 @@ function createPlayerRuntime(options) {
|
|||||||
const clientName = String(connection.clientName || '').trim();
|
const clientName = String(connection.clientName || '').trim();
|
||||||
const clientId = String(connection.clientId || '').trim();
|
const clientId = String(connection.clientId || '').trim();
|
||||||
const userAgent = String(connection.userAgent || '').trim();
|
const userAgent = String(connection.userAgent || '').trim();
|
||||||
const clientIp = String(connection.clientIp || '').trim();
|
|
||||||
const viewport = connection.viewport && typeof connection.viewport === 'object'
|
const viewport = connection.viewport && typeof connection.viewport === 'object'
|
||||||
? connection.viewport
|
? connection.viewport
|
||||||
: null;
|
: null;
|
||||||
@@ -161,10 +230,6 @@ function createPlayerRuntime(options) {
|
|||||||
labelParts.push(`id ${clientId.slice(-6)}`);
|
labelParts.push(`id ${clientId.slice(-6)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientIp) {
|
|
||||||
labelParts.push(clientIp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewport && Number.isFinite(Number(viewport.width)) && Number.isFinite(Number(viewport.height))) {
|
if (viewport && Number.isFinite(Number(viewport.width)) && Number.isFinite(Number(viewport.height))) {
|
||||||
labelParts.push(`${Number(viewport.width)}x${Number(viewport.height)}`);
|
labelParts.push(`${Number(viewport.width)}x${Number(viewport.height)}`);
|
||||||
}
|
}
|
||||||
@@ -198,8 +263,6 @@ function createPlayerRuntime(options) {
|
|||||||
blackout: Boolean(connection.blackout),
|
blackout: Boolean(connection.blackout),
|
||||||
currentSlideId: connection.currentSlide && connection.currentSlide.id ? connection.currentSlide.id : null,
|
currentSlideId: connection.currentSlide && connection.currentSlide.id ? connection.currentSlide.id : null,
|
||||||
currentSlideTitle: connection.currentSlide && connection.currentSlide.title ? connection.currentSlide.title : null,
|
currentSlideTitle: connection.currentSlide && connection.currentSlide.title ? connection.currentSlide.title : null,
|
||||||
clientIp: connection.clientIp || null,
|
|
||||||
remoteAddress: connection.remoteAddress || null,
|
|
||||||
connectedAt: connection.connectedAt ? connection.connectedAt.toISOString() : null,
|
connectedAt: connection.connectedAt ? connection.connectedAt.toISOString() : null,
|
||||||
lastSeenAt: connection.lastSeenAt ? connection.lastSeenAt.toISOString() : null
|
lastSeenAt: connection.lastSeenAt ? connection.lastSeenAt.toISOString() : null
|
||||||
};
|
};
|
||||||
@@ -431,9 +494,6 @@ function createPlayerRuntime(options) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteAddress = request.socket && request.socket.remoteAddress ? request.socket.remoteAddress : null;
|
|
||||||
const forwardedFor = normalizeClientIp(String(request.headers['x-forwarded-for'] || '').split(',')[0]);
|
|
||||||
const normalizedRemoteAddress = normalizeClientIp(remoteAddress);
|
|
||||||
const connectionId = crypto.randomUUID();
|
const connectionId = crypto.randomUUID();
|
||||||
const connection = {
|
const connection = {
|
||||||
id: connectionId,
|
id: connectionId,
|
||||||
@@ -448,9 +508,7 @@ function createPlayerRuntime(options) {
|
|||||||
paused: false,
|
paused: false,
|
||||||
blackout: false,
|
blackout: false,
|
||||||
playerPublicBaseUrl: null,
|
playerPublicBaseUrl: null,
|
||||||
clientIp: forwardedFor || normalizedRemoteAddress,
|
label: 'connected client',
|
||||||
remoteAddress: normalizedRemoteAddress,
|
|
||||||
label: forwardedFor || normalizedRemoteAddress || 'connected client',
|
|
||||||
connectedAt: new Date(),
|
connectedAt: new Date(),
|
||||||
lastSeenAt: new Date()
|
lastSeenAt: new Date()
|
||||||
};
|
};
|
||||||
@@ -491,7 +549,6 @@ function createPlayerRuntime(options) {
|
|||||||
}
|
}
|
||||||
connection.paused = Boolean(payload.paused);
|
connection.paused = Boolean(payload.paused);
|
||||||
connection.blackout = Boolean(payload.blackout);
|
connection.blackout = Boolean(payload.blackout);
|
||||||
connection.clientIp = payload.clientIp ? normalizeClientIp(payload.clientIp) || connection.clientIp : connection.clientIp;
|
|
||||||
connection.currentSlide = payload.currentSlide && typeof payload.currentSlide === 'object' ? {
|
connection.currentSlide = payload.currentSlide && typeof payload.currentSlide === 'object' ? {
|
||||||
id: payload.currentSlide.id || null,
|
id: payload.currentSlide.id || null,
|
||||||
title: payload.currentSlide.title || '',
|
title: payload.currentSlide.title || '',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selectedSlug = String(select.value || '').trim();
|
var selectedSlug = String(select.value || '').trim();
|
||||||
|
var hasSelectedGroup = Boolean(selectedSlug);
|
||||||
var actionTarget = '/clients/' + encodeURIComponent(selectedSlug || '__all__') + '/commands';
|
var actionTarget = '/clients/' + encodeURIComponent(selectedSlug || '__all__') + '/commands';
|
||||||
var selectedName = getSelectedScreenLabel();
|
var selectedName = getSelectedScreenLabel();
|
||||||
var selectedClients = getSelectedScreenClients(latestDashboardState);
|
var selectedClients = getSelectedScreenClients(latestDashboardState);
|
||||||
@@ -134,6 +135,10 @@
|
|||||||
var action = String(form.getAttribute('data-screen-command-action') || '').trim();
|
var action = String(form.getAttribute('data-screen-command-action') || '').trim();
|
||||||
var button = form.querySelector('button[type="submit"]');
|
var button = form.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = !hasSelectedGroup;
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'pause' || action === 'blackout') {
|
if (action === 'pause' || action === 'blackout') {
|
||||||
updateToggleButton(button, form, latestDashboardState);
|
updateToggleButton(button, form, latestDashboardState);
|
||||||
return;
|
return;
|
||||||
@@ -240,8 +245,6 @@
|
|||||||
client && client.slug,
|
client && client.slug,
|
||||||
client && client.screen_slug,
|
client && client.screen_slug,
|
||||||
client && client.screen_name,
|
client && client.screen_name,
|
||||||
client && client.ipAddress,
|
|
||||||
client && client.clientIp,
|
|
||||||
client && client.status,
|
client && client.status,
|
||||||
client && client.currentSlideTitle
|
client && client.currentSlideTitle
|
||||||
];
|
];
|
||||||
@@ -259,7 +262,6 @@
|
|||||||
client: function (client) { return String(client && (getClientDisplayName(client) || client.client_name || client.name || client.clientId) || '').trim(); },
|
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(); },
|
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
||||||
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
||||||
ip: function (client) { return String(client && client.clientIp || '').trim(); },
|
|
||||||
viewport: function (client) {
|
viewport: function (client) {
|
||||||
var viewport = client && client.viewport;
|
var viewport = client && client.viewport;
|
||||||
if (!viewport || !viewport.width || !viewport.height) {
|
if (!viewport || !viewport.width || !viewport.height) {
|
||||||
@@ -278,12 +280,6 @@
|
|||||||
? [normalizedSortKey]
|
? [normalizedSortKey]
|
||||||
: ['client'];
|
: ['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) {
|
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
|
||||||
for (var index = 0; index < sortKeys.length; index += 1) {
|
for (var index = 0; index < sortKeys.length; index += 1) {
|
||||||
var sortKeyName = sortKeys[index];
|
var sortKeyName = sortKeys[index];
|
||||||
@@ -534,13 +530,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasActionsColumn) {
|
if (!hasActionsColumn) {
|
||||||
if (row.cells.length > 6) {
|
if (row.cells.length > 5) {
|
||||||
row.deleteCell(row.cells.length - 1);
|
row.deleteCell(row.cells.length - 1);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionCell = row.cells.length > 6 ? row.cells[6] : null;
|
var actionCell = row.cells.length > 5 ? row.cells[5] : null;
|
||||||
if (!actionCell) {
|
if (!actionCell) {
|
||||||
actionCell = row.insertCell(-1);
|
actionCell = row.insertCell(-1);
|
||||||
actionCell.setAttribute('data-label', 'Actions');
|
actionCell.setAttribute('data-label', 'Actions');
|
||||||
@@ -552,8 +548,6 @@
|
|||||||
|
|
||||||
function renderClientRow(client, hasActionsColumn) {
|
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 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 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 clientNameValue = getClientDisplayName(client);
|
||||||
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
||||||
@@ -566,7 +560,6 @@
|
|||||||
'<td data-label="Client"><div>' + clientName + '</div></td>',
|
'<td data-label="Client"><div>' + clientName + '</div></td>',
|
||||||
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
|
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
|
||||||
'<td data-label="Current Slide">' + currentSlide + '</td>',
|
'<td data-label="Current Slide">' + currentSlide + '</td>',
|
||||||
'<td data-label="IP">' + clientIp + '</td>',
|
|
||||||
'<td data-label="Viewport">' + viewport + '</td>',
|
'<td data-label="Viewport">' + viewport + '</td>',
|
||||||
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
|
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
|
||||||
actionCell,
|
actionCell,
|
||||||
@@ -575,13 +568,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateClientRowCells(row, client, hasActionsColumn) {
|
function updateClientRowCells(row, client, hasActionsColumn) {
|
||||||
if (!row || !row.cells || row.cells.length < 6) {
|
if (!row || !row.cells || row.cells.length < 5) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 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 clientNameValue = getClientDisplayName(client);
|
||||||
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
||||||
@@ -598,9 +589,8 @@
|
|||||||
setCellHtml(row.cells[0], '<div>' + clientName + '</div>');
|
setCellHtml(row.cells[0], '<div>' + clientName + '</div>');
|
||||||
setCellHtml(row.cells[1], '<div>' + screenName + '</div>');
|
setCellHtml(row.cells[1], '<div>' + screenName + '</div>');
|
||||||
setCellHtml(row.cells[2], currentSlide);
|
setCellHtml(row.cells[2], currentSlide);
|
||||||
setCellHtml(row.cells[3], clientIp);
|
setCellHtml(row.cells[3], viewport);
|
||||||
setCellHtml(row.cells[4], viewport);
|
setCellHtml(row.cells[4], connectedAt);
|
||||||
setCellHtml(row.cells[5], connectedAt);
|
|
||||||
|
|
||||||
syncClientActionCell(row, client, hasActionsColumn);
|
syncClientActionCell(row, client, hasActionsColumn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
|||||||
client: function (client) { return String(client && (client.client_name || client.name || client.clientId) || '').trim(); },
|
client: function (client) { return String(client && (client.client_name || client.name || client.clientId) || '').trim(); },
|
||||||
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
||||||
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
||||||
ip: function (client) { return String(client && client.clientIp || '').trim(); },
|
|
||||||
viewport: function (client) {
|
viewport: function (client) {
|
||||||
const viewport = client && client.viewport;
|
const viewport = client && client.viewport;
|
||||||
if (!viewport || !viewport.width || !viewport.height) {
|
if (!viewport || !viewport.width || !viewport.height) {
|
||||||
@@ -37,12 +36,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
|||||||
? [normalizedSortKey]
|
? [normalizedSortKey]
|
||||||
: ['client'];
|
: ['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) {
|
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
|
||||||
for (let index = 0; index < sortKeys.length; index += 1) {
|
for (let index = 0; index < sortKeys.length; index += 1) {
|
||||||
const sortKeyName = sortKeys[index];
|
const sortKeyName = sortKeys[index];
|
||||||
@@ -71,7 +64,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
|||||||
'deviceId',
|
'deviceId',
|
||||||
'slug',
|
'slug',
|
||||||
'screen_slug',
|
'screen_slug',
|
||||||
'ipAddress',
|
|
||||||
'status'
|
'status'
|
||||||
]);
|
]);
|
||||||
const filteredClients = (data.clients || []).filter(matchesSearch);
|
const filteredClients = (data.clients || []).filter(matchesSearch);
|
||||||
|
|||||||
@@ -42,17 +42,17 @@
|
|||||||
<div class="screen-command-actions">
|
<div class="screen-command-actions">
|
||||||
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-screen-command-form data-screen-command-action="reload" data-async-command>
|
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-screen-command-form data-screen-command-action="reload" data-async-command>
|
||||||
<input type="hidden" name="command" value="reload" />
|
<input type="hidden" name="command" value="reload" />
|
||||||
<button type="submit" class="btn btn-sm btn-danger"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
<button type="submit" class="btn btn-sm btn-danger" disabled><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-screen-command-form data-screen-command-action="pause" data-async-command>
|
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-screen-command-form data-screen-command-action="pause" data-async-command>
|
||||||
<input type="hidden" name="command" value="pause" />
|
<input type="hidden" name="command" value="pause" />
|
||||||
<input type="hidden" name="paused" value="true" />
|
<input type="hidden" name="paused" value="true" />
|
||||||
<button type="submit" class="btn btn-sm btn-info"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
<button type="submit" class="btn btn-sm btn-info" disabled><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-screen-command-form data-screen-command-action="blackout" data-async-command>
|
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-screen-command-form data-screen-command-action="blackout" data-async-command>
|
||||||
<input type="hidden" name="command" value="blackout" />
|
<input type="hidden" name="command" value="blackout" />
|
||||||
<input type="hidden" name="blackout" value="true" />
|
<input type="hidden" name="blackout" value="true" />
|
||||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
<button type="submit" class="btn btn-sm btn-secondary" disabled><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
<th data-table-sort-key="client">Client</th>
|
<th data-table-sort-key="client">Client</th>
|
||||||
<th data-table-sort-key="screen">Current Screen Group</th>
|
<th data-table-sort-key="screen">Current Screen Group</th>
|
||||||
<th data-table-sort-key="slide">Current Slide</th>
|
<th data-table-sort-key="slide">Current Slide</th>
|
||||||
<th data-table-sort-key="ip">IP</th>
|
|
||||||
<th data-table-sort-key="viewport">Viewport</th>
|
<th data-table-sort-key="viewport">Viewport</th>
|
||||||
<th data-table-sort-key="connected">Connected/Updated</th>
|
<th data-table-sort-key="connected">Connected/Updated</th>
|
||||||
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
|
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
|
||||||
@@ -111,7 +110,6 @@
|
|||||||
<span class="empty">No slide currently showing</span>
|
<span class="empty">No slide currently showing</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="IP">{{#if clientIp}}{{clientIp}}{{else}}<span class="empty">Unknown</span>{{/if}}</td>
|
|
||||||
<td data-label="Viewport">
|
<td data-label="Viewport">
|
||||||
{{#if viewport}}
|
{{#if viewport}}
|
||||||
{{viewport.width}}x{{viewport.height}}
|
{{viewport.width}}x{{viewport.height}}
|
||||||
@@ -169,7 +167,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
|
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}6{{else}}5{{/if}}" class="empty">No connected clients yet.</td></tr>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -342,3 +342,181 @@ test('dashboard move client button opens the move modal for the selected row', (
|
|||||||
assert.equal(clientNameInput.value, 'Lobby Client');
|
assert.equal(clientNameInput.value, 'Lobby Client');
|
||||||
assert.equal(playerBaseUrlInput.value, 'http://player.local');
|
assert.equal(playerBaseUrlInput.value, 'http://player.local');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dashboard screen group controls stay disabled until a group is selected', () => {
|
||||||
|
const script = fs.readFileSync(require.resolve('../src/web/public/js/dashboard/dashboard-page.js'), 'utf8');
|
||||||
|
const pauseInput = { value: 'true' };
|
||||||
|
const blackoutInput = { value: 'true' };
|
||||||
|
const reloadButton = {
|
||||||
|
disabled: false,
|
||||||
|
innerHTML: '',
|
||||||
|
setAttribute() {},
|
||||||
|
classList: { add() {}, remove() {} }
|
||||||
|
};
|
||||||
|
const pauseButton = {
|
||||||
|
disabled: false,
|
||||||
|
innerHTML: '',
|
||||||
|
setAttribute() {},
|
||||||
|
classList: { add() {}, remove() {} }
|
||||||
|
};
|
||||||
|
const blackoutButton = {
|
||||||
|
disabled: false,
|
||||||
|
innerHTML: '',
|
||||||
|
setAttribute() {},
|
||||||
|
classList: { add() {}, remove() {} }
|
||||||
|
};
|
||||||
|
const forms = [
|
||||||
|
{
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'data-screen-command-action') {
|
||||||
|
return 'reload';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
setAttribute() {},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === 'button[type="submit"]') {
|
||||||
|
return reloadButton;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'data-screen-command-action') {
|
||||||
|
return 'pause';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
setAttribute() {},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === 'button[type="submit"]') {
|
||||||
|
return pauseButton;
|
||||||
|
}
|
||||||
|
if (selector === 'input[name="paused"]') {
|
||||||
|
return pauseInput;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'data-screen-command-action') {
|
||||||
|
return 'blackout';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
setAttribute() {},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === 'button[type="submit"]') {
|
||||||
|
return blackoutButton;
|
||||||
|
}
|
||||||
|
if (selector === 'input[name="blackout"]') {
|
||||||
|
return blackoutInput;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const select = {
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ textContent: 'Select Screen Group', getAttribute() { return ''; } },
|
||||||
|
{ textContent: 'Lobby', getAttribute(name) { return name === 'data-screen-name' ? 'Lobby' : ''; } },
|
||||||
|
{ textContent: 'All Screens', getAttribute(name) { return name === 'data-screen-name' ? 'All screens' : ''; } }
|
||||||
|
],
|
||||||
|
selectedIndex: 0,
|
||||||
|
dataset: {},
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
if (type === 'change') {
|
||||||
|
this.changeHandler = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const context = {
|
||||||
|
document: {
|
||||||
|
body: {
|
||||||
|
classList: {
|
||||||
|
toggle() {},
|
||||||
|
add() {},
|
||||||
|
remove() {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getElementById(id) {
|
||||||
|
if (id === 'screen-command-select') {
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
if (id === 'dashboard-clients-table') {
|
||||||
|
return { getAttribute() { return 'false'; } };
|
||||||
|
}
|
||||||
|
if (id === 'dashboard-clients-table-body') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === '[data-screen-command-select]') {
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === '[data-screen-command-form]') {
|
||||||
|
return forms;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
addEventListener() {}
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
location: {
|
||||||
|
search: '',
|
||||||
|
protocol: 'http:',
|
||||||
|
host: 'example.test'
|
||||||
|
},
|
||||||
|
webUiHelpers: {
|
||||||
|
escapeHtml(value) { return String(value); },
|
||||||
|
formatDashboardDate(value) { return String(value); },
|
||||||
|
getClientRowKey(client) { return String(client && client.id || ''); },
|
||||||
|
getClientDisplayName(client) { return String(client && (client.client_name || client.name || client.clientId) || ''); },
|
||||||
|
setButtonVariant() {},
|
||||||
|
normalizeDisplayIp(value) { return String(value); }
|
||||||
|
},
|
||||||
|
WebSocket: null,
|
||||||
|
setTimeout() { return 1; },
|
||||||
|
clearTimeout() {},
|
||||||
|
alert() {},
|
||||||
|
prompt() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
webHandleDashboardState() {}
|
||||||
|
},
|
||||||
|
WebSocket: function MockWebSocket() {},
|
||||||
|
JSON: JSON,
|
||||||
|
Number: Number,
|
||||||
|
String: String,
|
||||||
|
Boolean: Boolean,
|
||||||
|
Array: Array,
|
||||||
|
Object: Object,
|
||||||
|
Math: Math,
|
||||||
|
Set: Set,
|
||||||
|
URLSearchParams: URLSearchParams,
|
||||||
|
FormData: function FormData() {}
|
||||||
|
};
|
||||||
|
context.window.document = context.document;
|
||||||
|
context.window.WebSocket = context.WebSocket;
|
||||||
|
|
||||||
|
vm.runInNewContext(script, context);
|
||||||
|
|
||||||
|
assert.equal(reloadButton.disabled, true);
|
||||||
|
assert.equal(pauseButton.disabled, true);
|
||||||
|
assert.equal(blackoutButton.disabled, true);
|
||||||
|
|
||||||
|
select.value = 'lobby';
|
||||||
|
select.selectedIndex = 1;
|
||||||
|
select.changeHandler();
|
||||||
|
|
||||||
|
assert.equal(reloadButton.disabled, false);
|
||||||
|
assert.equal(pauseButton.disabled, false);
|
||||||
|
assert.equal(blackoutButton.disabled, false);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user