feat: BreakPilot PWA - Full codebase (clean push without large binaries)
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed

All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
This commit is contained in:
BreakPilot Dev
2026-02-11 13:25:58 +01:00
commit 19855efacc
2512 changed files with 933814 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
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}