# Architektur + Datenmodell ## Verantwortung pro Service ### school-service (Go/Gin) - Persistenz fuer alle Stammdaten + Constraints + Solutions - API-Gateway fuer studio-v2: validiert, ownership-checked, faked Auth im Dev-Mode - Trigger-Aufruf an solver-service nach POST /solutions ### timetable-solver-service (Python/FastAPI + Timefold) - Liest Problem aus PG via asyncpg - Baut Timefold-Domain (Lessons, Timeslots, Rooms, Rules) - Loest im ThreadPoolExecutor (Solver ist CPU-gebunden) - Schreibt Loesung direkt nach tt_lesson, updated tt_solution.status ### studio-v2 (Next.js) - `/stundenplan` Tab-Page mit 9 Tabs - Next.js API-Route `/api/school/*` proxied zu school-service - Solution-Polling alle 4 s wenn Solve laeuft ## Datenmodell ### Stammdaten (7 Tabellen) | Tabelle | Inhalt | Owner | |---------|--------|-------| | tt_class | Klassen (Name, Klassenstufe) | created_by_user_id | | tt_period | Zeitraster (Mo-So × Stunde × Start/Ende) | created_by_user_id | | tt_room | Raeume (Name, Typ, Kapazitaet, Aufzug) | created_by_user_id | | tt_subject | Faecher (Name, Kuerzel, Farbe, RoomType) | created_by_user_id | | tt_teacher | Lehrer als planbare Ressource | created_by_user_id | | tt_curriculum | Klasse × Fach → Wochenstunden | indirect via tt_class | | tt_assignment | Lehrer × Klasse × Fach | indirect via tt_teacher | ### Constraints (15 Tabellen) Pro Tabelle die Felder: `is_hard` (bool), `weight` (0-100), `active` (bool), `note` (TEXT), `created_by_user_id`. Aufgeteilt nach Parent-Entitaet: - Lehrer: `unavailable_day`, `unavailable_window`, `max_hours_day`, `max_hours_week`, `excluded_subject`, `excluded_room` - Fach: `min_day_gap`, `max_consecutive`, `contiguous_when_repeated`, `preferred_period`, `double_lesson` - Klasse: `max_hours_day`, `no_gaps` - Raum: `requires_type`, `unavailable` ### Solutions (2 Tabellen, Phase 5+7) | Tabelle | Inhalt | |---------|--------| | tt_solution | Solve-Run: Status, hard/soft Score, parent_solution_id, seconds_limit | | tt_lesson | Eine Stunde im Plan (class, subject, teacher, room, day, period, pinned) | `tt_lesson` hat drei `UNIQUE`-Constraints, die der DB-Layer selbst Konflikt- Lessons ablehnen laesst: - `(solution_id, class_id, day, period)` — Klasse nicht doppelt - `(solution_id, teacher_id, day, period)` — Lehrer nicht doppelt - `(solution_id, room_id, day, period)` — Raum nicht doppelt Damit kann ein fehlerhafter Solver-Output nicht in Daten landen, die das UI inkonsistent darstellt. ## Ownership-Modell Alles ist single-tenant pro `created_by_user_id`. CRUD-Endpoints filtern via `WHERE EXISTS (SELECT 1 FROM tt_ WHERE id = $X AND created_by_user_id = $user)`. Cross-Tenant-Zugriff ist auf SQL-Ebene ausgeschlossen. Im Dev-Mode injiziert `AuthMiddleware` einen festen UUID, damit Tests ohne JWT laufen koennen. Production-Build (`ENVIRONMENT=production`) deaktiviert den Bypass — JWT wird Pflicht.