From 77c720e2dfd21caba52fbca0c10359d47a82881b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 22 May 2026 18:41:31 +0200 Subject: [PATCH] Document Stundenplan + Schulkalender end-of-session state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md gets a new section summarising the two feature strands, pitfalls (Timefold name, JSX quotes, LOC budget), the auth/messaging outsourcing, and pointers to the three memory files for next session. - docs-src/services/schulkalender/ — 5 MkDocs pages mirroring the stundenplan structure: index, architecture, holidays, parent-flow, notifications. Each with DB tables, endpoints, and the dispatch payload contract for the colleague's Matrix/Email services. - mkdocs.yml gains the Schulkalender nav entry under Services. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/CLAUDE.md | 29 ++++++ .../services/schulkalender/architecture.md | 64 +++++++++++++ docs-src/services/schulkalender/holidays.md | 71 ++++++++++++++ docs-src/services/schulkalender/index.md | 51 ++++++++++ .../services/schulkalender/notifications.md | 96 +++++++++++++++++++ .../services/schulkalender/parent-flow.md | 54 +++++++++++ mkdocs.yml | 6 ++ 7 files changed, 371 insertions(+) create mode 100644 docs-src/services/schulkalender/architecture.md create mode 100644 docs-src/services/schulkalender/holidays.md create mode 100644 docs-src/services/schulkalender/index.md create mode 100644 docs-src/services/schulkalender/notifications.md create mode 100644 docs-src/services/schulkalender/parent-flow.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ecb2a7c..0feaf04 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -243,6 +243,35 @@ ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-lehrer && git push all --- +## Stundenplan + Schulkalender (Mai 2026, alle Phasen deployed) + +Zwei groesse Feature-Strange, vollstaendig live auf Mac Mini: + +| Pfad | Beschreibung | +|------|--------------| +| `/stundenplan` (studio-v2) | Lehrer-UI mit 9 Tabs (Plan + 7 Stammdaten + Regeln), 15 Constraint-Editoren, Pin/Unpin im Wochengrid | +| `/schulkalender` (studio-v2) | Bundesland-Wizard, Monatsansicht mit Ferien (16 BL × 3 Jahre), Schul-Events, Schuljahres-Rollover, Eltern-Manager | +| `/eltern` (studio-v2) | Eltern-Sicht: Wochengrid des eigenen Kindes in Eltern-Sprache, Magic-Link-Login | +| `school-service` (Go, :8084) | Beide Backends — 30+ Tabellen, JWT-Auth (Dev-Bypass aktiv), Cron fuer Notifications | +| `timetable-solver-service` (Python+JVM, :8095) | Timefold-basierter Solver, 14 Constraints implementiert | + +**Wichtigste Memo-Dateien fuer Wiedereinstieg:** +- `~/.claude/projects/-Users-benjaminadmin-Projekte-breakpilot-lehrer/memory/session_summary_2026_05_22.md` — vollstaendiges Inventar +- `~/.claude/projects/-Users-benjaminadmin/memory/project_timetable_scheduler.md` — Stundenplan-Status +- `~/.claude/projects/-Users-benjaminadmin-Projekte-breakpilot-lehrer/memory/project_schulkalender.md` — Schulkalender-Status + +**Pitfalls (vermeidet diese):** +- Timefold Python-Package heisst `timefold` (NICHT `timefold-solver`), v1.24.0b0 +- Production-Auth + Matrix/Email-Services baut Kollege — Frontend-Hooks nutzen, kein eigener Service-Code +- JSX-Attribute mit deutschen Quotes `„X"` brechen, Loesung: `description={"..."}` Expression-Form +- LOC-Budget 500 pro File — bei specs mit shared Helpers arbeiten (`e2e/_helpers.ts`) + +**Test-Status (Stand 2026-05-22):** 89 Go + 21 Playwright im Schulkalender + 42 Playwright im Stundenplan = **152 grun** + +**Offen:** Seed-Daten fuer Demo-Schule, Vollschuljahr-ICS mit RRULE+EXDATE, Untis-Import (Phase 4 geparkt). + +--- + ## Wichtige Dateien (Referenz) | Datei | Beschreibung | diff --git a/docs-src/services/schulkalender/architecture.md b/docs-src/services/schulkalender/architecture.md new file mode 100644 index 0000000..6db5f37 --- /dev/null +++ b/docs-src/services/schulkalender/architecture.md @@ -0,0 +1,64 @@ +# Architektur + +## Datenmodell + +### Phase 9a — Kalender-Stammdaten + +| Tabelle | Inhalt | Owner | +|---------|--------|-------| +| `cal_public_event` | Ferien + Feiertage (region, type, name, start, end) | global (alle Bundeslaender) | +| `cal_school_config` | Bundesland-Auswahl + Schuljahr-Daten | 1 row per user_id | + +### Phase 9b — Schul-Events + +| Tabelle | Inhalt | Owner | +|---------|--------|-------| +| `cal_school_event` | Titel + Typ + Datum/Zeit + affected_class_ids + Notification-Flags | created_by_user_id | + +Event-Typen (CHECK constraint): `fortbildung`, `schulfeier`, `klassenfahrt`, `projekttag`, `eltern_info`, `andere`. + +### Phase 9c — Parent-Accounts + +| Tabelle | Inhalt | +|---------|--------| +| `parent_account` | Email + preferred_language, UNIQUE pro (Lehrer, Email) | +| `parent_child` | Vorname/Nachname + FK auf tt_class | +| `parent_magic_link` | Einmal-Token (SHA-256 in DB), expires_at 7 Tage | +| `parent_session` | Browser-Session-Token (SHA-256 in DB), expires_at 30 Tage | + +### Phase 9d — Notifications + +| Tabelle | Inhalt | +|---------|--------| +| `notification_log` | Idempotenz: UNIQUE(event_id, lead_days, audience, channel) | + +## Auth-Modell + +**Zwei voneinander unabhaengige Auth-Wege:** + +1. **Lehrer:** JWT in Authorization-Header (oder Dev-Bypass mit Default-User wenn `ENVIRONMENT != "production"`). Routen unter `/api/v1/school/...`. +2. **Eltern:** Session-Cookie `bp_parent_session` (HttpOnly, SameSite=Lax), gesetzt vom `/api/v1/parent/auth/redeem` Endpoint. ParentSessionMiddleware resolved Cookie → parent_account. + +Eltern sehen **nie** Daten anderer Eltern. Privacy-Check via `ChildBelongsToParent` in jedem GET, Plus Filterung der Lessons gegen tt_solution des einladenden Lehrers. + +## Bundesland-Wizard + +Erster Aufruf von `/schulkalender` → kein `cal_school_config` → `BundeslandWizard` UI → POST `/calendar/config` mit `{bundesland: "DE-NI"}` → MonthView lädt für die naechsten ~6 Wochen. + +## Schuljahres-Rollover + +POST `/calendar/school-year-rollover` (optional `{new_year_start, new_year_end}`): + +1. `DELETE FROM tt_class WHERE grade_level >= 13` (Abschlusskohorte) +2. `UPDATE tt_class SET grade_level = grade_level + 1` +3. `UPDATE cal_school_config SET school_year_start/end = ...` + +Alles in einer Transaction. Stundenplan-Lehrer-Faecher-Raum-Bestand bleibt unangetastet. + +## Auth + Messaging outsourced + +Production-Auth, Matrix-Bridge und Email-Gateway werden vom Kollegen gepflegt — siehe globale Memory `stundenplan_auth_and_messaging.md`. Wir definieren nur: + +- Dispatch-Payload-Struct (siehe [notifications.md](notifications.md)) +- Env-Vars `MATRIX_SERVICE_URL`, `EMAIL_SERVICE_URL` (leer = Stub-Mode) +- Endpoint-Vertrag (POST mit JSON-Body, HTTP 2xx = sent) diff --git a/docs-src/services/schulkalender/holidays.md b/docs-src/services/schulkalender/holidays.md new file mode 100644 index 0000000..1c7d1fa --- /dev/null +++ b/docs-src/services/schulkalender/holidays.md @@ -0,0 +1,71 @@ +# Ferien + Feiertage + +## Quelle + +[openholidaysapi.org](https://openholidaysapi.org) — EU-Initiative, MIT-Lizenz +fuer den API-Code, ODbL fuer die Daten. Liefert sowohl `PublicHolidays` als +auch `SchoolHolidays` je Bundesland mit ISO-Codes `DE-BW`, `DE-BY`, ... + +## Build-Time-Snapshot + +Statt zur Laufzeit zu pollen wird ein JSON-Snapshot committed: + +```bash +bash scripts/calendar-snapshot.sh 2026 2030 +``` + +Schreibt nach `school-service/internal/seed/calendar_holidays.json`. Das +Dockerfile kopiert die Datei ins Image; bei jedem Container-Start importiert +`CalendarService.SeedFromSnapshot()` die Eintraege idempotent (UNIQUE auf +region, event_type, name_de, start_date). + +**Stand 2026-05-22:** 854 Events fuer alle 16 Bundeslaender × 3 Schuljahre. + +## Aktualisierungs-Workflow + +1. Jaehrlich (z.B. im Mai vor neuem Schuljahr): + ```bash + bash scripts/calendar-snapshot.sh 2027 2031 + ``` +2. Diff im Git pruefen — sollte nur neue Eintraege haben, nicht alte ueberschreiben. +3. Commit + push + Container-Rebuild. +4. Beim ersten Boot werden neue Eintraege in `cal_public_event` eingefuegt; bestehende bleiben. + +## API + +``` +GET /api/v1/school/calendar/holidays?region=DE-NI&from=2026-08-01&to=2027-07-31 +``` + +Liefert Array sortiert nach `start_date`. Beispiel-Antwort: + +```json +[ + {"id":"…","region":"DE-NI","event_type":"school_holiday","name_de":"Sommerferien","start_date":"2026-07-02","end_date":"2026-08-12"}, + {"id":"…","region":"DE-NI","event_type":"public_holiday","name_de":"Tag der Deutschen Einheit","start_date":"2026-10-03","end_date":"2026-10-03"}, + ... +] +``` + +## Format-Mapping (Snapshot-Script) + +OpenHolidaysAPI gibt: +```json +{"id":"...","startDate":"2026-10-03","endDate":"2026-10-03","type":"Public", + "name":[{"language":"DE","text":"Tag der Deutschen Einheit"}]} +``` + +`scripts/calendar-snapshot.sh` normalisiert via jq: +```json +{"region":"DE-NI","event_type":"public_holiday","name_de":"Tag der Deutschen Einheit", + "name_en":null,"start_date":"2026-10-03","end_date":"2026-10-03"} +``` + +## Lizenz-Compliance + +- API-Code: MIT +- Daten: ODbL (Open Database License) + +Beides ist fuer kommerzielle Nutzung erlaubt. Die Quelle muss in einer +Lizenz-Aufstellung (SBOM) genannt werden — bereits in +`sbom/stundenplan/README.md` dokumentiert. diff --git a/docs-src/services/schulkalender/index.md b/docs-src/services/schulkalender/index.md new file mode 100644 index 0000000..7ddb030 --- /dev/null +++ b/docs-src/services/schulkalender/index.md @@ -0,0 +1,51 @@ +# Schulkalender + +Bundeslandweit kalibrierter Schulkalender mit Ferien, Feiertagen, Schul- +Events, Eltern-Sicht und mehrsprachigen Benachrichtigungen. + +## Auf einen Blick + +``` +studio-v2 /schulkalender → Lehrer-Sicht (CRUD Events, Eltern einladen, Rollover) +studio-v2 /eltern → Eltern-Sicht (Wochengrid des Kindes in eigener Sprache) + │ + │ HTTP /api/school/* und /api/parent/* (zwei separate Auth-Gruppen) + ▼ +school-service (Go, :8084) + ├── cal_public_event — Ferien/Feiertage-Snapshot (OpenHolidaysAPI) + ├── cal_school_config — Bundesland pro Rektor + ├── cal_school_event — Schulfeier, Fortbildung, Klassenfahrt etc. + ├── parent_account/_child/_magic_link/_session — Eltern-Auth + └── notification_log — Idempotenter Versand-Log + │ + ▼ POST DispatchPayload +Matrix-Bridge + Email-Gateway (vom Kollegen gepflegt, nicht in diesem Repo) +``` + +## Module + +| Bereich | Doku | +|---------|------| +| [Architektur](architecture.md) | DB-Modell, Auth-Ablauf, Phase-Reihenfolge | +| [Ferien-Snapshot](holidays.md) | OpenHolidaysAPI-Pipeline, jaehrliche Aktualisierung | +| [Eltern-Workflow](parent-flow.md) | Magic-Link, Cookie-Session, i18n-Fachnamen | +| [Notifications](notifications.md) | Cron, Templates, Dispatcher-Vertrag | + +## Phasen-Stand + +**Alle vier Phasen abgeschlossen (2026-05-22):** + +- 9a — Bundesland-Wizard + Monatsansicht +- 9b — Schul-Events + Schuljahres-Rollover +- 9c — Parent-Accounts + Magic-Link + Wochengrid in 8 Sprachen +- 9d — Notification-Cron + Templates + Status-Badges + +**Offen:** Vollschuljahr-ICS, Seed-Daten fuer Demo-Schule. + +## Test-Status + +| Suite | Tests | +|------|-------| +| Go (services + notifications) | 89 / 89 | +| Playwright Schulkalender | 16 / 16 | +| Playwright Eltern | 7 / 7 | diff --git a/docs-src/services/schulkalender/notifications.md b/docs-src/services/schulkalender/notifications.md new file mode 100644 index 0000000..ac0705f --- /dev/null +++ b/docs-src/services/schulkalender/notifications.md @@ -0,0 +1,96 @@ +# Notifications + +## Pipeline + +``` +06:00 Uhr (Berlin-Zeit, Container TZ=Europe/Berlin) + │ + ▼ +NotificationService.RunForDate(today) + │ + ▼ +dueEvents() findet cal_school_event mit + (start_date - today) ∈ notification_lead_days + │ + ▼ +Pro Event: fuer jede Audience (parents/students) und jeden Channel + (matrix für alle, email zusaetzlich nur fuer parents): + │ + ▼ +dispatchOne() + 1. Idempotenz-Check (UNIQUE notification_log) + 2. recipientsFor() — JOIN parent_account+parent_child fuer + betroffene Klassen, gibt Email-Liste + bevorzugte Sprache zurueck + 3. Render-Template (templates.go, 8 Sprachen) + 4. POST {MATRIX,EMAIL}_SERVICE_URL mit DispatchPayload + 5. notification_log writeLog (sent/failed/skipped) +``` + +## Cron-Mechanik + +`main.go` startet einen Goroutine-Ticker mit 1h-Intervall. Sobald `time.Now().Hour() == 6` wird `RunForDate` aufgerufen. Idempotent — die UNIQUE auf notification_log filtert Doppel-Calls am selben Tag. + +Bei Container-Restart vor 06:00 läuft trotzdem alles korrekt: der naechste 06-Tick fired bis spaetestens 06:59:59. Bei Restart nach 06:00: erste Notification erst am Folgetag (acceptable trade-off gegen einen 1-Min-Ticker). + +## Manueller Trigger + +```bash +# Heute jetzt scannen +curl -X POST http://localhost:8084/api/v1/school/calendar/notifications/run-now + +# Backfill (z.B. nach langem Container-Down) +curl -X POST 'http://localhost:8084/api/v1/school/calendar/notifications/run-now?date=2026-05-20' +``` + +Antwort: `{"date":"2026-05-22","sent":N,"failed":N,"skipped":N,"already_logged":N}`. + +## Template-Engine + +Datei: `school-service/internal/notifications/templates.go`. Schema: + +``` +templates[lang][event_type][audience][bucket] → {Subject, Body} +``` + +- `lang` ∈ de/en/tr/ar/uk/ru/pl/fr (Fallback `de`) +- `event_type` ∈ fortbildung/schulfeier/klassenfahrt/projekttag/eltern_info/andere (Fallback `andere`) +- `audience` ∈ parents/students (Fallback `parents`) +- `bucket` ∈ today/tomorrow/days (Fallback `days`) + +Placeholders: `{{title}}`, `{{date}}`, `{{date_pretty}}`, `{{class_name}}`, `{{class_suffix}}`, `{{teacher_name}}`, `{{lead}}`. + +Beispiel-Render (TR / schulfeier / parents / 1-Tag-Vorlauf): +``` +Subject: Yarın: Sommerfest (5a) +Body: Sayın veliler, yarın (15.06.2026) Sommerfest gerçekleşiyor (5a). +``` + +## DispatchPayload (Endpoint-Vertrag mit Matrix/Email Service) + +```json +{ + "channel": "matrix", + "recipient": "mama@example.de", + "language": "tr", + "subject": "Yarın: Sommerfest", + "body": "Sayın veliler, ...", + "event_id": "uuid-…", + "lead_days": 1 +} +``` + +Erwartete Antwort vom Upstream: HTTP 2xx = sent. 4xx/5xx = failed. Wir leiten **keine** Empfaenger-Identifier-Aufloesung weiter ans Upstream — die Matrix-Bridge mapt Email → Matrix-Handle in der eigenen Logik. + +Bei `MATRIX_SERVICE_URL` oder `EMAIL_SERVICE_URL` leer: status='skipped', kein Versandversuch. Erlaubt lokales Testen ohne Upstream. + +## Status-Anzeige im Lehrer-UI + +`DayDetail` mountet `NotificationStatus` fuer jedes Event mit `notify_parents` oder `notify_students`. Lädt `GET /api/v1/school/calendar/events/:id/notifications` und zeigt Badges: + +- ✓ gruen = sent +- ✗ rot = failed (Hover zeigt error_message) +- ⏱ amber = skipped (Upstream noch nicht konfiguriert) + +## Privacy + +`notification_log` ist nur über JOIN cal_school_event sichtbar — Lehrer sieht nur Logs seiner eigenen Events. Eltern haben gar keine UI fuer Logs. diff --git a/docs-src/services/schulkalender/parent-flow.md b/docs-src/services/schulkalender/parent-flow.md new file mode 100644 index 0000000..deaeaae --- /dev/null +++ b/docs-src/services/schulkalender/parent-flow.md @@ -0,0 +1,54 @@ +# Eltern-Workflow + +## Einladung (Lehrer) + +1. Lehrer offnet `/schulkalender`, scrollt zu `ParentManager`. +2. Klick "+ Eltern einladen" → Form mit Email, Vorname/Nachname Kind, Klasse, Sprache. +3. `POST /api/v1/school/calendar/parents/invite` legt parent_account (upsert), parent_child + parent_magic_link an, gibt Klartext-Token + voll qualifizierten Link zurueck. +4. Lehrer kopiert Link aus der UI und schickt ihn ueber Matrix oder Email (Versand-Automation kommt mit Phase 9d Notification-Pipeline). + +## Login (Eltern) + +1. Eltern klicken den Link `https://app/eltern/login?token=…`. +2. Browser laedt die Login-Page, sendet `POST /api/v1/parent/auth/redeem {token}`. +3. school-service validiert Token (Hash-Lookup + expires_at + used_at), markiert used_at, mintet Session-Token (32-Byte URL-safe Base64), setzt HttpOnly Cookie `bp_parent_session`. +4. Redirect auf `/eltern`. Folgende API-Calls senden Cookie automatisch. + +## Wochengrid + +`/eltern` ruft: + +- `GET /api/v1/parent/me` → Account + Kinder-Liste (Name, Klasse via JOIN tt_class) +- `GET /api/v1/parent/me/timetable?class_id=…` → letzte completed tt_solution der Klasse + +Filter laeuft strikt: ParentService prueft `ChildBelongsToParent(parent_id, class_id)` vor jeder Timetable-Query. + +## Fach-Uebersetzung + +`lib/calendar/subject-i18n.ts` hat 22 Standardfaecher in 8 Sprachen: + +```typescript +mathematik: { de: 'Mathematik', en: 'Mathematics', tr: 'Matematik', + ar: 'الرياضيات', uk: 'Математика', ru: 'Математика', + pl: 'Matematyka', fr: 'Mathématiques' } +``` + +`translateSubject(germanName, lang)`: + +1. Lowercase + trim → `key` +2. `SUBJECTS[key]` lookup +3. Wenn key nicht in Map: Original-Deutsch zurueck (z.B. "Imkern AG") +4. Wenn lang nicht in Sprachen: `de`-Fallback + +## Logout + +`POST /api/v1/parent/auth/logout` setzt Cookie auf max-age=-1. Session-Row bleibt in DB (laeuft selber ab nach 30 Tagen) — vereinfacht Tracking. + +## Was die Eltern NICHT sehen + +- Andere Eltern oder Kinder +- Stundenplan-Versionen die nicht "completed" sind +- Schul-Events mit `visible_to_parents=false` +- Lehrer-internes wie Stundentafel oder Lehrauftrag-Konfiguration + +Privacy-Garantien sind auf SQL-Ebene durchgesetzt (JOIN-Pfade + WHERE-Klauseln), nicht nur im Application-Layer. diff --git a/mkdocs.yml b/mkdocs.yml index d9e7930..463413d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,12 @@ nav: - Constraints: services/stundenplan/constraints.md - Solver-Tuning: services/stundenplan/solver-tuning.md - Export: services/stundenplan/export.md + - Schulkalender: + - Uebersicht: services/schulkalender/index.md + - Architektur: services/schulkalender/architecture.md + - Ferien-Snapshot: services/schulkalender/holidays.md + - Eltern-Workflow: services/schulkalender/parent-flow.md + - Notifications: services/schulkalender/notifications.md - Architektur: - Multi-Agent System: architecture/multi-agent.md - Zeugnis-System: architecture/zeugnis-system.md