Policy-Aufnahme von Staedten
Staedte veroeffentlichen ihre Policy ueber die MDS 2.0 Policy-API. Levy fragt diesen Feed jede Minute ab, validiert ihn, vergleicht ihn mit dem letzten bekannten Stand, materialisiert die resultierenden Geofences und plant deren Aktivierung. Der gesamte Ablauf ist idempotent -- ihn zweimal mit derselben Feed-Nutzlast auszufuehren ist ein No-Op.
Ein Feed pro Zustaendigkeit
Jede mds_jurisdictions-Zeile zeigt auf genau eine Policy-Feed-URL. Wenn eine Stadt mehrere Feeds veroeffentlicht (z.B. einen fuer Scooter, einen fuer E-Bikes), erstellen Sie eine Zustaendigkeit pro Feed. Der Slug ist die Unterscheidung.
Die Pipeline im Ueberblick
+-------------------+ 60s +----------------------+ +-------------------+
| mds-policy-poll |-------->| fetchPolicyFeed |--->| sha256 Hash |
| Cron | | | +---------+---------+
+-------------------+ +----------------------+ |
| unveraendert?
v Ende
+---------+---------+
| parsePolicyFeed |
| (Zod-Validierung) |
+---------+---------+
|
v
+---------+---------+
| ingestPolicies |
| upsert policies |
| replace rules |
| materialize zones |
| mirror into zones |
+---------+---------+
|
v
+---------+---------+
| mds_policy_audit |
| run_id + diff |
+-------------------+
Schritt 1 -- Abrufen + Hashen
fetchPolicyFeed(jurisdiction) sendet ein GET an die konfigurierte policy_feed_url, mit optionalem policy_feed_auth_token als Bearer-Header. Der vollstaendige Antwort-Body wird mit sha256 gehasht.
Wenn der neue Hash dem letzten bekannten Hash auf der Zustaendigkeitszeile entspricht, kurzschliesst der Ingester und beendet sich. Kein Parsen, kein Diff, kein Audit-Eintrag. Das haelt die Steady-State-Polling-Kosten niedrig und vermeidet Spam im Audit-Log mit No-Op-Laeufen.
Wenn der Hash abweicht (oder kein letzter bekannter Hash existiert), faehrt die Verarbeitung fort.
Schritt 2 -- Parsen + Validieren
parsePolicyFeed(raw) laeuft das Roh-JSON durch Zod-Schemas, modelliert nach der MDS 2.0 Policy-Spezifikation:
| Feld | Schema |
|---|---|
policies[].policy_id | UUID |
policies[].name | String |
policies[].start_date | RFC3339 |
policies[].end_date | RFC3339, nullable |
policies[].published_date | RFC3339 |
policies[].rules[] | Array von Regeln, jeweils mit rule_type, geographies, rule_units, vehicle_types[], days[], start_time, end_time |
geographies[].geography_id | UUID |
geographies[].geography_json | GeoJSON-Feature |
Ein zweiter Durchgang (parseGeographyFeed) loest jede aus rules[].geographies[] referenzierte geography_id gegen den /geographies-Feed der Stadt auf. Nicht aufloesbare Referenzen werden als Warnungen im Audit-Lauf protokolliert, blockieren aber die Aufnahme nicht (die betroffene Regel wird uebersprungen).
Validierungsfehler werden in mds_policy_audit mit dem Zod-Fehlerpfad erfasst. Der vorherige Policy-Zustand bleibt in Kraft -- wir wenden einen fehlerhaften Feed nie teilweise an.
Schritt 3 -- Upsert + Ersetzen
ingestPolicies() laeuft in einer einzigen Transaktion:
- Policies:
INSERT ... ON CONFLICT (jurisdiction_id, policy_external_id) DO UPDATEaufmds_policies. Dieraw_json-Spalte speichert das geparste Payload fuer den Diff-Viewer. - Rules: Delete-then-Insert gegen
mds_policy_rulesfuer die betroffenen Policies (Regeln sind zu verflochten fuer sauberes Upsert). - Geofences:
policy_geofences-Zeilen aus den von jeder Regel referenzierten Geografien materialisieren. Eine Zeile pro(rule_id, geography_id). Dasgeojson-Feld speichert das aufgeloeste Feature; die PostGIS-geometry-Spalte ist derzeit null und wird vom In-JS-pointInPolygon-Checker gefuellt (siehe Prioritaet gestapelter Geofences und die Implementierungsnotizen). - Spiegeln in
zones: Fuer Rueckwaertskompatibilitaet mit der bestehenden Zonen-Durchsetzungs-Engine schreibt jeder Policy-Geofence zusaetzlich eine Zeile in die Legacy-zones-Tabelle, markiert mitsource = 'city',source_jurisdiction_idundsource_policy_rule_id. Ein eindeutiger Teilindex aufsource_policy_rule_idmacht den Upsert idempotent.
raw_json und feed_hash werden am Ende der Transaktion auf der Zustaendigkeitszeile aktualisiert -- erst nachdem jeder Downstream-Schreibvorgang erfolgreich war.
Schritt 4 -- Audit + Diff
Jeder Aufnahmevorgang schreibt eine Zeile in mds_policy_audit:
| Spalte | Inhalt |
|---|---|
id | UUID fuer den Lauf |
jurisdiction_id | FK |
applied_at | Zeitstempel |
feed_hash_before | sha256 des letzten bekannten Payloads |
feed_hash_after | sha256 des neuen Payloads |
diff | JSON von {added: [...], removed: [...], modified: [...]} |
errors | Zod-Fehler, nicht aufgeloeste Geografien usw. |
status | success / partial / failed |
Das Diff wird auf Policy-Ebene berechnet -- eine Policy mit einer neuen Regel erscheint in modified mit den spezifischen Regel-Deltas eingebettet. Der Viewer im Betreiber-Dashboard rendert dies als Seite-an-Seite-JSON.
Aktivierungs-Cron
Ein separater Cron, mds-policy-activate, laeuft jede Minute und flippt Policy-Statusmaschinen:
| Statusuebergang | Trigger |
|---|---|
pending -> active | start_date <= now() AND status = 'pending' |
active -> expired | end_date IS NOT NULL AND end_date <= now() AND status = 'active' |
pending -> superseded | Eine neuere Policy mit gleicher external_id und ueberlappenden Regeln verdraengt sie |
Bei pending -> active ruft der Aktivierungs-Cron den Enforcement-Service auf, um die OEM-spezifischen Geschwindigkeits-/Sperrbefehle an alle Fahrzeuge derzeit innerhalb der Regelgeometrie zu senden. Siehe Echtzeit-Geschwindigkeitsdurchsetzung.
Der Cron ueberspringt Aktivierungsereignisse aelter als 30 Sekunden -- wenn der Cron sich verspaetet hat und ein start_date jetzt 5 Minuten in der Vergangenheit liegt, wenden wir die Regel trotzdem an, aber loggen eine late_activation-Warnung. Enforcement-Ereignisse werden nie rueckdatiert.
Der Diff-Viewer
Oeffnen Sie /dashboard/compliance/{jurisdiction-id}/policies/{policy-id}/diff zur Ansicht des letzten Audit-Eintrags, der eine Policy beruehrt hat. Die Seite rendert:
- Header: Feed-Hash vorher und nachher, applied_at, Status
- Linker Bereich: Roh-JSON aus der vorherigen Aufnahme
- Rechter Bereich: Roh-JSON aus dieser Aufnahme
- Inline-Diff-Hervorhebung auf Feldebene
Sie koennen historische Audit-Eintraege ueber das Audit-Log auf der Zustaendigkeitsseite durchblaettern. Jeder Eintrag ist per run_id permalinkfaehig.
Das Audit-Log
Liegt unter /dashboard/compliance/{jurisdiction-id} im Audit-Tab. Eine Zeile pro Aufnahmelauf. Filterbar nach Status und Datumsbereich. Das Erweitern einer Zeile zeigt das Roh-diff-JSON und etwaige Fehler.
Nuetzliche Filter:
- Status = failed -- zeigt die Feed-Payloads, die wir nicht parsen konnten. Meist hat eine Stadt eine fehlerhafte Policy gepusht und wir behielten den vorherigen Zustand.
- Status = partial -- der Feed wurde geparst, aber mindestens eine Geografie-Referenz konnte nicht aufgeloest werden. Die betroffene Regel ist in
errorsgelistet. - Datumsbereich -- fuer Quartalsbewertungen, wenn eine Stadt fragt "wann ist diese Policy bei Ihnen in Kraft getreten?".
Konfliktloesung beim Aufnehmen
Der Ingester loest beim Aufnehmen keine Konflikte zwischen Betreiberzonen und staedtischer Policy auf -- er materialisiert nur die staedtischen Regeln in ihrer Prioritaetsstufe. Konflikterkennung passiert beim Lesen, sichtbar gemacht als:
- Konflikt-Banner auf dem Compliance-Index -- zaehlt Betreiberzonen, die derzeit von einer staedtischen Regel ueberdeckt werden.
- Operator-Konflikte-API unter
GET /api/admin/compliance/conflicts-- JSON fuer jede ueberdeckte Zone mit der ueberlagernden Regel-ID.
Diese Trennung bedeutet, dass eine Stadt eine Policy veroeffentlichen kann, die Ihre Betreiberzonen ueberlappt, ohne etwas zu brechen -- Ihre Zonen existieren weiterhin, sie sind nur niedriger eingestuft. Wenn die Stadt die Policy spaeter zurueckzieht, kommen Ihre Zonen automatisch wieder an die Spitze der Prioritaetsreihe.
Siehe Prioritaet gestapelter Geofences fuer die Leiter.
Soft-Loeschen einer Zustaendigkeit
Wenn ein Betreiber eine Zustaendigkeit entfernt (Genehmigung abgelaufen, Marktaustritt), wird die Zeile soft-geloescht. Wir halten historische Daten 30 Tage vor und geben dann HTTP 410 auf den oeffentlichen MDS-Endpunkten zurueck. Policy-Aufnahme stoppt beim Soft-Loeschen; das Audit-Log bleibt unbefristet erhalten fuer Betreiber-eigene Akten.
Haeufige Fallstricke
- Policy-Feed der Stadt liefert 404 -- haeufigste Ursache: die URL war provisorisch und aenderte sich beim Stadt-Go-Live. Vom Terminal aufrufen und bestaetigen. Das Audit-Log erfasst 4xx- und 5xx-Antworten.
- Feed validiert, aber keine
policy_geofences-- jede Regel referenzierte eine Geografie, die wir nicht aufloesen konnten. Pruefen, dass die Stadt Policy-Feed und Geografie-Feed veroeffentlicht. - Dieselbe Policy taucht in jedem Diff auf -- die Stadt regeneriert ihren Feed bei jedem Poll (andere
published_dateoder Whitespace-Unterschiede im JSON). Der sha256-Kurzschluss feuert nur bei bytegleichen Payloads; bitten Sie die Stadt, ihre Serialisierung zu stabilisieren. - Aktivierung passiert, aber kein Fahrzeug wird durchgesetzt -- Fahrzeuge ausserhalb der Geometrie oder mit veraltetem GPS (>5 min). Siehe Echtzeit-Geschwindigkeitsdurchsetzung.
Was kommt als naechstes?
- Prioritaet gestapelter Geofences -- die Prioritaetsleiter.
- Echtzeit-Geschwindigkeitsdurchsetzung -- was bei Regelaktivierung passiert.
- Fehlerbehebung -- wenn Polls oder Aktivierungen nicht feuern.