# 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.