Die GIS-Abteilung unserer Stadt benötigte eine Möglichkeit, damit Einwohner Schlaglöcher, Wasserlecks, Graffiti, illegale Müllablagerungen, Bauvorschriftenprobleme und ähnliche Anliegen melden können, und damit das Personal diese Tickets ohne ein Drittanbieter-311-Produkt bearbeiten kann. Wir haben es auf dem bereits betriebenen ArcGIS Enterprise aufgebaut und nun das gesamte System auf GitHub veröffentlicht, verallgemeinert, sodass jede Organisation es einrichten kann.
https://community.esri.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fgithub.com%2Fbrianmcleer%2Freport-a-concern
Dieser Beitrag ist eine technische Schritt-für-Schritt-Anleitung, wie es zusammengesetzt ist und warum einige Entscheidungen getroffen wurden. Das Repository enthält das vollständige Deployment-Handbuch.
Was im Paket enthalten ist
Zwei Experience Builder 1.21 Widgets (öffentlicher Einreichungsassistent und Mitarbeiter-Ticketmanager), ein Enterprise-Geodatabase-Schema mit Attributregeln, ein kleiner Flask-Proxy, der den öffentlichen Feature-Service voranstellt, sieben Python-Skripte, die im Windows Task Scheduler laufen, zugängliche HTML-E-Mail-Vorlagen, Task Scheduler Job-Definitionen und Dokumentation. Nichts im Code ist an unsere Organisation gebunden. Hostnamen, E-Mail-Adressen, Abteilungsnamen und Element-IDs stammen aus zwei git-ignorierten Konfigurationsdateien und den Widget-Einstellungsfenstern.
Das Datenmodell
Alles befindet sich in einer SQL Server Enterprise-Geodatabase. Das System der Aufzeichnung ist eine Punkt-Feature-Class namens Tickets mit einer GUID-Ticket-ID, einer menschenlesbaren Ticketnummer, Kategorie als Subtyp (13 Codes), einem Unterkategorie-Textfeld dessen codierter Wertbereich je nach Subtyp wechselt, Status, zugewiesener Abteilung, der Grenze in der der Punkt liegt, Kontaktfeldern des Einreichers und zwei Flags die von den Skripten abgefragt werden: notification_sent und survey_sent. Anhänge sind bei Tickets für Fotos aktiviert.
Daran angrenzend befinden sich verwandte Tabellen, verbunden durch Ticket-ID mit zusammengesetzten Beziehungsklassen, sodass das Löschen eines Tickets kaskadiert: Ticket_Comments (mit einem is_public-Flag und einem email_sent-Flag), Ticket_Photos_Meta und Survey_Responses. Drei Nachschlagetabellen steuern die Weiterleitung: Service_Boundaries (Polygone mit is_active-Flag), Category_Boundary_Lookup (welche Kategorien innerhalb welcher Grenze gültig sind plus eine Umleitungsnachricht für ungültige) und Ticket_Routing (Kategorie, optionale Unterkategorie, Grenz-ID, Standardabteilung, Abteilungs-E-Mail). Notification_Log ist eine nur anhängbare Audit-Tabelle, in die jedes Skript und der Proxy schreiben.
Tickets, Kommentare und Umfrageantworten sind versioniert und archiviert. Die Nachschlagetabellen und Notification_Log sind absichtlich nicht versioniert, was unten wichtig ist.
Einreichungsablauf von Anfang bis Ende
- Der Einwohner öffnet die öffentliche Experience Builder App und setzt einen Pin. Das Submit-Widget fragt clientseitig Service_Boundaries ab, um aktive Polygone zu finden die den Punkt enthalten, dann Category_Boundary_Lookup damit die Kategorienliste nur gültige Optionen anzeigt. Ein Wasserticket kann nicht innerhalb eines benachbarten Wasserbezirks eingereicht werden; der Einwohner sieht stattdessen die Kontaktinformationen dieses Bezirks statt einer Sackgasse.
- Das Widget sendet ein applyEdits an eine URL derselben Herkunft unter der App, nicht direkt an den Feature-Service. IIS URL Rewrite (auf Site-Ebene, sodass ein Experience Builder Neuveröffentlichen es nicht löscht) leitet diesen Pfad über Application Request Routing an einen Flask-Proxy auf localhost weiter. Eine zweite Regel auf Site-Ebene gibt bei direktem POST von außen gegen den FeatureServer 403 zurück.
- Der Proxy begrenzt die Rate pro Client-IP, lehnt mehr als ein Feature pro Anfrage ab, prüft die Geometrie gegen eine Begrenzungsbox, validiert Kategorie, Beschreibungslänge sowie E-Mail- und Telefonformat und leitet erst dann an den FeatureServer auf localhost weiter. Für Fotos liest er die ersten Bytes aus und akzeptiert nur echte JPEG-, PNG-, WebP- und HEIC-Inhalte unabhängig vom deklarierten Typ mit Größenbegrenzung. Jede akzeptierte oder abgelehnte Anfrage wird mit Client-IP in Notification_Log geschrieben – so haben wir eine Prüfspur ohne Dateilogging.
- Fünf Arcade-Attributregeln laufen beim Einfügen in die Geodatabase: eine Geofence-Beschränkung (blockiert ungültige oder außerhalb liegende Kategorien mit Nachricht aus Lookup), eine Routing-Berechnung die die Grenze findet, versucht Kategorie plus Unterkategorie plus Grenztreffer in Ticket_Routing zu finden und fällt sonst auf die Kategorie-Allgemein-Zeile zurück um die zugewiesene Abteilung zu setzen; eine Ticketnummernberechnung aus einer SQL-Sequenz beginnend bei 10000; sowie zwei Validierungsbeschränkungen für Pflichtfelder und Geschäftsregeln. Routing ist Daten-getrieben: das Hinzufügen einer Abteilung oder Ändern wer Wassertickets in einem Bezirk erhält ist eine Zeilenänderung.
- Alle fünf Minuten findet das Skript zur Benachrichtigung neuer Tickets Tickets mit notification_sent = 0, sendet dem Einwohner eine Bestätigungsmail mit Statuslink und der zuständigen Abteilung eine Arbeitsbenachrichtigung mit Deep Link zur Manager-App.
Mitarbeiterseite
Das Manager-Widget läuft in einer internen Experience Builder App hinter Portal-Anmeldung. Es liest die Tickets-Layer und verwandte Tabellen aus der Webkarte – keine zusätzlichen Tokens oder Service-URLs nötig. Mitarbeiter filtern nach Status-, Kategorie- und Abteilungs-Badges, öffnen ein Ticket, ändern den Status (Statusänderung erfordert Kommentar; Zurückdatierung eines gelösten Datums schreibt interne Auditnotiz), fügen öffentliche oder interne Kommentare hinzu, sehen Fotos in Lightbox und erhalten Umfrageantworten falls vorhanden. Öffentliche Kommentare lösen per Kommentar-Mailer-Skript E-Mails an den Einwohner aus. Es gibt einen Excel-Export mit Zusammenfassungstabelle und verwandten Datensätzen. Eine Hilfefunktion ist im Widget integriert nach dem Muster aller unserer Widgets.
Die Skripte und zwei Fehler beim ersten Mal
Sieben Skripte teilen ein gemeinsames Modul für Konfiguration, Logging, Fehlerwarnungen, E-Mail-Versand, Template-Rendering und Audit-Schreibvorgänge – so enthält jedes Skript nur seine eigene Logik: Benachrichtigung neuer Tickets; Benachrichtigung bei Neu-Zuweisung (erkennt Änderung von assigned_to und mailt neue Abteilung); Mailer für öffentliche Kommentare; Mailer für Umfrageeinladungen (bei Status Resolved oder Closed); Survey123-Antwortabruf (schreibt in Survey_Responses und mailt Bearbeiter wenn Rückruf gewünscht); wöchentlicher Bericht für Direktoren über überfällige Tickets nach Kategorie; monatlicher Bericht für alle Abteilungen. Jeder Fehler wird gesammelt und als einzelne Warnmail am Ende gesendet; Prozess beendet sich mit Code 1 damit Task Scheduler es anzeigt.
Zwei Lektionen sind im Code verankert. Erstens doppelte E-Mails: Der ursprüngliche Notifier versendete Mails innerhalb einer großen Batch-Bearbeitungssitzung und setzte das Flag am Ende. Wenn dieser Commit fehlschlug (z.B. wegen Versionskonflikt durch geöffnetes Ticket im Manager), waren alle Mails schon rausgegangen aber Flags zurückgesetzt – nächster Lauf schickte alle erneut. Jetzt wird das Flag pro Ticket in einer kurzen eigenen Bearbeitung vor dem Mailversand gesetzt. Scheitert Commit: keine Mail & sicherer nächster Versuch; gelingt Commit & Versand scheitert: wird protokolliert aber nie erneut gesendet.
Zweitens die Audit-Tabelle: Notification_Log war ursprünglich versioniert; Skripte schrieben per Cursor was langsam war & Compress voraussetzte. Jetzt ist sie von Versionierung abgemeldet; jeder Schreiber fügt per direktem SQL mit expliziter OBJECTID MAX+1 inkl. kurzem Retry ein – kein Schreiben hängt vom Geodatabase-Zähler ab & keine Kollision zwischen Skripten. Damit zusammenhängend: Der Umfragemailer las früher Basistabelle & sah Auflösung erst nach nächstem Compress – Umfragen gingen verspätet raus. Jetzt lesen alle Skripte über versionierte Feature-Class.
Umfrageschleife
Wenn ein Ticket gelöst wird erhält der Einwohner einen Link zu einem Survey123 Formular mit Ticket-ID in der URL. Das Pull-Skript liest neue Antworten von ArcGIS Online aus schreibt sie in Survey_Responses der Geodatabase & mailt bei Rückrufwunsch den Bearbeiter (aus Editor Tracking mit Abteilungs-Fallback). Das Manager-Widget zeigt Bewertung & Kommentare zum Ticket an.
Sicherheit & Datenschutz
Anonyme Nutzer haben Leserechte auf der öffentlichen Ansicht & können nur über den Proxy erstellen. Site-Level IIS Header setzen HSTS, nosniff & Frame Options. Der Feature-Service beschränkt Upload-Dateitypen & -größen. Der Proxy vertraut nie dem deklarierten Content-Type. Aufbewahrung erfolgt über Zusammenfassungstabellen mit Zählungen nach Monat/Kategorie/Abteilung ohne Ticket-IDs oder Freitext – alte Tickets mit persönlichen Daten können planmäßig gelöscht werden während Statistiken erhalten bleiben.
Inbetriebnahme
Das Runbook in docs/deployment.md ist die geordnete Liste: Schema mit arcpy-Skript erstellen (erst Trockenlauf), drei Services veröffentlichen (öffentlich schreiben/lesen & Mitarbeiter), Attributregeln hinzufügen, zwei Widgets in your-extensions legen & Apps bauen, IIS-Regeln auf Site-Ebene setzen, Proxy in geklonter ArcGIS Pro Python Umgebung starten, config.py & rac_secrets.py ausfüllen sowie Task Scheduler Jobs importieren. Alle Skripte haben Testmodus an – keine echte Mail bis Umschalten. Troubleshooting-Doku zeigt Symptome & Lösungen beim Serverwechsel: IIS HTML 500 heißt Proxy hört nicht; 404 heißt ARR fehlt; CORS Fehler heißt Widget URL nicht gleiche Herkunft wie Seite; Windows Server 2025 Task Scheduler Fehler heißt Job zeigt auf Basis ArcGIS Pro Python statt Klon.
Widget-Zips sind jedem GitHub Release angehängt. Issues & Pull Requests sind im Repository willkommen.