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>
144 lines
4.3 KiB
Python
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()
|
|
}
|