[split-required] Split 700-870 LOC files across all services

backend-lehrer (11 files):
- llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6)
- messenger_api.py (840 → 5), print_generator.py (824 → 5)
- unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4)
- llm_gateway/routes/edu_search_seeds.py (710 → 4)

klausur-service (12 files):
- ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4)
- legal_corpus_api.py (790 → 4), page_crop.py (758 → 3)
- mail/ai_service.py (747 → 4), github_crawler.py (767 → 3)
- trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4)
- dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4)

website (6 pages):
- audit-checklist (867 → 8), content (806 → 6)
- screen-flow (790 → 4), scraper (789 → 5)
- zeugnisse (776 → 5), modules (745 → 4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 08:01:18 +02:00
parent b6983ab1dc
commit 34da9f4cda
106 changed files with 16500 additions and 16947 deletions

View File

@@ -1,726 +1,25 @@
"""
Classroom API - Context Routes
Classroom API - Context Routes — Barrel Re-export.
Split into submodules:
- context_core.py — Teacher context, onboarding endpoints
- context_events.py — Events & routines CRUD
- context_static.py — Static data, suggestions, sidebar, school year path
School year context, events, routines, and suggestions endpoints (Phase 8).
"""
from typing import Dict, Any, Optional
from datetime import datetime
import logging
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, Query, Depends
from classroom_engine import (
FEDERAL_STATES,
SCHOOL_TYPES,
MacroPhaseEnum,
)
from ..models import (
TeacherContextResponse,
SchoolInfo,
SchoolYearInfo,
MacroPhaseInfo,
CoreCounts,
ContextFlags,
UpdateContextRequest,
CreateEventRequest,
EventResponse,
CreateRoutineRequest,
RoutineResponse,
)
from ..services.persistence import (
init_db_if_needed,
DB_ENABLED,
SessionLocal,
)
logger = logging.getLogger(__name__)
from .context_core import router as _core_router
from .context_events import router as _events_router
from .context_static import router as _static_router
# Combine all sub-routers into a single router for backwards compatibility.
# The consumer imports `from .routes.context import router as context_router`.
router = APIRouter(tags=["Context"])
router.include_router(_core_router)
router.include_router(_events_router)
router.include_router(_static_router)
def get_db():
"""Database session dependency."""
if DB_ENABLED and SessionLocal:
db = SessionLocal()
try:
yield db
finally:
db.close()
else:
yield None
def _get_macro_phase_label(phase) -> str:
"""Gibt den Anzeigenamen einer Makro-Phase zurueck."""
labels = {
"onboarding": "Einrichtung",
"schuljahresstart": "Schuljahresstart",
"unterrichtsaufbau": "Unterrichtsaufbau",
"leistungsphase_1": "Leistungsphase 1",
"halbjahresabschluss": "Halbjahresabschluss",
"leistungsphase_2": "Leistungsphase 2",
"jahresabschluss": "Jahresabschluss",
}
phase_value = phase.value if hasattr(phase, 'value') else str(phase)
return labels.get(phase_value, phase_value)
# === Context Endpoints ===
@router.get("/v1/context", response_model=TeacherContextResponse)
async def get_teacher_context(
teacher_id: str = Query(..., description="Teacher ID"),
db=Depends(get_db)
):
"""
Liefert den aktuellen Makro-Kontext eines Lehrers.
Der Kontext beinhaltet:
- Schul-Informationen (Bundesland, Schulart)
- Schuljahr-Daten (aktuelles Jahr, Woche)
- Makro-Phase (ONBOARDING bis JAHRESABSCHLUSS)
- Zaehler (Klassen, geplante Klausuren, etc.)
- Status-Flags (Onboarding abgeschlossen, etc.)
"""
if DB_ENABLED and db:
try:
from classroom_engine.repository import TeacherContextRepository, SchoolyearEventRepository
repo = TeacherContextRepository(db)
context = repo.get_or_create(teacher_id)
# Zaehler berechnen
event_repo = SchoolyearEventRepository(db)
upcoming_exams = event_repo.get_upcoming(teacher_id, days=30)
exams_count = len([e for e in upcoming_exams if e.event_type.value == "exam"])
return TeacherContextResponse(
schema_version="1.0",
teacher_id=teacher_id,
school=SchoolInfo(
federal_state=context.federal_state or "BY",
federal_state_name=FEDERAL_STATES.get(context.federal_state, ""),
school_type=context.school_type or "gymnasium",
school_type_name=SCHOOL_TYPES.get(context.school_type, ""),
),
school_year=SchoolYearInfo(
id=context.schoolyear or "2024-2025",
start=context.schoolyear_start.isoformat() if context.schoolyear_start else None,
current_week=context.current_week or 1,
),
macro_phase=MacroPhaseInfo(
id=context.macro_phase.value,
label=_get_macro_phase_label(context.macro_phase),
confidence=1.0,
),
core_counts=CoreCounts(
classes=1 if context.has_classes else 0,
exams_scheduled=exams_count,
corrections_pending=0,
),
flags=ContextFlags(
onboarding_completed=context.onboarding_completed,
has_classes=context.has_classes,
has_schedule=context.has_schedule,
is_exam_period=context.is_exam_period,
is_before_holidays=context.is_before_holidays,
),
)
except Exception as e:
logger.error(f"Failed to get teacher context: {e}")
raise HTTPException(status_code=500, detail=f"Fehler beim Laden des Kontexts: {e}")
# Fallback ohne DB
return TeacherContextResponse(
schema_version="1.0",
teacher_id=teacher_id,
school=SchoolInfo(
federal_state="BY",
federal_state_name="Bayern",
school_type="gymnasium",
school_type_name="Gymnasium",
),
school_year=SchoolYearInfo(
id="2024-2025",
start=None,
current_week=1,
),
macro_phase=MacroPhaseInfo(
id="onboarding",
label="Einrichtung",
confidence=1.0,
),
core_counts=CoreCounts(),
flags=ContextFlags(),
)
@router.put("/v1/context", response_model=TeacherContextResponse)
async def update_teacher_context(
teacher_id: str,
request: UpdateContextRequest,
db=Depends(get_db)
):
"""
Aktualisiert den Kontext eines Lehrers.
"""
if not DB_ENABLED or not db:
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
try:
from classroom_engine.repository import TeacherContextRepository
repo = TeacherContextRepository(db)
# Validierung
if request.federal_state and request.federal_state not in FEDERAL_STATES:
raise HTTPException(status_code=400, detail=f"Ungueltiges Bundesland: {request.federal_state}")
if request.school_type and request.school_type not in SCHOOL_TYPES:
raise HTTPException(status_code=400, detail=f"Ungueltige Schulart: {request.school_type}")
# Parse datetime if provided
schoolyear_start = None
if request.schoolyear_start:
schoolyear_start = datetime.fromisoformat(request.schoolyear_start.replace('Z', '+00:00'))
repo.update_context(
teacher_id=teacher_id,
federal_state=request.federal_state,
school_type=request.school_type,
schoolyear=request.schoolyear,
schoolyear_start=schoolyear_start,
macro_phase=request.macro_phase,
current_week=request.current_week,
)
return await get_teacher_context(teacher_id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update teacher context: {e}")
raise HTTPException(status_code=500, detail=f"Fehler beim Aktualisieren: {e}")
@router.post("/v1/context/complete-onboarding")
async def complete_onboarding(
teacher_id: str = Query(...),
db=Depends(get_db)
):
"""Markiert das Onboarding als abgeschlossen."""
if not DB_ENABLED or not db:
return {"success": True, "macro_phase": "schuljahresstart", "note": "DB not available"}
try:
from classroom_engine.repository import TeacherContextRepository
repo = TeacherContextRepository(db)
context = repo.complete_onboarding(teacher_id)
return {
"success": True,
"macro_phase": context.macro_phase.value,
"teacher_id": teacher_id,
}
except Exception as e:
logger.error(f"Failed to complete onboarding: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.post("/v1/context/reset-onboarding")
async def reset_onboarding(
teacher_id: str = Query(...),
db=Depends(get_db)
):
"""Setzt das Onboarding zurueck (fuer Tests)."""
if not DB_ENABLED or not db:
return {"success": True, "macro_phase": "onboarding", "note": "DB not available"}
try:
from classroom_engine.repository import TeacherContextRepository
repo = TeacherContextRepository(db)
context = repo.get_or_create(teacher_id)
context.onboarding_completed = False
context.macro_phase = MacroPhaseEnum.ONBOARDING
db.commit()
db.refresh(context)
return {
"success": True,
"macro_phase": "onboarding",
"teacher_id": teacher_id,
}
except Exception as e:
logger.error(f"Failed to reset onboarding: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
# === Events Endpoints ===
@router.get("/v1/events")
async def get_events(
teacher_id: str = Query(...),
status: Optional[str] = None,
event_type: Optional[str] = None,
limit: int = 50,
db=Depends(get_db)
):
"""Holt Events eines Lehrers."""
if not DB_ENABLED or not db:
return {"events": [], "count": 0}
try:
from classroom_engine.repository import SchoolyearEventRepository
repo = SchoolyearEventRepository(db)
events = repo.get_by_teacher(teacher_id, status=status, event_type=event_type, limit=limit)
return {
"events": [repo.to_dict(e) for e in events],
"count": len(events),
}
except Exception as e:
logger.error(f"Failed to get events: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.get("/v1/events/upcoming")
async def get_upcoming_events(
teacher_id: str = Query(...),
days: int = 30,
limit: int = 10,
db=Depends(get_db)
):
"""Holt anstehende Events der naechsten X Tage."""
if not DB_ENABLED or not db:
return {"events": [], "count": 0}
try:
from classroom_engine.repository import SchoolyearEventRepository
repo = SchoolyearEventRepository(db)
events = repo.get_upcoming(teacher_id, days=days, limit=limit)
return {
"events": [repo.to_dict(e) for e in events],
"count": len(events),
}
except Exception as e:
logger.error(f"Failed to get upcoming events: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.post("/v1/events", response_model=EventResponse)
async def create_event(
teacher_id: str,
request: CreateEventRequest,
db=Depends(get_db)
):
"""Erstellt ein neues Schuljahr-Event."""
if not DB_ENABLED or not db:
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
try:
from classroom_engine.repository import SchoolyearEventRepository
repo = SchoolyearEventRepository(db)
start_date = datetime.fromisoformat(request.start_date.replace('Z', '+00:00'))
end_date = None
if request.end_date:
end_date = datetime.fromisoformat(request.end_date.replace('Z', '+00:00'))
event = repo.create(
teacher_id=teacher_id,
title=request.title,
event_type=request.event_type,
start_date=start_date,
end_date=end_date,
class_id=request.class_id,
subject=request.subject,
description=request.description,
needs_preparation=request.needs_preparation,
reminder_days_before=request.reminder_days_before,
)
return EventResponse(
id=event.id,
teacher_id=event.teacher_id,
event_type=event.event_type.value,
title=event.title,
description=event.description,
start_date=event.start_date.isoformat(),
end_date=event.end_date.isoformat() if event.end_date else None,
class_id=event.class_id,
subject=event.subject,
status=event.status.value,
needs_preparation=event.needs_preparation,
preparation_done=event.preparation_done,
reminder_days_before=event.reminder_days_before,
)
except Exception as e:
logger.error(f"Failed to create event: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.delete("/v1/events/{event_id}")
async def delete_event(event_id: str, db=Depends(get_db)):
"""Loescht ein Event."""
if not DB_ENABLED or not db:
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
try:
from classroom_engine.repository import SchoolyearEventRepository
repo = SchoolyearEventRepository(db)
if repo.delete(event_id):
return {"success": True, "deleted_id": event_id}
raise HTTPException(status_code=404, detail="Event nicht gefunden")
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete event: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
# === Routines Endpoints ===
@router.get("/v1/routines")
async def get_routines(
teacher_id: str = Query(...),
is_active: bool = True,
routine_type: Optional[str] = None,
db=Depends(get_db)
):
"""Holt Routinen eines Lehrers."""
if not DB_ENABLED or not db:
return {"routines": [], "count": 0}
try:
from classroom_engine.repository import RecurringRoutineRepository
repo = RecurringRoutineRepository(db)
routines = repo.get_by_teacher(teacher_id, is_active=is_active, routine_type=routine_type)
return {
"routines": [repo.to_dict(r) for r in routines],
"count": len(routines),
}
except Exception as e:
logger.error(f"Failed to get routines: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.get("/v1/routines/today")
async def get_today_routines(teacher_id: str = Query(...), db=Depends(get_db)):
"""Holt Routinen die heute stattfinden."""
if not DB_ENABLED or not db:
return {"routines": [], "count": 0}
try:
from classroom_engine.repository import RecurringRoutineRepository
repo = RecurringRoutineRepository(db)
routines = repo.get_today(teacher_id)
return {
"routines": [repo.to_dict(r) for r in routines],
"count": len(routines),
}
except Exception as e:
logger.error(f"Failed to get today's routines: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.post("/v1/routines", response_model=RoutineResponse)
async def create_routine(
teacher_id: str,
request: CreateRoutineRequest,
db=Depends(get_db)
):
"""Erstellt eine neue wiederkehrende Routine."""
if not DB_ENABLED or not db:
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
try:
from classroom_engine.repository import RecurringRoutineRepository
repo = RecurringRoutineRepository(db)
routine = repo.create(
teacher_id=teacher_id,
title=request.title,
routine_type=request.routine_type,
recurrence_pattern=request.recurrence_pattern,
day_of_week=request.day_of_week,
day_of_month=request.day_of_month,
time_of_day=request.time_of_day,
duration_minutes=request.duration_minutes,
description=request.description,
)
return RoutineResponse(
id=routine.id,
teacher_id=routine.teacher_id,
routine_type=routine.routine_type.value,
title=routine.title,
description=routine.description,
recurrence_pattern=routine.recurrence_pattern.value,
day_of_week=routine.day_of_week,
day_of_month=routine.day_of_month,
time_of_day=routine.time_of_day.isoformat() if routine.time_of_day else None,
duration_minutes=routine.duration_minutes,
is_active=routine.is_active,
)
except Exception as e:
logger.error(f"Failed to create routine: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
@router.delete("/v1/routines/{routine_id}")
async def delete_routine(routine_id: str, db=Depends(get_db)):
"""Loescht eine Routine."""
if not DB_ENABLED or not db:
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
try:
from classroom_engine.repository import RecurringRoutineRepository
repo = RecurringRoutineRepository(db)
if repo.delete(routine_id):
return {"success": True, "deleted_id": routine_id}
raise HTTPException(status_code=404, detail="Routine nicht gefunden")
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete routine: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
# === Static Data Endpoints ===
@router.get("/v1/federal-states")
async def get_federal_states():
"""Gibt alle Bundeslaender zurueck."""
return {
"federal_states": [{"id": k, "name": v} for k, v in FEDERAL_STATES.items()]
}
@router.get("/v1/school-types")
async def get_school_types():
"""Gibt alle Schularten zurueck."""
return {
"school_types": [{"id": k, "name": v} for k, v in SCHOOL_TYPES.items()]
}
@router.get("/v1/macro-phases")
async def get_macro_phases():
"""Gibt alle Makro-Phasen mit Beschreibungen zurueck."""
phases = [
{"id": "onboarding", "label": "Einrichtung", "description": "Ersteinrichtung (Klassen, Stundenplan)", "order": 1},
{"id": "schuljahresstart", "label": "Schuljahresstart", "description": "Erste 2-3 Wochen des Schuljahres", "order": 2},
{"id": "unterrichtsaufbau", "label": "Unterrichtsaufbau", "description": "Routinen etablieren, erste Bewertungen", "order": 3},
{"id": "leistungsphase_1", "label": "Leistungsphase 1", "description": "Erste Klassenarbeiten und Klausuren", "order": 4},
{"id": "halbjahresabschluss", "label": "Halbjahresabschluss", "description": "Notenschluss, Zeugnisse, Konferenzen", "order": 5},
{"id": "leistungsphase_2", "label": "Leistungsphase 2", "description": "Zweites Halbjahr, Pruefungsvorbereitung", "order": 6},
{"id": "jahresabschluss", "label": "Jahresabschluss", "description": "Finale Noten, Versetzung, Schuljahresende", "order": 7},
]
return {"macro_phases": phases}
@router.get("/v1/event-types")
async def get_event_types():
"""Gibt alle Event-Typen zurueck."""
types = [
{"id": "exam", "label": "Klassenarbeit/Klausur"},
{"id": "parent_evening", "label": "Elternabend"},
{"id": "trip", "label": "Klassenfahrt/Ausflug"},
{"id": "project", "label": "Projektwoche"},
{"id": "internship", "label": "Praktikum"},
{"id": "presentation", "label": "Referate/Praesentationen"},
{"id": "sports_day", "label": "Sporttag"},
{"id": "school_festival", "label": "Schulfest"},
{"id": "parent_consultation", "label": "Elternsprechtag"},
{"id": "grade_deadline", "label": "Notenschluss"},
{"id": "report_cards", "label": "Zeugnisausgabe"},
{"id": "holiday_start", "label": "Ferienbeginn"},
{"id": "holiday_end", "label": "Ferienende"},
{"id": "other", "label": "Sonstiges"},
]
return {"event_types": types}
@router.get("/v1/routine-types")
async def get_routine_types():
"""Gibt alle Routine-Typen zurueck."""
types = [
{"id": "teacher_conference", "label": "Lehrerkonferenz"},
{"id": "subject_conference", "label": "Fachkonferenz"},
{"id": "office_hours", "label": "Sprechstunde"},
{"id": "team_meeting", "label": "Teamsitzung"},
{"id": "supervision", "label": "Pausenaufsicht"},
{"id": "correction_time", "label": "Korrekturzeit"},
{"id": "prep_time", "label": "Vorbereitungszeit"},
{"id": "other", "label": "Sonstiges"},
]
return {"routine_types": types}
# === Suggestions & Sidebar ===
@router.get("/v1/suggestions")
async def get_suggestions(
teacher_id: str = Query(...),
limit: int = Query(5, ge=1, le=20),
db=Depends(get_db)
):
"""Generiert kontextbasierte Vorschlaege fuer einen Lehrer."""
if DB_ENABLED and db:
try:
from classroom_engine.suggestions import SuggestionGenerator
generator = SuggestionGenerator(db)
result = generator.generate(teacher_id, limit=limit)
return result
except Exception as e:
logger.error(f"Failed to generate suggestions: {e}")
raise HTTPException(status_code=500, detail=f"Fehler: {e}")
return {
"active_contexts": [],
"suggestions": [],
"signals_summary": {
"macro_phase": "onboarding",
"current_week": 1,
"has_classes": False,
"exams_soon": 0,
"routines_today": 0,
},
"total_suggestions": 0,
}
@router.get("/v1/sidebar")
async def get_sidebar(
teacher_id: str = Query(...),
mode: str = Query("companion"),
db=Depends(get_db)
):
"""Generiert das dynamische Sidebar-Model."""
if mode == "companion":
now_relevant = []
if DB_ENABLED and db:
try:
from classroom_engine.suggestions import SuggestionGenerator
generator = SuggestionGenerator(db)
result = generator.generate(teacher_id, limit=5)
now_relevant = [
{
"id": s["id"],
"label": s["title"],
"state": "recommended" if s["priority"] > 70 else "default",
"badge": s.get("badge"),
"icon": s.get("icon", "lightbulb"),
"action_url": s.get("action_url"),
}
for s in result.get("suggestions", [])
]
except Exception as e:
logger.warning(f"Failed to get suggestions for sidebar: {e}")
return {
"mode": "companion",
"sections": [
{"id": "SEARCH", "type": "search_bar", "placeholder": "Suchen..."},
{
"id": "NOW_RELEVANT",
"type": "list",
"title": "Jetzt relevant",
"items": now_relevant if now_relevant else [
{"id": "no_suggestions", "label": "Keine Vorschlaege", "state": "default", "icon": "check_circle"}
],
},
{
"id": "ALL_MODULES",
"type": "folder",
"label": "Alle Module",
"icon": "folder",
"collapsed": True,
"items": [
{"id": "lesson", "label": "Stundenmodus", "icon": "timer"},
{"id": "classes", "label": "Klassen", "icon": "groups"},
{"id": "exams", "label": "Klausuren", "icon": "quiz"},
{"id": "grades", "label": "Noten", "icon": "calculate"},
{"id": "calendar", "label": "Kalender", "icon": "calendar_month"},
{"id": "materials", "label": "Materialien", "icon": "folder_open"},
],
},
{
"id": "QUICK_ACTIONS",
"type": "actions",
"title": "Kurzaktionen",
"items": [
{"id": "scan", "label": "Scan hochladen", "icon": "upload_file"},
{"id": "note", "label": "Notiz erstellen", "icon": "note_add"},
],
},
],
}
else:
return {
"mode": "classic",
"sections": [
{
"id": "NAVIGATION",
"type": "tree",
"items": [
{"id": "dashboard", "label": "Dashboard", "icon": "dashboard", "url": "/dashboard"},
{"id": "lesson", "label": "Stundenmodus", "icon": "timer", "url": "/lesson"},
{"id": "classes", "label": "Klassen", "icon": "groups", "url": "/classes"},
{"id": "exams", "label": "Klausuren", "icon": "quiz", "url": "/exams"},
{"id": "grades", "label": "Noten", "icon": "calculate", "url": "/grades"},
{"id": "calendar", "label": "Kalender", "icon": "calendar_month", "url": "/calendar"},
{"id": "materials", "label": "Materialien", "icon": "folder_open", "url": "/materials"},
{"id": "settings", "label": "Einstellungen", "icon": "settings", "url": "/settings"},
],
},
],
}
@router.get("/v1/path")
async def get_schoolyear_path(teacher_id: str = Query(...), db=Depends(get_db)):
"""Generiert den Schuljahres-Pfad mit Meilensteinen."""
current_phase = "onboarding"
if DB_ENABLED and db:
try:
from classroom_engine.repository import TeacherContextRepository
repo = TeacherContextRepository(db)
context = repo.get_or_create(teacher_id)
current_phase = context.macro_phase.value
except Exception as e:
logger.warning(f"Failed to get context for path: {e}")
phase_order = [
"onboarding", "schuljahresstart", "unterrichtsaufbau",
"leistungsphase_1", "halbjahresabschluss", "leistungsphase_2", "jahresabschluss",
]
current_index = phase_order.index(current_phase) if current_phase in phase_order else 0
milestones = [
{"id": "MS_START", "label": "Start", "phase": "onboarding", "icon": "flag"},
{"id": "MS_SETUP", "label": "Einrichtung", "phase": "schuljahresstart", "icon": "tune"},
{"id": "MS_ROUTINE", "label": "Routinen", "phase": "unterrichtsaufbau", "icon": "repeat"},
{"id": "MS_EXAM_1", "label": "Klausuren", "phase": "leistungsphase_1", "icon": "quiz"},
{"id": "MS_HALFYEAR", "label": "Halbjahr", "phase": "halbjahresabschluss", "icon": "event"},
{"id": "MS_EXAM_2", "label": "Pruefungen", "phase": "leistungsphase_2", "icon": "school"},
{"id": "MS_END", "label": "Abschluss", "phase": "jahresabschluss", "icon": "celebration"},
]
for milestone in milestones:
phase = milestone["phase"]
phase_index = phase_order.index(phase) if phase in phase_order else 999
if phase_index < current_index:
milestone["status"] = "done"
elif phase_index == current_index:
milestone["status"] = "current"
else:
milestone["status"] = "upcoming"
current_milestone_id = next(
(m["id"] for m in milestones if m["status"] == "current"),
milestones[0]["id"]
)
progress = int((current_index / (len(phase_order) - 1)) * 100) if len(phase_order) > 1 else 0
return {
"milestones": milestones,
"current_milestone_id": current_milestone_id,
"progress_percent": progress,
"current_phase": current_phase,
}
__all__ = ["router"]