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>
198 lines
5.7 KiB
Python
198 lines
5.7 KiB
Python
from typing import List, Dict, Any, Optional
|
||
from datetime import datetime
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
|
||
from learning_units import (
|
||
LearningUnit,
|
||
LearningUnitCreate,
|
||
LearningUnitUpdate,
|
||
list_learning_units,
|
||
get_learning_unit,
|
||
create_learning_unit,
|
||
update_learning_unit,
|
||
delete_learning_unit,
|
||
)
|
||
|
||
|
||
router = APIRouter(
|
||
prefix="/learning-units",
|
||
tags=["learning-units"],
|
||
)
|
||
|
||
|
||
# ---------- Payload-Modelle für das Frontend ----------
|
||
|
||
|
||
class LearningUnitCreatePayload(BaseModel):
|
||
"""
|
||
Payload so, wie er aus dem Frontend kommt:
|
||
{
|
||
"student": "...",
|
||
"subject": "...",
|
||
"title": "...",
|
||
"grade": "7a"
|
||
}
|
||
"""
|
||
student: Optional[str] = None
|
||
subject: Optional[str] = None
|
||
title: Optional[str] = None
|
||
grade: Optional[str] = None
|
||
|
||
|
||
class AttachWorksheetsPayload(BaseModel):
|
||
worksheet_files: List[str]
|
||
|
||
|
||
class RemoveWorksheetPayload(BaseModel):
|
||
worksheet_file: str
|
||
|
||
|
||
# ---------- Hilfsfunktion: Backend-Modell -> Frontend-Objekt ----------
|
||
|
||
|
||
def unit_to_frontend_dict(lu: LearningUnit) -> Dict[str, Any]:
|
||
"""
|
||
Wandelt eine LearningUnit in das Format um, das das Frontend erwartet.
|
||
Wichtig sind:
|
||
- id
|
||
- label (sichtbarer Name)
|
||
- meta (Untertitelzeile)
|
||
- worksheet_files (Liste von Dateinamen)
|
||
"""
|
||
label = lu.title or "Lerneinheit"
|
||
|
||
# Meta-Text: z.B. "Thema: Auge · Klasse: 7a · angelegt am 10.12.2025"
|
||
meta_parts: List[str] = []
|
||
if lu.topic:
|
||
meta_parts.append(f"Thema: {lu.topic}")
|
||
if lu.grade_level:
|
||
meta_parts.append(f"Klasse: {lu.grade_level}")
|
||
created_str = lu.created_at.strftime("%d.%m.%Y")
|
||
meta_parts.append(f"angelegt am {created_str}")
|
||
|
||
meta = " · ".join(meta_parts)
|
||
|
||
return {
|
||
"id": lu.id,
|
||
"label": label,
|
||
"meta": meta,
|
||
"title": lu.title,
|
||
"topic": lu.topic,
|
||
"grade_level": lu.grade_level,
|
||
"language": lu.language,
|
||
"status": lu.status,
|
||
"worksheet_files": lu.worksheet_files,
|
||
"created_at": lu.created_at.isoformat(),
|
||
"updated_at": lu.updated_at.isoformat(),
|
||
}
|
||
|
||
|
||
# ---------- Endpunkte ----------
|
||
|
||
|
||
@router.get("/", response_model=List[Dict[str, Any]])
|
||
def api_list_learning_units():
|
||
"""Alle Lerneinheiten für das Frontend auflisten."""
|
||
units = list_learning_units()
|
||
return [unit_to_frontend_dict(u) for u in units]
|
||
|
||
|
||
@router.post("/", response_model=Dict[str, Any])
|
||
def api_create_learning_unit(payload: LearningUnitCreatePayload):
|
||
"""
|
||
Neue Lerneinheit anlegen.
|
||
Mapped das Frontend-Payload (student/subject/title/grade)
|
||
auf das generische LearningUnit-Modell.
|
||
"""
|
||
|
||
# Mindestens eines der Felder muss gesetzt sein
|
||
if not (payload.student or payload.subject or payload.title):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="Bitte mindestens Schüler/in, Fach oder Thema angeben.",
|
||
)
|
||
|
||
# Titel/Topic bestimmen
|
||
# sichtbarer Titel: bevorzugt Thema (title), sonst Kombination
|
||
if payload.title:
|
||
title = payload.title
|
||
else:
|
||
parts = []
|
||
if payload.subject:
|
||
parts.append(payload.subject)
|
||
if payload.student:
|
||
parts.append(payload.student)
|
||
title = " – ".join(parts) if parts else "Lerneinheit"
|
||
|
||
topic = payload.title or payload.subject or None
|
||
grade_level = payload.grade or None
|
||
|
||
lu_create = LearningUnitCreate(
|
||
title=title,
|
||
description=None,
|
||
topic=topic,
|
||
grade_level=grade_level,
|
||
language="de",
|
||
worksheet_files=[],
|
||
status="raw",
|
||
)
|
||
|
||
lu = create_learning_unit(lu_create)
|
||
return unit_to_frontend_dict(lu)
|
||
|
||
|
||
@router.post("/{unit_id}/attach-worksheets", response_model=Dict[str, Any])
|
||
def api_attach_worksheets(unit_id: str, payload: AttachWorksheetsPayload):
|
||
"""
|
||
Fügt der Lerneinheit eine oder mehrere Arbeitsblätter hinzu.
|
||
"""
|
||
lu = get_learning_unit(unit_id)
|
||
if not lu:
|
||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||
|
||
files_to_add = [f for f in payload.worksheet_files if f not in lu.worksheet_files]
|
||
if files_to_add:
|
||
new_list = lu.worksheet_files + files_to_add
|
||
update = LearningUnitUpdate(worksheet_files=new_list)
|
||
lu = update_learning_unit(unit_id, update)
|
||
if not lu:
|
||
raise HTTPException(status_code=500, detail="Lerneinheit konnte nicht aktualisiert werden.")
|
||
|
||
return unit_to_frontend_dict(lu)
|
||
|
||
|
||
@router.post("/{unit_id}/remove-worksheet", response_model=Dict[str, Any])
|
||
def api_remove_worksheet(unit_id: str, payload: RemoveWorksheetPayload):
|
||
"""
|
||
Entfernt genau ein Arbeitsblatt aus der Lerneinheit.
|
||
"""
|
||
lu = get_learning_unit(unit_id)
|
||
if not lu:
|
||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||
|
||
if payload.worksheet_file not in lu.worksheet_files:
|
||
# Nichts zu tun, aber kein Fehler – einfach unverändert zurückgeben
|
||
return unit_to_frontend_dict(lu)
|
||
|
||
new_list = [f for f in lu.worksheet_files if f != payload.worksheet_file]
|
||
update = LearningUnitUpdate(worksheet_files=new_list)
|
||
lu = update_learning_unit(unit_id, update)
|
||
if not lu:
|
||
raise HTTPException(status_code=500, detail="Lerneinheit konnte nicht aktualisiert werden.")
|
||
|
||
return unit_to_frontend_dict(lu)
|
||
|
||
|
||
@router.delete("/{unit_id}")
|
||
def api_delete_learning_unit(unit_id: str):
|
||
"""
|
||
Lerneinheit komplett löschen (aktuell vom Frontend noch nicht verwendet).
|
||
"""
|
||
ok = delete_learning_unit(unit_id)
|
||
if not ok:
|
||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||
return {"status": "deleted", "id": unit_id}
|
||
|