diff --git a/apps/api/main.py b/apps/api/main.py index 82ccdfc..6acbf58 100644 --- a/apps/api/main.py +++ b/apps/api/main.py @@ -301,41 +301,42 @@ async def event_generator(request: Request) -> AsyncGenerator[str, None]: try: await pubsub.subscribe(redis_channel) + logger.info(f"SSE client connected, subscribed to {redis_channel}") - # Create heartbeat task + # Create heartbeat tracking last_heartbeat = asyncio.get_event_loop().time() + heartbeat_interval = 25 while True: # Check if client disconnected if await request.is_disconnected(): + logger.info("SSE client disconnected") break - # Get message with timeout for heartbeat - try: - message = await asyncio.wait_for( - pubsub.get_message(ignore_subscribe_messages=True), - timeout=1.0 - ) - - if message and message["type"] == "message": - # Send data event - data = message["data"] - yield f"event: message\ndata: {data}\n\n" - last_heartbeat = asyncio.get_event_loop().time() - - except asyncio.TimeoutError: - pass + # Try to get message (non-blocking) + message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=0.1) + + # Handle actual data messages + if message and message["type"] == "message": + data = message["data"] + logger.debug(f"Sending SSE message: {data[:100]}...") + yield f"event: message\ndata: {data}\n\n" + last_heartbeat = asyncio.get_event_loop().time() + else: + # No message, sleep a bit to avoid busy loop + await asyncio.sleep(0.1) # Send heartbeat every 25 seconds current_time = asyncio.get_event_loop().time() - if current_time - last_heartbeat >= 25: + if current_time - last_heartbeat >= heartbeat_interval: yield "event: ping\ndata: heartbeat\n\n" last_heartbeat = current_time finally: await pubsub.unsubscribe(redis_channel) - await pubsub.close() - await redis_client.close() + await pubsub.aclose() + await redis_client.aclose() + logger.info("SSE connection closed") @app.get("/realtime") diff --git a/apps/ui/templates/dashboard.html b/apps/ui/templates/dashboard.html index 49d6caf..70e1c13 100644 --- a/apps/ui/templates/dashboard.html +++ b/apps/ui/templates/dashboard.html @@ -855,6 +855,12 @@ // 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; @@ -930,11 +936,13 @@ 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...'); + // 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); } } }); @@ -942,26 +950,16 @@ // 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); } }); - // 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();