diff --git a/apps/ui/templates/dashboard.html b/apps/ui/templates/dashboard.html
index dab3397..49d6caf 100644
--- a/apps/ui/templates/dashboard.html
+++ b/apps/ui/templates/dashboard.html
@@ -673,7 +673,6 @@
});
if (response.ok) {
- thermostatTargets[deviceId] = newTarget;
console.log(`Sent target ${newTarget} to ${deviceId}`);
addEvent({
action: 'target_adjusted',
@@ -822,61 +821,147 @@
}
// Connect to SSE
+ let reconnectAttempts = 0;
+ const maxReconnectDelay = 30000; // Max 30 seconds
+
function connectSSE() {
- eventSource = new EventSource(api('/realtime'));
-
- eventSource.onopen = () => {
- console.log('SSE connected');
- 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);
-
- addEvent(data);
-
- // Update device state
- if (data.type === 'state' && data.device_id && data.payload) {
- const card = document.querySelector(`[data-device-id="${data.device_id}"]`);
-
- // Check if it's a light
- if (data.payload.power !== undefined) {
- 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) {
- updateThermostatUI(
- data.device_id,
- data.payload.current,
- data.payload.target,
- data.payload.mode
- );
- }
+ // Close existing connection if any
+ if (eventSource) {
+ try {
+ eventSource.close();
+ } catch (e) {
+ console.error('Error closing EventSource:', e);
}
- });
+ eventSource = null;
+ }
- eventSource.addEventListener('ping', (e) => {
- console.log('Heartbeat received');
- });
+ console.log(`Connecting to SSE... (attempt ${reconnectAttempts + 1})`);
- eventSource.onerror = (error) => {
- console.error('SSE error:', error);
+ 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);
+
+ addEvent(data);
+
+ // Update device state
+ if (data.type === 'state' && data.device_id && data.payload) {
+ // 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
+ );
+ }
+ }
+ });
+
+ eventSource.addEventListener('ping', (e) => {
+ console.log('Heartbeat received');
+ });
+
+ eventSource.onerror = (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);
+ };
+ } catch (error) {
+ console.error('Failed to create EventSource:', error);
document.getElementById('connection-status').textContent = 'Getrennt';
document.getElementById('connection-status').className = 'status disconnected';
- eventSource.close();
- // Reconnect after 5 seconds
- setTimeout(connectSSE, 5000);
- };
+ reconnectAttempts++;
+ const delay = Math.min(
+ 1000 * Math.pow(2, reconnectAttempts - 1),
+ maxReconnectDelay
+ );
+
+ setTimeout(connectSSE, delay);
+ }
}
+ // Safari/iOS specific: Reconnect when page becomes visible
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ console.log('Page visible, checking SSE connection...');
+ // Force reconnect if connection is dead
+ if (!eventSource || eventSource.readyState === EventSource.CLOSED || eventSource.readyState === EventSource.CONNECTING) {
+ console.log('SSE connection dead or connecting, forcing reconnect...');
+ reconnectAttempts = 0; // Reset for immediate reconnect
+ connectSSE();
+ }
+ }
+ });
+
+ // Safari/iOS specific: Reconnect on page focus
+ window.addEventListener('focus', () => {
+ console.log('Window focused, checking SSE connection...');
+ if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
+ console.log('SSE connection dead, forcing reconnect...');
+ reconnectAttempts = 0; // Reset for immediate reconnect
+ connectSSE();
+ }
+ });
+
+ // Safari/iOS specific: Keep connection alive with periodic check
+ setInterval(() => {
+ if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
+ console.log('Periodic check: SSE connection dead, reconnecting...');
+ reconnectAttempts = 0;
+ connectSSE();
+ } else if (eventSource.readyState === EventSource.CONNECTING) {
+ console.log('Periodic check: SSE still connecting, readyState:', eventSource.readyState);
+ } else {
+ console.log('Periodic check: SSE connection alive, readyState:', eventSource.readyState);
+ }
+ }, 10000); // Check every 10 seconds
+
// Initialize
connectSSE();