Phase 8: CSV + ICS export, print view, MkDocs docs, SBOM + dev-mode auth

Auth (Test-Mode):
  - middleware.AuthMiddleware now takes a devMode flag. In dev,
    requests without Authorization fall back to a deterministic dev
    UUID (00000000-...-001) and role=teacher. ENVIRONMENT=production
    re-enables the strict 401 path.
  - main.go wires devMode = cfg.Environment != "production".
  - page.tsx replaces the red 'Anmeldung noch nicht integriert' banner
    with a softer Testumgebung notice; the manual-token form moves
    behind a nested details block.

Export endpoints (school-service):
  - LoadExportLessons joins tt_lesson with tt_period for wall-clock
    times; one query feeds both CSV and ICS.
  - WriteCSV streams 10 columns including pinned flag.
  - WriteICS emits one VEVENT per lesson anchored to a Monday — caller
    overridable via ?start=YYYY-MM-DD. RFC 5545 escapes for ',', ';',
    '\n' in icsEscape().
  - NextMonday helper for the default anchor.
  - GET /timetable/solutions/:id/export.{csv,ics} handlers attach
    Content-Disposition: attachment so browsers download instead of
    rendering.

Frontend:
  - lib/stundenplan/api.ts downloadSolutionExport() fetches as blob,
    triggers a synthetic <a download> click, and forwards the JWT when
    present.
  - PlanView gains CSV / ICS / Drucken buttons next to the perspective
    selector. The toolbar carries class 'no-print' so window.print()
    yields only the grid.
  - globals.css @media print rule hides chrome, forces white
    background, gives the table proper borders for A4.

Docs:
  - docs-src/services/stundenplan/{index,architecture,constraints,
    solver-tuning,export}.md with nav entry in mkdocs.yml under
    Services → Stundenplaner.
  - sbom/stundenplan/README.md lists manually-verified key dependencies
    and the policy reference. scripts/stundenplan-sbom.sh generates
    full machine-readable inventories via go-licenses + pip-licenses
    + license-checker when those tools are available.

Tests:
  - internal/services/timetable_exports_test.go: 4 unit tests covering
    CSV column layout + quoting, ICS structure + DTSTART formatting,
    icsEscape special chars, NextMonday weekday math.
  - studio-v2/e2e/stundenplan-export.spec.ts split out of the main spec
    file (LOC budget) — 3 tests for button render, CSV download,
    ICS download.
  - mockSchoolApi extended with export.csv + export.ics routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-22 08:57:07 +02:00
parent bf5ea860cc
commit 306886a42b
20 changed files with 1014 additions and 43 deletions
@@ -0,0 +1,71 @@
# Constraint-Referenz
Jeder Constraint-Eintrag im UI legt eine Row in der korrespondierenden
`tt_constraint_*` Tabelle an. Der Solver liest sie als Problem-Facts und
joined sie gegen die Lessons.
## Gemeinsame Felder
| Feld | Typ | Bedeutung |
|------|-----|-----------|
| `is_hard` | bool | true = Solver muss einhalten (HardScore -1 pro Verstoss). false = Soft-Penalty (SoftScore -weight pro Verstoss) |
| `weight` | int 0-100 | Multiplikator fuer Soft-Penalties; bei Hard ignoriert |
| `active` | bool | inaktive Rows werden vom Solver ignoriert |
| `note` | TEXT | Freier Begruendungstext fuer den Rektor |
## Constraint-Typen
### Universal (immer aktiv, nicht abschaltbar)
| Constraint | Bedeutung |
|------------|-----------|
| `class_conflict` | Eine Klasse hat nur eine Lesson pro Timeslot |
| `teacher_conflict` | Ein Lehrer haelt nur eine Lesson pro Timeslot |
| `room_conflict` | Ein Raum hostet nur eine Lesson pro Timeslot |
### DB-Driven (vom Rektor konfigurierbar)
Jeder Typ existiert als `_hard` und `_soft` Constraint im Provider:
| Typ | Tabelle | Beispiel |
|-----|---------|----------|
| Lehrer Tag nicht verfuegbar | `tt_constraint_teacher_unavailable_day` | „Anna nie Montags" |
| Lehrer Zeitfenster nicht verfuegbar | `tt_constraint_teacher_unavailable_window` | „Bob Dienstag 1317 Uhr nicht" |
| Lehrer Max h/Tag | `tt_constraint_teacher_max_hours_day` | Anti-Burnout |
| Lehrer Max h/Woche | `tt_constraint_teacher_max_hours_week` | Teilzeit-Cap |
| Lehrer Fach ausgeschlossen | `tt_constraint_teacher_excluded_subject` | Qualifikationsluecke |
| Lehrer Raum ausgeschlossen | `tt_constraint_teacher_excluded_room` | Rollstuhl, kein Fahrstuhl |
| Fach Mindest-Tagesabstand | `tt_constraint_subject_min_day_gap` | Mathe nicht 2 Tage hintereinander |
| Fach Max Stunden am Stueck | `tt_constraint_subject_max_consecutive` | Keine Dreifachstunde |
| Fach Mehrfach=zusammen | `tt_constraint_subject_contiguous_when_repeated` | Wenn 2× am Tag, dann benachbart |
| Fach Bevorzugte Stunden | `tt_constraint_subject_preferred_period` | Hauptfaecher morgens |
| Fach Doppelstunde bevorzugt | `tt_constraint_subject_double_lesson` | Sport als 90-min-Block |
| Klasse Max h/Tag | `tt_constraint_class_max_hours_day` | Jugendgerecht |
| Klasse Keine Freistunden | `tt_constraint_class_no_gaps` | Soft, minimiert Loecher |
| Raumtyp erforderlich | `tt_constraint_room_requires_type` | Sport → Sporthalle |
| Raum nicht verfuegbar | `tt_constraint_room_unavailable` | Wartung, Renovierung |
## Hard vs. Soft — Faustregel
- **Hard** wenn die Schule den Plan rechtlich oder physisch nicht
ausfuehren kann (Lehrervertrag, Behinderung, Raum existiert nicht).
- **Soft** wenn es nur eine Praeferenz ist („Mathe lieber morgens",
„keine Freistunden").
Score-Bewertung im UI:
- `hard_score = 0` → Plan ist gueltig
- `hard_score < 0` → mindestens eine harte Regel ist verletzt (Solver
meldet das als `infeasible`)
- `soft_score` → wird in den UI angezeigt; je naeher an 0, desto besser
## Erweitern um einen 16. Constraint-Typ
1. Neue Tabelle in `school-service/internal/database/timetable_constraints_migrations.go`
2. Model + DTO in `models/timetable_constraints.go`
3. Service + Handler im gleichen Paket-Pattern wie die existierenden 15
4. Route in `cmd/server/main.go`
5. Rule-Dataclass in `timetable-solver-service/app/rules.py`
6. ProblemFactCollection in `domain.py`
7. ConstraintProvider-Funktion in `constraints.py` (Hard + Soft Variante)
8. Frontend: Editor-Komponente in `_components/regeln/`, dann in
`RegelnHub.tsx` registrieren