Files
breakpilot-lehrer/backend-lehrer/classroom/routes/websocket_routes.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

144 lines
4.3 KiB
Python

"""
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()
}