""" Classroom API - WebSocket Routes Real-time WebSocket endpoints for timer updates. """ import json from typing import Dict, Any from datetime import datetime import logging from fastapi import APIRouter, WebSocket, WebSocketDisconnect from ..services.persistence import ( sessions, init_db_if_needed, DB_ENABLED, SessionLocal, ) from ..websocket_manager import ( ws_manager, start_timer_broadcast, build_timer_status, is_timer_broadcast_running, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["WebSocket"]) @router.websocket("/ws/{session_id}") async def websocket_timer(websocket: WebSocket, session_id: str): """ WebSocket-Endpoint fuer Echtzeit-Timer-Updates. Features: - Sub-Sekunden Timer-Updates (jede Sekunde) - Phasenwechsel-Benachrichtigungen - Session-Ende-Benachrichtigungen - Multi-Device Support Protocol: - Server sendet JSON-Messages mit "type" und "data" - Types: "timer_update", "phase_change", "session_ended", "error", "connected" - Client kann "ping" senden fuer Keepalive """ # Session validieren bevor Connect session = sessions.get(session_id) if not session: # Versuche aus DB zu laden if DB_ENABLED: try: init_db_if_needed() from classroom_engine.repository import SessionRepository db = SessionLocal() repo = SessionRepository(db) db_session = repo.get_by_id(session_id) if db_session: session = repo.to_dataclass(db_session) sessions[session_id] = session db.close() except Exception as e: logger.error(f"WebSocket: Failed to load session {session_id}: {e}") if not session: await websocket.close(code=4004, reason="Session not found") return if session.is_ended: await websocket.close(code=4001, reason="Session already ended") return # Verbindung akzeptieren und registrieren await ws_manager.connect(websocket, session_id) # Timer-Broadcast-Task starten wenn noetig start_timer_broadcast(sessions) # Initiale Daten senden try: initial_timer = build_timer_status(session) await websocket.send_json({ "type": "connected", "data": { "session_id": session_id, "client_count": ws_manager.get_client_count(session_id), "timer": initial_timer } }) except Exception as e: logger.error(f"WebSocket: Failed to send initial data: {e}") # Message-Loop try: while True: try: message = await websocket.receive_text() data = json.loads(message) if data.get("type") == "ping": await websocket.send_json({"type": "pong"}) elif data.get("type") == "get_timer": # Client kann manuell Timer-Status anfordern session = sessions.get(session_id) if session and not session.is_ended: timer_data = build_timer_status(session) await websocket.send_json({ "type": "timer_update", "data": timer_data }) except json.JSONDecodeError: await websocket.send_json({ "type": "error", "data": {"message": "Invalid JSON"} }) except WebSocketDisconnect: logger.info(f"WebSocket: Client disconnected from session {session_id}") except Exception as e: logger.error(f"WebSocket error: {e}") finally: await ws_manager.disconnect(websocket) @router.get("/ws/status") async def websocket_status() -> Dict[str, Any]: """ Status-Endpoint fuer WebSocket-Verbindungen. Zeigt aktive Sessions und Verbindungszahlen. """ active_sessions = ws_manager.get_active_sessions() return { "active_sessions": len(active_sessions), "sessions": [ { "session_id": sid, "client_count": ws_manager.get_client_count(sid) } for sid in active_sessions ], "broadcast_task_running": is_timer_broadcast_running() }