[split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
173
backend-lehrer/classroom/routes/sessions_actions.py
Normal file
173
backend-lehrer/classroom/routes/sessions_actions.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Classroom API - Session Actions Routes
|
||||
|
||||
Quick actions (pause, extend, timer), suggestions, utility endpoints.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
|
||||
from classroom_engine import (
|
||||
LessonPhase,
|
||||
LessonStateMachine,
|
||||
PhaseTimer,
|
||||
SuggestionEngine,
|
||||
LESSON_PHASES,
|
||||
)
|
||||
|
||||
from ..models import (
|
||||
ExtendTimeRequest,
|
||||
TimerStatus,
|
||||
SuggestionItem,
|
||||
SuggestionsResponse,
|
||||
PhasesListResponse,
|
||||
ActiveSessionsResponse,
|
||||
)
|
||||
from ..services.persistence import (
|
||||
sessions,
|
||||
persist_session,
|
||||
get_session_or_404,
|
||||
DB_ENABLED,
|
||||
SessionLocal,
|
||||
)
|
||||
from .sessions_core import build_session_response, SessionResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["Sessions"])
|
||||
|
||||
|
||||
# === Quick Actions (Feature f26/f27/f28) ===
|
||||
|
||||
@router.post("/sessions/{session_id}/pause", response_model=SessionResponse)
|
||||
async def toggle_pause(session_id: str) -> SessionResponse:
|
||||
"""Pausiert oder setzt die laufende Stunde fort (Feature f27)."""
|
||||
session = get_session_or_404(session_id)
|
||||
|
||||
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
|
||||
raise HTTPException(status_code=400, detail="Stunde ist nicht aktiv")
|
||||
|
||||
if session.is_paused:
|
||||
if session.pause_started_at:
|
||||
pause_duration = (datetime.utcnow() - session.pause_started_at).total_seconds()
|
||||
session.total_paused_seconds += int(pause_duration)
|
||||
session.is_paused = False
|
||||
session.pause_started_at = None
|
||||
else:
|
||||
session.is_paused = True
|
||||
session.pause_started_at = datetime.utcnow()
|
||||
|
||||
persist_session(session)
|
||||
return build_session_response(session)
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/extend", response_model=SessionResponse)
|
||||
async def extend_phase(session_id: str, request: ExtendTimeRequest) -> SessionResponse:
|
||||
"""Verlaengert die aktuelle Phase um zusaetzliche Minuten (Feature f28)."""
|
||||
session = get_session_or_404(session_id)
|
||||
|
||||
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
|
||||
raise HTTPException(status_code=400, detail="Stunde ist nicht aktiv")
|
||||
|
||||
phase_id = session.current_phase.value
|
||||
current_duration = session.phase_durations.get(phase_id, 10)
|
||||
session.phase_durations[phase_id] = current_duration + request.minutes
|
||||
|
||||
persist_session(session)
|
||||
return build_session_response(session)
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/timer", response_model=TimerStatus)
|
||||
async def get_timer(session_id: str) -> TimerStatus:
|
||||
"""Ruft den Timer-Status der aktuellen Phase ab."""
|
||||
session = get_session_or_404(session_id)
|
||||
timer = PhaseTimer()
|
||||
status = timer.get_phase_status(session)
|
||||
return TimerStatus(**status)
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/suggestions", response_model=SuggestionsResponse)
|
||||
async def get_suggestions(
|
||||
session_id: str,
|
||||
limit: int = Query(3, ge=1, le=10)
|
||||
) -> SuggestionsResponse:
|
||||
"""Ruft phasenspezifische Aktivitaets-Vorschlaege ab."""
|
||||
session = get_session_or_404(session_id)
|
||||
engine = SuggestionEngine()
|
||||
response = engine.get_suggestions_response(session, limit)
|
||||
|
||||
return SuggestionsResponse(
|
||||
suggestions=[SuggestionItem(**s) for s in response["suggestions"]],
|
||||
current_phase=response["current_phase"],
|
||||
phase_display_name=response["phase_display_name"],
|
||||
total_available=response["total_available"],
|
||||
)
|
||||
|
||||
|
||||
# === Utility 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_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)}"
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "classroom-engine",
|
||||
"active_sessions": len(sessions),
|
||||
"db_enabled": DB_ENABLED,
|
||||
"db_status": db_status,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user