""" Classroom API - Session Core Routes Session CRUD, lifecycle, and history endpoints. """ from uuid import uuid4 from typing import Dict, Optional, Any from datetime import datetime import logging from fastapi import APIRouter, HTTPException, Query from classroom_engine import ( LessonPhase, LessonSession, LessonStateMachine, PhaseTimer, LESSON_PHASES, ) from ..models import ( CreateSessionRequest, NotesRequest, PhaseInfo, TimerStatus, SessionResponse, PhasesListResponse, ActiveSessionsResponse, SessionHistoryItem, SessionHistoryResponse, ) from ..services.persistence import ( sessions, init_db_if_needed, persist_session, get_session_or_404, DB_ENABLED, SessionLocal, ) from ..websocket_manager import notify_phase_change, notify_session_ended logger = logging.getLogger(__name__) router = APIRouter(tags=["Sessions"]) def build_session_response(session: LessonSession) -> SessionResponse: """Baut die vollstaendige Session-Response.""" fsm = LessonStateMachine() timer = PhaseTimer() timer_status = timer.get_phase_status(session) phases_info = fsm.get_phases_info(session) return SessionResponse( session_id=session.session_id, teacher_id=session.teacher_id, class_id=session.class_id, subject=session.subject, topic=session.topic, current_phase=session.current_phase.value, phase_display_name=session.get_phase_display_name(), phase_started_at=session.phase_started_at.isoformat() if session.phase_started_at else None, lesson_started_at=session.lesson_started_at.isoformat() if session.lesson_started_at else None, lesson_ended_at=session.lesson_ended_at.isoformat() if session.lesson_ended_at else None, timer=TimerStatus(**timer_status), phases=[PhaseInfo(**p) for p in phases_info], phase_history=session.phase_history, notes=session.notes, homework=session.homework, is_active=fsm.is_lesson_active(session), is_ended=fsm.is_lesson_ended(session), is_paused=session.is_paused, ) # === Session CRUD Endpoints === @router.post("/sessions", response_model=SessionResponse) async def create_session(request: CreateSessionRequest) -> SessionResponse: """Erstellt eine neue Unterrichtsstunde (Session).""" init_db_if_needed() phase_durations = { "einstieg": 8, "erarbeitung": 20, "sicherung": 10, "transfer": 7, "reflexion": 5, } if request.phase_durations: phase_durations.update(request.phase_durations) session = LessonSession( session_id=str(uuid4()), teacher_id=request.teacher_id, class_id=request.class_id, subject=request.subject, topic=request.topic, phase_durations=phase_durations, ) sessions[session.session_id] = session persist_session(session) return build_session_response(session) @router.get("/sessions/{session_id}", response_model=SessionResponse) async def get_session(session_id: str) -> SessionResponse: """Ruft den aktuellen Status einer Session ab.""" session = get_session_or_404(session_id) return build_session_response(session) @router.post("/sessions/{session_id}/start", response_model=SessionResponse) async def start_lesson(session_id: str) -> SessionResponse: """Startet die Unterrichtsstunde.""" session = get_session_or_404(session_id) if session.current_phase != LessonPhase.NOT_STARTED: raise HTTPException( status_code=400, detail=f"Stunde bereits gestartet (aktuelle Phase: {session.current_phase.value})" ) fsm = LessonStateMachine() session = fsm.transition(session, LessonPhase.EINSTIEG) persist_session(session) return build_session_response(session) @router.post("/sessions/{session_id}/next-phase", response_model=SessionResponse) async def next_phase(session_id: str) -> SessionResponse: """Wechselt zur naechsten Phase.""" session = get_session_or_404(session_id) fsm = LessonStateMachine() next_p = fsm.next_phase(session.current_phase) if not next_p: raise HTTPException( status_code=400, detail=f"Keine naechste Phase verfuegbar (aktuelle Phase: {session.current_phase.value})" ) session = fsm.transition(session, next_p) persist_session(session) response = build_session_response(session) await notify_phase_change(session_id, session.current_phase.value, { "phase_display_name": session.get_phase_display_name(), "is_ended": session.current_phase == LessonPhase.ENDED }) return response @router.post("/sessions/{session_id}/end", response_model=SessionResponse) async def end_lesson(session_id: str) -> SessionResponse: """Beendet die Unterrichtsstunde sofort.""" session = get_session_or_404(session_id) if session.current_phase == LessonPhase.ENDED: raise HTTPException(status_code=400, detail="Stunde bereits beendet") if session.current_phase == LessonPhase.NOT_STARTED: raise HTTPException(status_code=400, detail="Stunde noch nicht gestartet") fsm = LessonStateMachine() while session.current_phase != LessonPhase.ENDED: next_p = fsm.next_phase(session.current_phase) if next_p: session = fsm.transition(session, next_p) else: break persist_session(session) await notify_session_ended(session_id) return build_session_response(session) @router.put("/sessions/{session_id}/notes", response_model=SessionResponse) async def update_notes(session_id: str, request: NotesRequest) -> SessionResponse: """Aktualisiert Notizen und Hausaufgaben der Stunde.""" session = get_session_or_404(session_id) session.notes = request.notes session.homework = request.homework persist_session(session) return build_session_response(session) @router.delete("/sessions/{session_id}") async def delete_session(session_id: str) -> Dict[str, str]: """Loescht eine Session.""" if session_id not in sessions: raise HTTPException(status_code=404, detail="Session nicht gefunden") del sessions[session_id] if DB_ENABLED: try: from ..services.persistence import delete_session_from_db delete_session_from_db(session_id) except Exception as e: logger.error(f"Failed to delete session {session_id} from DB: {e}") return {"status": "deleted", "session_id": session_id} # === Session History (Feature f17) === @router.get("/history/{teacher_id}", response_model=SessionHistoryResponse) async def get_session_history( teacher_id: str, limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0) ) -> SessionHistoryResponse: """Ruft die Session-History eines Lehrers ab (Feature f17).""" init_db_if_needed() if not DB_ENABLED: ended_sessions = [ s for s in sessions.values() if s.teacher_id == teacher_id and s.current_phase == LessonPhase.ENDED ] ended_sessions.sort(key=lambda x: x.lesson_ended_at or datetime.min, reverse=True) paginated = ended_sessions[offset:offset + limit] items = [] for s in paginated: duration = None if s.lesson_started_at and s.lesson_ended_at: duration = int((s.lesson_ended_at - s.lesson_started_at).total_seconds() / 60) items.append(SessionHistoryItem( session_id=s.session_id, teacher_id=s.teacher_id, class_id=s.class_id, subject=s.subject, topic=s.topic, lesson_started_at=s.lesson_started_at.isoformat() if s.lesson_started_at else None, lesson_ended_at=s.lesson_ended_at.isoformat() if s.lesson_ended_at else None, total_duration_minutes=duration, phases_completed=len(s.phase_history), notes=s.notes, homework=s.homework, )) return SessionHistoryResponse( sessions=items, total_count=len(ended_sessions), limit=limit, offset=offset, ) try: from classroom_engine.repository import SessionRepository db = SessionLocal() repo = SessionRepository(db) db_sessions = repo.get_history_by_teacher(teacher_id, limit, offset) from classroom_engine.db_models import LessonSessionDB, LessonPhaseEnum total_count = db.query(LessonSessionDB).filter( LessonSessionDB.teacher_id == teacher_id, LessonSessionDB.current_phase == LessonPhaseEnum.ENDED ).count() items = [] for db_session in db_sessions: duration = None if db_session.lesson_started_at and db_session.lesson_ended_at: duration = int((db_session.lesson_ended_at - db_session.lesson_started_at).total_seconds() / 60) phase_history = db_session.phase_history or [] items.append(SessionHistoryItem( session_id=db_session.id, teacher_id=db_session.teacher_id, class_id=db_session.class_id, subject=db_session.subject, topic=db_session.topic, lesson_started_at=db_session.lesson_started_at.isoformat() if db_session.lesson_started_at else None, lesson_ended_at=db_session.lesson_ended_at.isoformat() if db_session.lesson_ended_at else None, total_duration_minutes=duration, phases_completed=len(phase_history), notes=db_session.notes or "", homework=db_session.homework or "", )) db.close() return SessionHistoryResponse( sessions=items, total_count=total_count, limit=limit, offset=offset, ) except Exception as e: logger.error(f"Failed to get session history: {e}") raise HTTPException(status_code=500, detail="Fehler beim Laden der History")