Genehmigungs-Konditionsberichte
Die meisten Stadtgenehmigungen enthalten durchsetzbare Bedingungen -- Flotten-Cap, Equity-Zonen-Einsatz-Prozentsatz, Beschwerde-SLA, Tagesende-Trip-Bericht, Parkstellplatzauslastung. Levy speichert jede als Zeile in permit_conditions, wertet sie planmaessig aus und rendert die Pass/Fail-Tendenz sowohl im Betreiber-Dashboard als auch im Stadt-Portal.
Die fuenf Bedingungstypen
condition_type | Was geprueft wird | Kadenz |
|---|---|---|
fleet_cap | COUNT(aktive Fahrzeuge in Zustaendigkeit) <= Cap | Taeglich |
equity_zone_pct | % der eingesetzten Flotte in Equity-Geografien >= Schwellwert | Stuendlich |
complaint_sla | responded_at jeder Beschwerde innerhalb N Stunden nach created_at | Taeglich |
trip_report_eod | Eine Trip-CSV wird taeglich um 23:59 Ortszeit der Zustaendigkeit erstellt | Taeglich |
corral_utilization | Stuendliche Momentaufnahme der Parkstellplatz-Belegung | Stuendlich |
Jede Zeile in permit_conditions traegt die jurisdiction_id, den condition_type und eine config-JSONB, die je Typ variiert.
Bedingung konfigurieren
Im Betreiber-Dashboard unter /dashboard/compliance/{jurisdiction-id} -> Genehmigungsbedingungen-Tab auf Bedingung hinzufuegen klicken.
fleet_cap
{
"cap": 250,
"vehicle_types": ["scooter"],
"exclude_statuses": ["maintenance", "removed"]
}
Der Evaluator zaehlt Fahrzeuge, bei denen:
subaccount_idzum Subaccount der Zustaendigkeit passtlast_lat/last_lnginnerhalbmds_jurisdictions.geometryliegt (oder NULL mitlast_seenin den letzten 24h -- grosszuegig fuer Fahrzeuge, die gerade ihr GPS verloren haben)status NOT IN config.exclude_statusesmodel.vehicle_type IN config.vehicle_types(falls gesetzt)
Pass = Anzahl <= Cap. Fail = Anzahl > Cap. Das Dashboard-Scoreboard zeigt die aktuelle Anzahl und das Cap nebeneinander.
equity_zone_pct
{
"threshold_pct": 20,
"equity_geography_ids": ["geo-1", "geo-2"],
"min_vehicles_required": 5
}
Pass = eingesetzt-in-Equity / total-eingesetzt >= threshold_pct / 100. Der min_vehicles_required ist ein Mindestwert -- falls weniger Fahrzeuge ueberhaupt eingesetzt sind, wird die Bedingung fuer die Stunde als n/a markiert statt fehlgeschlagen (duenne Frueh-Morgen-Einsaetze).
Equity-Geografien stammen entweder aus dem Policy-Feed der Stadt (Regeln mit rule_type = 'equity_zone') oder einer manuell konfigurierten Liste (wenn die Stadt sie nicht veroeffentlicht -- haeufig bei kleineren Staedten). Die manuelle Liste lebt in derselben permit_conditions.config.
complaint_sla
{
"max_response_hours": 24,
"complaint_sources": ["311", "in-app", "email"]
}
Der Evaluator joinst complaints, um Zeilen zu finden, bei denen responded_at IS NULL AND created_at < now() - interval '<hours>' (offen und ueberfaellig) oder responded_at - created_at > interval '<hours>' (geschlossen aber zu spaet). Pass = keine spaeten Beschwerden im Fenster.
trip_report_eod
{
"format": "csv",
"delivery": "email",
"recipient_email": "permits@city.gov"
}
Ein geplanter Bericht laeuft um 23:59 Ortszeit der Zustaendigkeit und emittiert eine Trip-CSV fuer den Tag. Pass = der Bericht wurde erfolgreich zugestellt. Fail = E-Mail abgewiesen oder die Berichtszeile materialisierte sich nicht.
corral_utilization
{
"min_utilization_pct": 10,
"max_utilization_pct": 90,
"exclude_corral_ids": []
}
Stuendliche Momentaufnahme der Belegung jedes Parkstellplatzes. Pass = alle Parkstellplaetze zwischen konfiguriertem Boden und Decke. Fail = mindestens einer ausserhalb des Bands. Die Bedingung wird von Staedten verwendet, die sicherstellen wollen, dass Parken tatsaechlich genutzt wird (niedriges Ende) und dass Betreiber nicht ueberkonzentrieren (hohes Ende).
Das Scoreboard
Betreiber sehen das Scoreboard unter /dashboard/compliance/{jurisdiction-id}. Staedte sehen es im Stadt-Portal unter /city/{slug}. Die Form ist gleich:
| Bedingung | Aktueller Wert | Schwellwert | Status (heute) | 30-Tage-Pass-Rate |
|---|---|---|---|---|
| Flotten-Cap | 247 Fahrzeuge | 250 | Pass | 100% |
| Equity-Zone % | 18% | >= 20% | Fail | 88% |
| Beschwerde-SLA | 2 offen ueber 24h | 0 | Fail | 92% |
| Trip-Bericht EOD | Zugestellt | n/a | Pass | 100% |
| Parkstellplatz-Auslastung | 6 von 6 im Band | Alle im Band | Pass | 95% |
Fehlgeschlagene Bedingungen sind rot hervorgehoben. Die 30-Tage-Pass-Rate wird aus der city_compliance_reports-Historie berechnet.
Die Reporter-Bibliothek
src/lib/compliance/permit-reporter.ts exponiert Per-Bedingungs-Evaluatoren. Jeder nimmt die Zustaendigkeit + die zugehoerige permit_conditions.config und gibt zurueck:
type ConditionResult = {
condition_id: string;
passed: boolean;
current_value: number | string;
threshold: number | string;
measured_at: string; // RFC3339
details: Record<string, unknown>;
};
Der aggregierte Bericht zur Digest-Zeit:
const report = await buildComplianceReport({
jurisdictionId,
period: 'daily', // oder 'weekly' / 'monthly'
date: '2026-05-18',
});
// report.conditions: ConditionResult[]
// report.summary.passed: number
// report.summary.failed: number
// report.trips: { count, total_distance_km, ... }
// report.complaints: { open, closed, late, ... }
// report.enforcement_events: { speed_limits_applied, locks_issued, ... }
Diese Form ist es, was die Digest-E-Mail rendert, was die Compliance-Report-API liefert und was an city_compliance_reports.payload persistiert wird.
Die Compliance-Report-API
Stadtkontakte holen formelle Berichte unter:
GET /api/city/{slug}/compliance-report?period=monthly&date=2026-05
Gibt dieselbe JSON-Form wie der Digest zurueck, plus einen pdf_url wenn period=monthly (wir rendern ein einseitiges PDF fuer Monatsberichte -- nuetzlich fuer Genehmigungsverlaengerungen).
Jeder abgerufene Bericht wird in city_compliance_reports persistiert -- der Audit-Trail jedes Berichts, den die Stadt tatsaechlich abgerufen hat. Wenn ein Stadt-Auditor fragt "haben Sie einen Bericht fuer Januar gesendet?", ist die Zeile in city_compliance_reports die Antwort.
Wenn Bedingungen fehlschlagen
Eine fehlgeschlagene Bedingung pausiert Ihre Flotte nicht automatisch. Sie wird sichtbar gemacht als:
- Eine rote Zeile im Scoreboard
- Eine Zeile im heutigen Digest
- Ein Sentry-Breadcrumb markiert mit
compliance.condition.failed
Was Sie damit tun, haengt von der Bedingung ab. Bei fleet_cap-Ueberschreitung weniger Fahrzeuge einsetzen. Bei equity_zone_pct umverteilen. Bei complaint_sla auf offene Beschwerden reagieren. Das System trifft keine operativen Entscheidungen fuer Sie.
Die Ausnahme ist fleet_cap, wo das Dashboard einen "Neue Fahrtstarts in Zustaendigkeit pausieren"-Schalter anbietet. Das ist ein Soft-Constraint -- bestehende Fahrten laufen weiter, keine neuen Fahrten starten in der Zustaendigkeit, bis der Cap wieder unter Schwellwert liegt. Der Schalter wird vom Betreiber gesteuert; wir aktivieren ihn nicht automatisch, weil Fehlalarme schlimmer waeren als ein voruebergehender fleet_cap-Fehler.
Benutzerdefinierte Bedingungen
Wenn Ihre Genehmigung eine Bedingung enthaelt, die zu keinem der fuenf eingebauten Typen passt (z.B. "muss 90% mehrsprachige Beschilderung erreichen"), fuegen Sie eine Zeile mit condition_type = 'custom' und einem manuellen passed-Feld hinzu. Der Evaluator gibt zurueck, was Sie setzen; Staedte sehen es weiterhin im Scoreboard. Das ist die Notfalloption fuer Genehmigungen mit einer kuriosen Klausel.
Was kommt als naechstes?
- Digest-E-Mails und Kadenz -- wann diese Ergebnisse im Postfach der Stadt landen.
- Stadt-Portal & Magic-Link-Auth -- wo der Stadtkontakt das Scoreboard ansieht.