fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
143
backend/classroom/routes/websocket_routes.py
Normal file
143
backend/classroom/routes/websocket_routes.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user