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>
370 lines
10 KiB
Python
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
|
|
}
|