advanced
Policy
Aufnahme
Diff

Policy-Aufnahme von Staedten

Wie Levy den veroeffentlichten Policy-Feed jeder Stadt abruft, mit Zod validiert, bei unveraendertem sha256 abkuerzt, Geofences materialisiert und Diff + Audit + Aktivierungsablauf bereitstellt.

Levy Fleets TeamMay 18, 202616 min read

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:

FeldSchema
policies[].policy_idUUID
policies[].nameString
policies[].start_dateRFC3339
policies[].end_dateRFC3339, nullable
policies[].published_dateRFC3339
policies[].rules[]Array von Regeln, jeweils mit rule_type, geographies, rule_units, vehicle_types[], days[], start_time, end_time
geographies[].geography_idUUID
geographies[].geography_jsonGeoJSON-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:

  1. Policies: INSERT ... ON CONFLICT (jurisdiction_id, policy_external_id) DO UPDATE auf mds_policies. Die raw_json-Spalte speichert das geparste Payload fuer den Diff-Viewer.
  2. Rules: Delete-then-Insert gegen mds_policy_rules fuer die betroffenen Policies (Regeln sind zu verflochten fuer sauberes Upsert).
  3. Geofences: policy_geofences-Zeilen aus den von jeder Regel referenzierten Geografien materialisieren. Eine Zeile pro (rule_id, geography_id). Das geojson-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).
  4. Spiegeln in zones: Fuer Rueckwaertskompatibilitaet mit der bestehenden Zonen-Durchsetzungs-Engine schreibt jeder Policy-Geofence zusaetzlich eine Zeile in die Legacy-zones-Tabelle, markiert mit source = 'city', source_jurisdiction_id und source_policy_rule_id. Ein eindeutiger Teilindex auf source_policy_rule_id macht 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:

SpalteInhalt
idUUID fuer den Lauf
jurisdiction_idFK
applied_atZeitstempel
feed_hash_beforesha256 des letzten bekannten Payloads
feed_hash_aftersha256 des neuen Payloads
diffJSON von {added: [...], removed: [...], modified: [...]}
errorsZod-Fehler, nicht aufgeloeste Geografien usw.
statussuccess / 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:

StatusuebergangTrigger
pending -> activestart_date <= now() AND status = 'pending'
active -> expiredend_date IS NOT NULL AND end_date <= now() AND status = 'active'
pending -> supersededEine 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 errors gelistet.
  • 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_date oder 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?