sse iphone fix 2

This commit is contained in:
2025-11-09 20:12:08 +01:00
parent 01b60671db
commit 0c73e36e82

View File

@@ -737,12 +737,27 @@
// API_BASE injected from backend (supports Docker/K8s environments) // API_BASE injected from backend (supports Docker/K8s environments)
window.API_BASE = '{{ api_base }}'; window.API_BASE = '{{ api_base }}';
window.RUNTIME_CONFIG = window.RUNTIME_CONFIG || {};
// Helper function to construct API URLs // Helper function to construct API URLs
function api(url) { function api(url) {
return `${window.API_BASE}${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 eventSource = null;
let currentState = {}; let currentState = {};
let thermostatTargets = {}; let thermostatTargets = {};
@@ -999,35 +1014,12 @@
} }
} }
// Connect to SSE // Safari/iOS-kompatibler SSE Client mit Auto-Reconnect
let reconnectAttempts = 0; let reconnectDelay = 2500;
const maxReconnectDelay = 30000; // Max 30 seconds let reconnectTimer = null;
function connectSSE() { // Global handleSSE function für SSE-Nachrichten
// Close existing connection if any window.handleSSE = function(data) {
if (eventSource) {
try {
eventSource.close();
} catch (e) {
console.error('Error closing EventSource:', e);
}
eventSource = null;
}
console.log(`Connecting to SSE... (attempt ${reconnectAttempts + 1})`);
try {
eventSource = new EventSource(api('/realtime'));
eventSource.onopen = () => {
console.log('SSE connected successfully');
reconnectAttempts = 0; // Reset counter on successful connection
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); console.log('SSE message:', data);
addEvent(data); addEvent(data);
@@ -1066,80 +1058,104 @@
); );
} }
} }
};
function cleanupSSE() {
if (eventSource) {
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();
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 {
// Verwende Polyfill wenn verfügbar, sonst native EventSource
const EventSourceImpl = window.EventSourcePolyfill || window.EventSource;
eventSource = new EventSourceImpl(REALTIME_URL, {
withCredentials: false
}); });
eventSource.addEventListener('ping', (e) => { eventSource.onopen = function() {
console.log('Heartbeat received'); console.log('SSE connected successfully');
}); reconnectDelay = 2500; // Reset backoff
document.getElementById('connection-status').textContent = 'Verbunden';
document.getElementById('connection-status').className = 'status connected';
};
eventSource.onerror = (error) => { eventSource.onmessage = function(evt) {
if (!evt || !evt.data) return;
// 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.onerror = function(error) {
console.error('SSE error:', error, 'readyState:', eventSource?.readyState); console.error('SSE error:', error, 'readyState:', eventSource?.readyState);
document.getElementById('connection-status').textContent = 'Getrennt'; document.getElementById('connection-status').textContent = 'Getrennt';
document.getElementById('connection-status').className = 'status disconnected'; document.getElementById('connection-status').className = 'status disconnected';
if (eventSource) { // Safari/iOS verliert Netz beim App-Switch: ruhig reconnecten
try { scheduleReconnect();
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);
}; };
} catch (error) { } catch (error) {
console.error('Failed to create EventSource:', error); console.error('Failed to create EventSource:', error);
document.getElementById('connection-status').textContent = 'Getrennt'; document.getElementById('connection-status').textContent = 'Getrennt';
document.getElementById('connection-status').className = 'status disconnected'; document.getElementById('connection-status').className = 'status disconnected';
scheduleReconnect();
reconnectAttempts++;
const delay = Math.min(
1000 * Math.pow(2, reconnectAttempts - 1),
maxReconnectDelay
);
setTimeout(connectSSE, delay);
} }
} }
// Safari/iOS specific: Reconnect when page becomes visible // Visibility-Change Handler für iOS App-Switch
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') { if (!document.hidden) {
console.log('Page visible, checking SSE connection...'); // Wenn wieder sichtbar & keine offene Verbindung: verbinden
// Only reconnect if connection is actually dead (CLOSED = 2) if (!eventSource || eventSource.readyState !== 1) {
if (!eventSource || eventSource.readyState === EventSource.CLOSED) { console.log('Page visible again, reconnecting SSE...');
console.log('SSE connection dead, forcing reconnect...');
reconnectAttempts = 0; // Reset for immediate reconnect
connectSSE(); connectSSE();
} else {
console.log('SSE connection OK, readyState:', eventSource.readyState);
} }
} }
}); });
// Safari/iOS specific: Reconnect on page focus // Start SSE connection
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
connectSSE(); connectSSE();
// Load initial device states // Load initial device states