// Connection Status Widget - Persistent indicator in header // Shows WebSocket and API connection state with reconnect button import { sensingService } from '../services/sensing.service.js'; export class ConnectionStatus { constructor() { this.widget = null; this._unsub = null; } init() { this.createWidget(); this.subscribe(); } createWidget() { this.widget = document.createElement('div'); this.widget.className = 'conn-status'; this.widget.setAttribute('role', 'status'); this.widget.setAttribute('aria-live', 'polite'); this.widget.innerHTML = ` Connecting `; this.widget.querySelector('.conn-status-reconnect').addEventListener('click', () => { this.setStatus('reconnecting', 'Reconnecting...'); sensingService.reconnect?.(); }); // Insert into header-info, after theme toggle if present const headerInfo = document.querySelector('.header-info'); if (headerInfo) { headerInfo.prepend(this.widget); } } subscribe() { this._unsub = sensingService.onStateChange(() => { this.update(); }); // Initial this.update(); } update() { const state = sensingService.state; const source = sensingService.dataSource; if (state === 'connected' || state === 'streaming') { const label = source === 'live' ? 'Live' : source === 'server-simulated' ? 'Simulated' : 'Connected'; this.setStatus('connected', label); } else if (state === 'connecting' || state === 'reconnecting') { this.setStatus('reconnecting', 'Connecting...'); } else if (state === 'error') { this.setStatus('error', 'Error'); } else { this.setStatus('disconnected', 'Offline'); } } setStatus(status, label) { if (!this.widget) return; this.widget.className = `conn-status conn-status-${status}`; this.widget.querySelector('.conn-status-label').textContent = label; const reconnectBtn = this.widget.querySelector('.conn-status-reconnect'); reconnectBtn.style.display = (status === 'disconnected' || status === 'error') ? '' : 'none'; } dispose() { if (this._unsub) this._unsub(); if (this.widget?.parentNode) { this.widget.parentNode.removeChild(this.widget); } } }