Files
breakpilot-lehrer/backend-lehrer/classroom/routes/analytics.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

370 lines
10 KiB
Python

"""
Classroom API - Analytics Routes
Analytics and reflection endpoints (Phase 5).
"""
import uuid
from typing import Dict, Any
from datetime import datetime, timedelta
import logging
from fastapi import APIRouter, HTTPException, Query
from classroom_engine import LessonReflection
from ..models import (
SessionSummaryResponse,
TeacherAnalyticsResponse,
ReflectionCreate,
ReflectionUpdate,
ReflectionResponse,
)
from ..services.persistence import (
init_db_if_needed,
DB_ENABLED,
SessionLocal,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Analytics"])
# === Analytics Endpoints ===
@router.get("/analytics/session/{session_id}")
async def get_session_summary(session_id: str) -> SessionSummaryResponse:
"""
Gibt die Analytics-Zusammenfassung einer Session zurueck.
Berechnet Phasen-Dauer Statistiken, Overtime und Pausen-Analyse.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import AnalyticsRepository
repo = AnalyticsRepository(db)
summary = repo.get_session_summary(session_id)
if not summary:
raise HTTPException(
status_code=404,
detail=f"Session {session_id} not found"
)
return SessionSummaryResponse(**summary.to_dict())
@router.get("/analytics/teacher/{teacher_id}")
async def get_teacher_analytics(
teacher_id: str,
days: int = Query(30, ge=1, le=365)
) -> TeacherAnalyticsResponse:
"""
Gibt aggregierte Analytics fuer einen Lehrer zurueck.
Berechnet Trends ueber den angegebenen Zeitraum.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
period_end = datetime.utcnow()
period_start = period_end - timedelta(days=days)
with SessionLocal() as db:
from classroom_engine.repository import AnalyticsRepository
repo = AnalyticsRepository(db)
analytics = repo.get_teacher_analytics(
teacher_id, period_start, period_end
)
return TeacherAnalyticsResponse(**analytics.to_dict())
@router.get("/analytics/phase-trends/{teacher_id}/{phase}")
async def get_phase_trends(
teacher_id: str,
phase: str,
limit: int = Query(20, ge=1, le=100)
) -> Dict[str, Any]:
"""
Gibt die Dauer-Trends fuer eine Phase zurueck.
Nuetzlich fuer Charts und Visualisierungen.
"""
if phase not in ["einstieg", "erarbeitung", "sicherung", "transfer", "reflexion"]:
raise HTTPException(
status_code=400,
detail=f"Invalid phase: {phase}"
)
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import AnalyticsRepository
repo = AnalyticsRepository(db)
trends = repo.get_phase_duration_trends(teacher_id, phase, limit)
return {
"teacher_id": teacher_id,
"phase": phase,
"data_points": trends,
"count": len(trends)
}
@router.get("/analytics/overtime/{teacher_id}")
async def get_overtime_analysis(
teacher_id: str,
limit: int = Query(30, ge=1, le=100)
) -> Dict[str, Any]:
"""
Analysiert Overtime-Muster nach Phase.
Zeigt welche Phasen am haeufigsten ueberzogen werden.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import AnalyticsRepository
repo = AnalyticsRepository(db)
analysis = repo.get_overtime_analysis(teacher_id, limit)
return {
"teacher_id": teacher_id,
"sessions_analyzed": limit,
"phases": analysis
}
# === Reflection Endpoints ===
@router.post("/reflections", status_code=201)
async def create_reflection(data: ReflectionCreate) -> ReflectionResponse:
"""
Erstellt eine Post-Lesson Reflection.
Erlaubt Lehrern, nach der Stunde Notizen zu speichern.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import ReflectionRepository
repo = ReflectionRepository(db)
# Pruefen ob schon eine Reflection existiert
existing = repo.get_by_session(data.session_id)
if existing:
raise HTTPException(
status_code=409,
detail=f"Reflection for session {data.session_id} already exists"
)
reflection = LessonReflection(
reflection_id=str(uuid.uuid4()),
session_id=data.session_id,
teacher_id=data.teacher_id,
notes=data.notes,
overall_rating=data.overall_rating,
what_worked=data.what_worked,
improvements=data.improvements,
notes_for_next_lesson=data.notes_for_next_lesson,
created_at=datetime.utcnow(),
)
db_reflection = repo.create(reflection)
result = repo.to_dataclass(db_reflection)
return ReflectionResponse(**result.to_dict())
@router.get("/reflections/session/{session_id}")
async def get_reflection_by_session(session_id: str) -> ReflectionResponse:
"""
Holt die Reflection einer Session.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import ReflectionRepository
repo = ReflectionRepository(db)
db_reflection = repo.get_by_session(session_id)
if not db_reflection:
raise HTTPException(
status_code=404,
detail=f"No reflection for session {session_id}"
)
result = repo.to_dataclass(db_reflection)
return ReflectionResponse(**result.to_dict())
@router.get("/reflections/teacher/{teacher_id}")
async def get_reflections_by_teacher(
teacher_id: str,
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0)
) -> Dict[str, Any]:
"""
Holt alle Reflections eines Lehrers.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import ReflectionRepository
repo = ReflectionRepository(db)
db_reflections = repo.get_by_teacher(teacher_id, limit, offset)
reflections = [
repo.to_dataclass(r).to_dict()
for r in db_reflections
]
return {
"teacher_id": teacher_id,
"reflections": reflections,
"count": len(reflections),
"offset": offset,
"limit": limit
}
@router.put("/reflections/{reflection_id}")
async def update_reflection(
reflection_id: str,
data: ReflectionUpdate,
teacher_id: str = Query(...)
) -> ReflectionResponse:
"""
Aktualisiert eine Reflection.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import ReflectionRepository
repo = ReflectionRepository(db)
db_reflection = repo.get_by_id(reflection_id)
if not db_reflection:
raise HTTPException(
status_code=404,
detail=f"Reflection {reflection_id} not found"
)
if db_reflection.teacher_id != teacher_id:
raise HTTPException(
status_code=403,
detail="Not authorized to update this reflection"
)
# Vorhandene Werte beibehalten wenn nicht im Update
reflection = repo.to_dataclass(db_reflection)
if data.notes is not None:
reflection.notes = data.notes
if data.overall_rating is not None:
reflection.overall_rating = data.overall_rating
if data.what_worked is not None:
reflection.what_worked = data.what_worked
if data.improvements is not None:
reflection.improvements = data.improvements
if data.notes_for_next_lesson is not None:
reflection.notes_for_next_lesson = data.notes_for_next_lesson
reflection.updated_at = datetime.utcnow()
db_updated = repo.update(reflection)
result = repo.to_dataclass(db_updated)
return ReflectionResponse(**result.to_dict())
@router.delete("/reflections/{reflection_id}")
async def delete_reflection(
reflection_id: str,
teacher_id: str = Query(...)
) -> Dict[str, Any]:
"""
Loescht eine Reflection.
"""
if not DB_ENABLED:
raise HTTPException(
status_code=503,
detail="Database not available"
)
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import ReflectionRepository
repo = ReflectionRepository(db)
db_reflection = repo.get_by_id(reflection_id)
if not db_reflection:
raise HTTPException(
status_code=404,
detail=f"Reflection {reflection_id} not found"
)
if db_reflection.teacher_id != teacher_id:
raise HTTPException(
status_code=403,
detail="Not authorized to delete this reflection"
)
success = repo.delete(reflection_id)
return {
"success": success,
"deleted_id": reflection_id
}