diff --git a/ui/services/sensing.service.js b/ui/services/sensing.service.js index 0992483b..2d5635e4 100644 --- a/ui/services/sensing.service.js +++ b/ui/services/sensing.service.js @@ -9,11 +9,25 @@ * emit simulated frames so the UI can clearly distinguish live vs. fallback data. */ -// Derive WebSocket URL from the page origin so it works on any port. -// The /ws/sensing endpoint is available on the same HTTP port (3000). -const _wsProto = (typeof window !== 'undefined' && window.location.protocol === 'https:') ? 'wss:' : 'ws:'; -const _wsHost = (typeof window !== 'undefined' && window.location.host) ? window.location.host : 'localhost:3000'; -const SENSING_WS_URL = `${_wsProto}//${_wsHost}/ws/sensing`; +const SENSING_WS_PORT_BY_HTTP_PORT = { + // Docker image: HTTP UI/API on 3000, sensing stream on 3001. + '3000': '3001', + // Python sensing stack: UI on 8080, sensing stream on 8765. + '8080': '8765', +}; + +export function buildSensingWsUrl(locationLike = (typeof window !== 'undefined' ? window.location : null)) { + const protocol = locationLike && locationLike.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = locationLike && locationLike.host ? locationLike.host : 'localhost:3001'; + const hostname = locationLike && locationLike.hostname ? locationLike.hostname : host.split(':')[0]; + const port = locationLike && locationLike.port ? locationLike.port : ''; + const wsPort = SENSING_WS_PORT_BY_HTTP_PORT[port]; + const wsHost = wsPort ? `${hostname}:${wsPort}` : host; + + return `${protocol}//${wsHost}/ws/sensing`; +} + +const SENSING_WS_URL = buildSensingWsUrl(); const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]; const MAX_RECONNECT_ATTEMPTS = 20; // Number of failed attempts that must occur before simulation starts. diff --git a/ui/services/websocket.service.js b/ui/services/websocket.service.js index 26f4a231..c27cea4f 100644 --- a/ui/services/websocket.service.js +++ b/ui/services/websocket.service.js @@ -136,9 +136,22 @@ export class WebSocketService { // Set up WebSocket event handlers setupEventHandlers(url, ws, handlers) { - const connection = this.connections.get(url); + const getConnection = (eventName) => { + const connection = this.connections.get(url); + if (!connection) { + this.logger.warn(`Ignoring WebSocket ${eventName} for unregistered connection`, { + url, + readyState: ws.readyState + }); + return null; + } + return connection; + }; ws.onopen = (event) => { + const connection = getConnection('open'); + if (!connection) return; + const connectionTime = Date.now() - connection.connectionStartTime; this.logger.info(`WebSocket connected successfully`, { url, connectionTime }); @@ -158,6 +171,9 @@ export class WebSocketService { }; ws.onmessage = (event) => { + const connection = getConnection('message'); + if (!connection) return; + connection.lastActivity = Date.now(); connection.messageCount++; @@ -188,6 +204,9 @@ export class WebSocketService { }; ws.onerror = (event) => { + const connection = getConnection('error'); + if (!connection) return; + connection.errorCount++; this.logger.error(`WebSocket error occurred`, { url, @@ -208,6 +227,9 @@ export class WebSocketService { }; ws.onclose = (event) => { + const connection = getConnection('close'); + if (!connection) return; + const { code, reason, wasClean } = event; this.logger.info(`WebSocket closed`, { url, code, reason, wasClean }); @@ -607,4 +629,4 @@ export class WebSocketService { } // Create singleton instance -export const wsService = new WebSocketService(); \ No newline at end of file +export const wsService = new WebSocketService(); diff --git a/ui/tests/test-runner.js b/ui/tests/test-runner.js index 7d35977a..5e22111a 100644 --- a/ui/tests/test-runner.js +++ b/ui/tests/test-runner.js @@ -3,6 +3,7 @@ import { API_CONFIG, buildApiUrl, buildWsUrl } from '../config/api.config.js'; import { apiService } from '../services/api.service.js'; import { wsService } from '../services/websocket.service.js'; +import { buildSensingWsUrl } from '../services/sensing.service.js'; import { poseService } from '../services/pose.service.js'; import { healthService } from '../services/health.service.js'; import { TabManager } from '../components/TabManager.js'; @@ -232,6 +233,17 @@ testRunner.test('buildWsUrl constructs WebSocket URLs', 'apiConfig', () => { testRunner.assert(url.includes('token=test-token'), 'URL should contain token parameter'); }); +testRunner.test('buildSensingWsUrl maps Docker UI port to sensing WebSocket port', 'apiConfig', () => { + const url = buildSensingWsUrl({ + protocol: 'http:', + host: '192.168.28.147:3000', + hostname: '192.168.28.147', + port: '3000', + }); + + testRunner.assertEqual(url, 'ws://192.168.28.147:3001/ws/sensing'); +}); + // API Service Tests testRunner.test('apiService has required methods', 'apiService', () => { testRunner.assert(typeof apiService.get === 'function', 'get method should exist'); @@ -473,4 +485,4 @@ document.addEventListener('DOMContentLoaded', () => { testRunner.updateSummary(); }); -export { testRunner }; \ No newline at end of file +export { testRunner };