Digest-E-Mails & Kadenz
Stadtkontakte waehlen, wie oft sie einen Compliance-Digest wollen -- taeglich, woechentlich oder monatlich. Ein stuendlicher Cron geht jeden city_contacts-Eintrag durch, prueft, ob das Kadenzfenster abgelaufen ist, und baut (falls ja) den Digest und sendet ihn.
Kadenzoptionen
digest_frequency | E-Mail kommt | Am besten fuer |
|---|---|---|
daily | Morgens nach Schluss des vorherigen Tages (Ortszeit der Zustaendigkeit) | Operativ orientierte Stadtkontakte |
weekly | Montagmorgen, fuer die vergangene Mo-So | Leitende Compliance-Officer |
monthly | 1. des Monats, fuer den vergangenen Kalendermonat | Genehmigungspruefer, Auditoren |
Kadenz ist pro Kontakt, nicht pro Zustaendigkeit. Gaengiges Muster: leitender Compliance-Officer bekommt woechentlich, ein Ops-Analyst taeglich, ein Auditor monatlich.
Der stuendliche Cron
/api/cron/compliance-digest laeuft stuendlich (0 * * * * in vercel.json). Er:
- Geht
city_contacts-Zeilen mitportal_access = truedurch. - Prueft fuer jede Zeile das Kadenzfenster:
- Daily:
last_digest_sent_at < (heute 00:00 Ortszeit der Zustaendigkeit) - Weekly:
last_digest_sent_at < (Montag 00:00 dieser Woche Ortszeit)und es ist Montag - Monthly:
last_digest_sent_at < (1. des Monats 00:00)und es ist der 1.
- Daily:
- Ruft fuer jeden faelligen Kontakt
buildComplianceReport({jurisdictionId, period, date})zur Berechnung der Payload auf. - Ruft
buildDigestEmail(report, contact.locale)zum Formatieren von Betreff + HTML + Text auf. - Uebergibt die E-Mail an den pluggable
complianceDigestSender-- in Produktion an die bestehende transaktionale E-Mail-Infra verdrahtet. - Aktualisiert
last_digest_sent_atauf der Kontaktzeile. - Persistiert die Payload in
city_compliance_reportsmit der Audit-Zeile.
Wenn ein Schritt eine Exception wirft, wird der Kontakt geloggt, aber der Cron faehrt fort -- ein fehlgeschlagener Kontakt blockiert den Rest des Batches nicht.
Der pluggable Sender
src/lib/compliance/digest-email.ts exportiert setComplianceDigestSender(fn). Der Standard ist ein No-Op-Sender, der in die Konsole loggt; Produktion setzt ihn beim Start auf die Operator-App-E-Mail-Infra. Diese Indirektion erlaubt Tests, die Digest-Pipeline laufen zu lassen, ohne E-Mails zu senden.
E-Mail-Form
| Abschnitt | Inhalt |
|---|---|
| Betreff | Levy Compliance Digest -- Boulder, CO -- Daily Report for 2026-05-17 |
| Kopfzeile | Betreibername, Zustaendigkeitsname, Periode |
| Compliance-Scoreboard | Pass/Fail pro Bedingung mit aktuellem Wert vs Schwellwert |
| Heutige Momentaufnahme | Fahrten, eingesetzte Fahrzeuge, geoeffnete/geschlossene Beschwerden |
| Enforcement-Zusammenfassung | Gesendete Geschwindigkeitsbefehle, ausgegebene Sperren, Stunden in Slow-Zonen |
| Offene Punkte | Fehlgeschlagene Bedingungen, offene Beschwerden ueber SLA |
| Fussnote | Link zum Stadt-Portal, Support-Kontakt |
Bei Monats-Digests haengen wir ein einseitiges PDF an. Taeglich und woechentlich sind nur HTML + Text.
HTML-Escaping
buildDigestEmail HTML-escaped jeden vom Nutzer kontrollierten String (Zustaendigkeitsname, Beschwerdetitel, Fahrzeugnummern). Getestet in digest-email.test.ts als sicher gegen eingeschleuste <script>-Tags in Policy-Namen -- selbst wenn eine Stadt eine Policy mit seltsamen Zeichen im Namen veroeffentlicht, rendert der Digest korrekt ohne Layout-Bruch.
Lokalisierung
Die E-Mail wird in city_contacts.locale gerendert (Standard en). Launch unterstuetzt en und es. Zusaetzliche Locales fallen mit einem Hinweis in der Fussnote auf Englisch zurueck ("Dieser Bericht ist nur auf Englisch verfuegbar").
Die Pass/Fail-Labels und Bedingungsnamen des Compliance-Scoreboards verwenden die Locale des Kontakts; Betreibername und Stadtname werden unveraendert durchgereicht.
last_digest_sent_at und Idempotenz
Wenn der Cron wiederholt (z.B. ein Deploy mitten im Zyklus, ein Vercel-Timeout), ist last_digest_sent_at der Schutz gegen Doppelsendungen. Der Cron aktualisiert ihn innerhalb derselben Transaktion wie den city_compliance_reports-Insert, sodass entweder beides oder keines passiert.
Wenn Sie jemals einen erneuten Versand erzwingen muessen (ein Kontakt sagt, er habe den Montags-Digest nicht erhalten), setzen Sie last_digest_sent_at ueber die "Digest erneut senden"-Aktion des Dashboards auf einen frueheren Wert. Der naechste Cron-Lauf erkennt den Kontakt als faellig und liefert ihn erneut aus.
Digests vorübergehend deaktivieren
Um Digests fuer einen Kontakt zu pausieren, ohne ihn zu entfernen:
- Nur Portal-Zugang:
portal_access = trueunddigest_frequency = nullsetzen. Der Kontakt kann sich weiterhin am Portal anmelden, erhaelt aber keine E-Mail. - Beides pausiert:
portal_access = falsesetzen. Magic-Link-Auth schlaegt fehl; keine Digests gesendet.
Persistierte Berichte
Jeder Digest-Lauf schreibt eine Zeile in city_compliance_reports:
| Spalte | Inhalt |
|---|---|
id | UUID |
jurisdiction_id | FK |
report_type | daily / weekly / monthly |
period_start / period_end | Abgedecktes Zeitfenster |
computed_at | Wann der Bericht gebaut wurde |
payload | Voller JSON-Snapshot |
pdf_url | Fuer Monatsberichte |
delivered_to_contact_id | Die city_contacts-Zeile, die ihn erhielt |
Diese Tabelle ist der Audit-Trail. Wenn ein Stadt-Auditor fragt "haben Sie am 14. April einen Digest gesendet?", ist die Zeile in city_compliance_reports die Antwort. Berichte werden unbegrenzt aufbewahrt.
Haeufige Fragen
"Mein Kontakt sagt, er habe die E-Mail nicht bekommen."
Spam pruefen, dann city_compliance_reports auf die erwartete Zeile. Wenn die Zeile existiert und delivered_to_contact_id passt, wurde die E-Mail versandt -- beim E-Mail-Anbieter pruefen. Wenn die Zeile fehlt, hat der Cron den Kontakt entweder uebersprungen (Kadenzfenster noch nicht abgelaufen) oder einen Fehler bekommen (Sentry).
"Kann derselbe Kontakt sowohl taeglich als auch monatlich erhalten?"
Nein -- digest_frequency ist ein einzelner Wert. Erstellen Sie zwei Kontaktzeilen fuer dieselbe E-Mail, wenn Sie beide Kadenzen wollen.
"Kann ich jemanden im Digest CCen?"
Nicht ueber die Kontaktzeile. Erstellen Sie eine separate city_contacts-Zeile fuer den CC-Empfaenger. Jeder erhaelt eigenen Magic-Link-Zugang und eigenes last_digest_sent_at.
Was kommt als naechstes?
- Stadt-Portal & Magic-Link-Auth -- wo Kontakte sich anmelden, um dieselben Daten live zu sehen.
- Genehmigungs-Konditionsberichte -- was das Scoreboard im Digest misst.