diff --git a/CHANGELOG.md b/CHANGELOG.md index d60c8ed..1ae30f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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 diff --git a/package.json b/package.json index 896db7e..c4cfac8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.6.8", + "version": "2.6.9", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/player-bridge/index.js b/src/player-bridge/index.js index 886d686..0f9eb9e 100644 --- a/src/player-bridge/index.js +++ b/src/player-bridge/index.js @@ -40,8 +40,7 @@ function normalizeRemoteAddress(value) { function formatPlayerConnectionLabel(deviceId, remoteAddress) { const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player'; - const normalizedRemoteAddress = normalizeRemoteAddress(remoteAddress); - return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId; + return normalizedDeviceId; } function normalizeProxyBaseUrl(value) { @@ -359,7 +358,7 @@ async function start() { return; } - logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId, socket.bridgeRemoteAddress)} has disconnected`); + logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId)} has disconnected`); } function resolveMediaPath(fileName) { @@ -828,7 +827,7 @@ async function start() { }); 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 })); return; } @@ -875,14 +874,12 @@ async function start() { } if (!verifyRequestAuth(request)) { - const remoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress); - logBridge(remoteAddress ? `Player (ip ${remoteAddress}) denied with wrong shared secret` : 'Player denied with wrong shared secret'); + logBridge('Player denied with wrong shared secret'); socket.destroy(); return; } playersWs.handleUpgrade(request, socket, head, function (ws) { - ws.bridgeRemoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress); playersWs.emit('connection', ws, request); }); }); diff --git a/src/player.js b/src/player.js index 1f12a9c..bad8577 100644 --- a/src/player.js +++ b/src/player.js @@ -114,6 +114,7 @@ async function start() { Accept: 'application/json' }, authHeaders) }); + return Boolean(response && response.ok); } catch (_error) { return false; diff --git a/src/player/runtime.js b/src/player/runtime.js index 61d2e8b..3229e78 100644 --- a/src/player/runtime.js +++ b/src/player/runtime.js @@ -45,6 +45,76 @@ function createPlayerRuntime(options) { 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) { return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) { if (!pair) { @@ -145,7 +215,6 @@ function createPlayerRuntime(options) { const clientName = String(connection.clientName || '').trim(); const clientId = String(connection.clientId || '').trim(); const userAgent = String(connection.userAgent || '').trim(); - const clientIp = String(connection.clientIp || '').trim(); const viewport = connection.viewport && typeof connection.viewport === 'object' ? connection.viewport : null; @@ -161,10 +230,6 @@ function createPlayerRuntime(options) { labelParts.push(`id ${clientId.slice(-6)}`); } - if (clientIp) { - labelParts.push(clientIp); - } - if (viewport && Number.isFinite(Number(viewport.width)) && Number.isFinite(Number(viewport.height))) { labelParts.push(`${Number(viewport.width)}x${Number(viewport.height)}`); } @@ -198,8 +263,6 @@ function createPlayerRuntime(options) { blackout: Boolean(connection.blackout), currentSlideId: connection.currentSlide && connection.currentSlide.id ? connection.currentSlide.id : 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, lastSeenAt: connection.lastSeenAt ? connection.lastSeenAt.toISOString() : null }; @@ -431,9 +494,6 @@ function createPlayerRuntime(options) { 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 connection = { id: connectionId, @@ -448,9 +508,7 @@ function createPlayerRuntime(options) { paused: false, blackout: false, playerPublicBaseUrl: null, - clientIp: forwardedFor || normalizedRemoteAddress, - remoteAddress: normalizedRemoteAddress, - label: forwardedFor || normalizedRemoteAddress || 'connected client', + label: 'connected client', connectedAt: new Date(), lastSeenAt: new Date() }; @@ -491,7 +549,6 @@ function createPlayerRuntime(options) { } connection.paused = Boolean(payload.paused); connection.blackout = Boolean(payload.blackout); - connection.clientIp = payload.clientIp ? normalizeClientIp(payload.clientIp) || connection.clientIp : connection.clientIp; connection.currentSlide = payload.currentSlide && typeof payload.currentSlide === 'object' ? { id: payload.currentSlide.id || null, title: payload.currentSlide.title || '', diff --git a/src/web/public/js/dashboard/dashboard-page.js b/src/web/public/js/dashboard/dashboard-page.js index d8e2387..4b4edae 100644 --- a/src/web/public/js/dashboard/dashboard-page.js +++ b/src/web/public/js/dashboard/dashboard-page.js @@ -245,8 +245,6 @@ client && client.slug, client && client.screen_slug, client && client.screen_name, - client && client.ipAddress, - client && client.clientIp, client && client.status, client && client.currentSlideTitle ]; @@ -264,7 +262,6 @@ 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) { @@ -283,12 +280,6 @@ ? [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]; @@ -539,13 +530,13 @@ } if (!hasActionsColumn) { - if (row.cells.length > 6) { + if (row.cells.length > 5) { row.deleteCell(row.cells.length - 1); } return; } - var actionCell = row.cells.length > 6 ? row.cells[6] : null; + var actionCell = row.cells.length > 5 ? row.cells[5] : null; if (!actionCell) { actionCell = row.insertCell(-1); actionCell.setAttribute('data-label', 'Actions'); @@ -557,8 +548,6 @@ function renderClientRow(client, hasActionsColumn) { var connectedAt = client.connectedAt ? '