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>
282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""
|
|
Classroom API - Static Data, Suggestions & Sidebar Routes
|
|
|
|
Federal states, school types, macro phases, event/routine types,
|
|
suggestions, sidebar model, and school year path.
|
|
"""
|
|
|
|
from typing import Optional
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
|
|
|
from classroom_engine import FEDERAL_STATES, SCHOOL_TYPES
|
|
|
|
from ..services.persistence import (
|
|
DB_ENABLED,
|
|
SessionLocal,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["Context"])
|
|
|
|
|
|
def get_db():
|
|
"""Database session dependency."""
|
|
if DB_ENABLED and SessionLocal:
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
else:
|
|
yield None
|
|
|
|
|
|
# === 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,
|
|
}
|