This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

186 lines
5.7 KiB
Python

"""
Classroom API - Utility Endpoints.
Health-Check, Phasen-Liste und andere Utility-Endpoints.
"""
from typing import Dict, List, Optional, Any
from datetime import datetime
import logging
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import HTMLResponse
from sqlalchemy import text
from pydantic import BaseModel
from classroom_engine import LESSON_PHASES, LessonStateMachine
from .shared import (
init_db_if_needed,
get_sessions,
get_session_or_404,
ws_manager,
DB_ENABLED,
logger,
)
try:
from classroom_engine.database import SessionLocal
except ImportError:
pass
router = APIRouter(tags=["Utility"])
# === Pydantic Models ===
class PhasesListResponse(BaseModel):
"""Liste aller verfuegbaren Phasen."""
phases: List[Dict[str, Any]]
class ActiveSessionsResponse(BaseModel):
"""Liste aktiver Sessions."""
sessions: List[Dict[str, Any]]
count: int
# === Endpoints ===
@router.get("/phases", response_model=PhasesListResponse)
async def list_phases() -> PhasesListResponse:
"""Listet alle verfuegbaren Unterrichtsphasen mit Metadaten."""
phases = []
for phase_id, config in LESSON_PHASES.items():
phases.append({
"phase": phase_id,
"display_name": config["display_name"],
"default_duration_minutes": config["default_duration_minutes"],
"activities": config["activities"],
"icon": config["icon"],
"description": config.get("description", ""),
})
return PhasesListResponse(phases=phases)
@router.get("/sessions", response_model=ActiveSessionsResponse)
async def list_active_sessions(
teacher_id: Optional[str] = Query(None)
) -> ActiveSessionsResponse:
"""Listet alle (optionally gefilterten) Sessions."""
sessions = get_sessions()
sessions_list = []
for session in sessions.values():
if teacher_id and session.teacher_id != teacher_id:
continue
fsm = LessonStateMachine()
sessions_list.append({
"session_id": session.session_id,
"teacher_id": session.teacher_id,
"class_id": session.class_id,
"subject": session.subject,
"current_phase": session.current_phase.value,
"is_active": fsm.is_lesson_active(session),
"lesson_started_at": session.lesson_started_at.isoformat() if session.lesson_started_at else None,
})
return ActiveSessionsResponse(sessions=sessions_list, count=len(sessions_list))
@router.get("/health")
async def health_check() -> Dict[str, Any]:
"""Health-Check fuer den Classroom Service."""
db_status = "disabled"
if DB_ENABLED:
try:
db = SessionLocal()
db.execute(text("SELECT 1"))
db.close()
db_status = "connected"
except Exception as e:
db_status = f"error: {str(e)}"
sessions = get_sessions()
return {
"status": "healthy",
"service": "classroom-engine",
"active_sessions": len(sessions),
"db_enabled": DB_ENABLED,
"db_status": db_status,
"websocket_connections": sum(
ws_manager.get_client_count(sid) for sid in ws_manager.get_active_sessions()
),
"timestamp": datetime.utcnow().isoformat(),
}
@router.get("/ws/status")
async def websocket_status() -> Dict[str, Any]:
"""Status der WebSocket-Verbindungen."""
active_sessions = ws_manager.get_active_sessions()
session_counts = {
sid: ws_manager.get_client_count(sid) for sid in active_sessions
}
return {
"active_sessions": len(active_sessions),
"session_connections": session_counts,
"total_connections": sum(session_counts.values()),
"timestamp": datetime.utcnow().isoformat(),
}
@router.get("/export/session/{session_id}", response_class=HTMLResponse)
async def export_session_html(session_id: str) -> HTMLResponse:
"""Exportiert eine Session als HTML-Dokument."""
session = get_session_or_404(session_id)
# Einfacher HTML-Export
html = f"""
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Session Export - {session.subject}</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }}
h1 {{ color: #333; }}
.meta {{ color: #666; margin-bottom: 20px; }}
.section {{ margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 8px; }}
.phase {{ display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #ddd; }}
</style>
</head>
<body>
<h1>{session.subject}: {session.topic or 'Ohne Thema'}</h1>
<div class="meta">
<p>Klasse: {session.class_id}</p>
<p>Datum: {session.lesson_started_at.strftime('%d.%m.%Y %H:%M') if session.lesson_started_at else 'Nicht gestartet'}</p>
<p>Status: {session.current_phase.value}</p>
</div>
<div class="section">
<h2>Phasen</h2>
{"".join(f'<div class="phase"><span>{p.get("phase", "")}</span><span>{p.get("duration_seconds", 0) // 60} min</span></div>' for p in session.phase_history)}
</div>
<div class="section">
<h2>Notizen</h2>
<p>{session.notes or 'Keine Notizen'}</p>
</div>
<div class="section">
<h2>Hausaufgaben</h2>
<p>{session.homework or 'Keine Hausaufgaben'}</p>
</div>
<footer style="margin-top: 40px; color: #999; font-size: 12px;">
Exportiert am {datetime.utcnow().strftime('%d.%m.%Y %H:%M')} UTC - BreakPilot Classroom
</footer>
</body>
</html>
"""
return HTMLResponse(content=html)