Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
186 lines
5.7 KiB
Python
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)
|