// ============================================================ // WebSocket: Connection, reconnection, authentication // ============================================================ let reconnectTimeout = null; let pingInterval = null; let wsReconnectAttempts = 0; function showAuthForm(errorMessage = '') { const overlay = document.getElementById('auth-overlay'); overlay.classList.remove('hidden'); const errorEl = document.getElementById('auth-error'); if (errorMessage) { errorEl.textContent = errorMessage; errorEl.classList.add('visible'); } else { errorEl.classList.remove('visible'); } } function hideAuthForm() { document.getElementById('auth-overlay').classList.add('hidden'); } function authenticate() { const token = document.getElementById('token-input').value.trim(); if (!token) { showAuthForm(t('auth.required')); return; } localStorage.setItem('media_server_token', token); connectWebSocket(token); } function clearToken() { localStorage.removeItem('media_server_token'); if (ws) { ws.close(); } showAuthForm(t('auth.cleared')); } function connectWebSocket(token) { if (pingInterval) { clearInterval(pingInterval); pingInterval = null; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/media/ws?token=${encodeURIComponent(token)}`; ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('WebSocket connected'); wsReconnectAttempts = 0; updateConnectionStatus(true); hideConnectionBanner(); hideAuthForm(); loadScripts(); loadScriptsTable(); loadCallbacksTable(); loadLinksTable(); loadHeaderLinks(); loadAudioDevices(); if (visualizerEnabled && visualizerAvailable) { ws.send(JSON.stringify({ type: 'enable_visualizer' })); } }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'status' || msg.type === 'status_update') { updateUI(msg.data); } else if (msg.type === 'scripts_changed') { console.log('Scripts changed, reloading...'); loadScripts(); loadScriptsTable(); } else if (msg.type === 'links_changed') { console.log('Links changed, reloading...'); loadHeaderLinks(); loadLinksTable(); displayQuickAccess(); } else if (msg.type === 'audio_data') { frequencyData = msg.data; } else if (msg.type === 'error') { console.error('WebSocket error:', msg.message); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); updateConnectionStatus(false); }; ws.onclose = (event) => { console.log('WebSocket closed:', event.code); updateConnectionStatus(false); stopPositionInterpolation(); if (event.code === 4001) { localStorage.removeItem('media_server_token'); showAuthForm(t('auth.invalid')); } else if (event.code !== 1000) { wsReconnectAttempts++; if (wsReconnectAttempts <= WS_MAX_RECONNECT_ATTEMPTS) { const delay = Math.min( WS_BACKOFF_BASE_MS * Math.pow(1.5, wsReconnectAttempts - 1), WS_BACKOFF_MAX_MS ); console.log(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${wsReconnectAttempts}/${WS_MAX_RECONNECT_ATTEMPTS})...`); if (wsReconnectAttempts >= 3) { showConnectionBanner(t('connection.reconnecting').replace('{attempt}', wsReconnectAttempts), false); } reconnectTimeout = setTimeout(() => { const savedToken = localStorage.getItem('media_server_token'); if (savedToken) { connectWebSocket(savedToken); } }, delay); } else { showConnectionBanner(t('connection.lost'), true); } } }; pingInterval = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, WS_PING_INTERVAL_MS); } function updateConnectionStatus(connected) { if (connected) { dom.statusDot.classList.add('connected'); } else { dom.statusDot.classList.remove('connected'); } } function showConnectionBanner(message, showButton) { const banner = document.getElementById('connectionBanner'); const text = document.getElementById('connectionBannerText'); const btn = document.getElementById('connectionBannerBtn'); text.textContent = message; btn.style.display = showButton ? '' : 'none'; banner.classList.remove('hidden'); } function hideConnectionBanner() { const banner = document.getElementById('connectionBanner'); banner.classList.add('hidden'); } function manualReconnect() { const savedToken = localStorage.getItem('media_server_token'); if (savedToken) { wsReconnectAttempts = 0; hideConnectionBanner(); connectWebSocket(savedToken); } }