From 0c73e36e82702b453aeb476518fd7d1c9e3c7660 Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Sun, 9 Nov 2025 20:12:08 +0100 Subject: [PATCH] sse iphone fix 2 --- apps/ui/templates/dashboard.html | 228 +++++++++++++++++-------------- 1 file changed, 122 insertions(+), 106 deletions(-) diff --git a/apps/ui/templates/dashboard.html b/apps/ui/templates/dashboard.html index 6948355..5b33feb 100644 --- a/apps/ui/templates/dashboard.html +++ b/apps/ui/templates/dashboard.html @@ -737,12 +737,27 @@ // API_BASE injected from backend (supports Docker/K8s environments) window.API_BASE = '{{ api_base }}'; + window.RUNTIME_CONFIG = window.RUNTIME_CONFIG || {}; // Helper function to construct API URLs function api(url) { return `${window.API_BASE}${url}`; } + // iOS/Safari Polyfill laden (nur wenn nötig) + (function() { + var isIOS = /iP(hone|od|ad)/.test(navigator.platform) || + (navigator.userAgent.includes("Mac") && "ontouchend" in document); + if (isIOS && typeof window.EventSourcePolyfill === "undefined") { + var s = document.createElement("script"); + s.src = "https://cdn.jsdelivr.net/npm/event-source-polyfill@1.0.31/src/eventsource.min.js"; + s.onerror = function() { + console.warn("EventSource polyfill konnte nicht geladen werden"); + }; + document.head.appendChild(s); + } + })(); + let eventSource = null; let currentState = {}; let thermostatTargets = {}; @@ -999,147 +1014,148 @@ } } - // Connect to SSE - let reconnectAttempts = 0; - const maxReconnectDelay = 30000; // Max 30 seconds + // Safari/iOS-kompatibler SSE Client mit Auto-Reconnect + let reconnectDelay = 2500; + let reconnectTimer = null; - function connectSSE() { - // Close existing connection if any + // Global handleSSE function für SSE-Nachrichten + window.handleSSE = function(data) { + console.log('SSE message:', data); + + addEvent(data); + + // Update device state + if (data.type === 'state' && data.device_id && data.payload) { + const card = document.querySelector(`[data-device-id="${data.device_id}"]`); + if (!card) { + console.warn(`No card found for device ${data.device_id}`); + return; + } + + // Check if it's a light + if (data.payload.power !== undefined) { + currentState[data.device_id] = data.payload.power; + updateDeviceUI( + data.device_id, + data.payload.power, + data.payload.brightness + ); + } + + // Check if it's a thermostat + if (data.payload.mode !== undefined || data.payload.target !== undefined || data.payload.current !== undefined) { + if (data.payload.mode !== undefined) { + thermostatModes[data.device_id] = data.payload.mode; + } + if (data.payload.target !== undefined) { + thermostatTargets[data.device_id] = data.payload.target; + } + updateThermostatUI( + data.device_id, + data.payload.current, + data.payload.target, + data.payload.mode + ); + } + } + }; + + function cleanupSSE() { if (eventSource) { - try { - eventSource.close(); - } catch (e) { + try { + eventSource.close(); + } catch(e) { console.error('Error closing EventSource:', e); } eventSource = null; } + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + } + + function scheduleReconnect() { + if (reconnectTimer) return; + console.log(`Reconnecting in ${reconnectDelay}ms...`); + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connectSSE(); + // Backoff bis 10s + reconnectDelay = Math.min(reconnectDelay * 2, 10000); + }, reconnectDelay); + } + + function connectSSE() { + cleanupSSE(); - console.log(`Connecting to SSE... (attempt ${reconnectAttempts + 1})`); + const REALTIME_URL = (window.RUNTIME_CONFIG && window.RUNTIME_CONFIG.REALTIME_URL) + ? window.RUNTIME_CONFIG.REALTIME_URL + : api('/realtime'); + + console.log('Connecting to SSE:', REALTIME_URL); try { - eventSource = new EventSource(api('/realtime')); + // Verwende Polyfill wenn verfügbar, sonst native EventSource + const EventSourceImpl = window.EventSourcePolyfill || window.EventSource; + eventSource = new EventSourceImpl(REALTIME_URL, { + withCredentials: false + }); - eventSource.onopen = () => { + eventSource.onopen = function() { console.log('SSE connected successfully'); - reconnectAttempts = 0; // Reset counter on successful connection + reconnectDelay = 2500; // Reset backoff document.getElementById('connection-status').textContent = 'Verbunden'; document.getElementById('connection-status').className = 'status connected'; }; - eventSource.addEventListener('message', (e) => { - const data = JSON.parse(e.data); - console.log('SSE message:', data); + eventSource.onmessage = function(evt) { + if (!evt || !evt.data) return; - addEvent(data); - - // Update device state - if (data.type === 'state' && data.device_id && data.payload) { - const card = document.querySelector(`[data-device-id="${data.device_id}"]`); - if (!card) { - console.warn(`No card found for device ${data.device_id}`); - return; - } - - // Check if it's a light - if (data.payload.power !== undefined) { - currentState[data.device_id] = data.payload.power; - updateDeviceUI( - data.device_id, - data.payload.power, - data.payload.brightness - ); - } - - // Check if it's a thermostat - if (data.payload.mode !== undefined || data.payload.target !== undefined || data.payload.current !== undefined) { - if (data.payload.mode !== undefined) { - thermostatModes[data.device_id] = data.payload.mode; - } - if (data.payload.target !== undefined) { - thermostatTargets[data.device_id] = data.payload.target; - } - updateThermostatUI( - data.device_id, - data.payload.current, - data.payload.target, - data.payload.mode - ); - } + // Heartbeats beginnen mit ":" -> ignorieren + if (typeof evt.data === "string" && evt.data.charAt(0) === ":") { + return; } - }); + + try { + const data = JSON.parse(evt.data); + if (window.handleSSE) { + window.handleSSE(data); + } + } catch (e) { + console.error('Error parsing SSE message:', e); + } + }; - eventSource.addEventListener('ping', (e) => { - console.log('Heartbeat received'); - }); - - eventSource.onerror = (error) => { + eventSource.onerror = function(error) { console.error('SSE error:', error, 'readyState:', eventSource?.readyState); document.getElementById('connection-status').textContent = 'Getrennt'; document.getElementById('connection-status').className = 'status disconnected'; - if (eventSource) { - try { - eventSource.close(); - } catch (e) { - console.error('Error closing EventSource on error:', e); - } - eventSource = null; - } - - // Exponential backoff with max delay - reconnectAttempts++; - const delay = Math.min( - 1000 * Math.pow(2, reconnectAttempts - 1), - maxReconnectDelay - ); - - console.log(`Reconnecting in ${delay}ms... (attempt ${reconnectAttempts})`); - setTimeout(connectSSE, delay); + // Safari/iOS verliert Netz beim App-Switch: ruhig reconnecten + scheduleReconnect(); }; + } catch (error) { console.error('Failed to create EventSource:', error); document.getElementById('connection-status').textContent = 'Getrennt'; document.getElementById('connection-status').className = 'status disconnected'; - - reconnectAttempts++; - const delay = Math.min( - 1000 * Math.pow(2, reconnectAttempts - 1), - maxReconnectDelay - ); - - setTimeout(connectSSE, delay); + scheduleReconnect(); } } - // Safari/iOS specific: Reconnect when page becomes visible - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - console.log('Page visible, checking SSE connection...'); - // Only reconnect if connection is actually dead (CLOSED = 2) - if (!eventSource || eventSource.readyState === EventSource.CLOSED) { - console.log('SSE connection dead, forcing reconnect...'); - reconnectAttempts = 0; // Reset for immediate reconnect + // Visibility-Change Handler für iOS App-Switch + document.addEventListener('visibilitychange', function() { + if (!document.hidden) { + // Wenn wieder sichtbar & keine offene Verbindung: verbinden + if (!eventSource || eventSource.readyState !== 1) { + console.log('Page visible again, reconnecting SSE...'); connectSSE(); - } else { - console.log('SSE connection OK, readyState:', eventSource.readyState); } } }); - // Safari/iOS specific: Reconnect on page focus - window.addEventListener('focus', () => { - console.log('Window focused, checking SSE connection...'); - // Only reconnect if connection is actually dead (CLOSED = 2) - if (!eventSource || eventSource.readyState === EventSource.CLOSED) { - console.log('SSE connection dead, forcing reconnect...'); - reconnectAttempts = 0; // Reset for immediate reconnect - connectSSE(); - } else { - console.log('SSE connection OK, readyState:', eventSource.readyState); - } - }); - - // Initialize + // Start SSE connection connectSSE(); // Load initial device states