- 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) <noreply@anthropic.com>
3.4 KiB
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
# 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 (Fallbackde)event_type∈ fortbildung/schulfeier/klassenfahrt/projekttag/eltern_info/andere (Fallbackandere)audience∈ parents/students (Fallbackparents)bucket∈ today/tomorrow/days (Fallbackdays)
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)
{
"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.