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:
185
backend/api/classroom/utility.py
Normal file
185
backend/api/classroom/utility.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user