De GIS-afdeling van onze stad had een manier nodig voor bewoners om meldingen te doen van gaten in de weg, waterlekken, graffiti, illegale stortingen, bouwkundige problemen en soortgelijke zorgen, en voor medewerkers om die tickets te verwerken zonder een derde partij 311-product. We hebben het gebouwd op de ArcGIS Enterprise die we al gebruiken, en we hebben nu het hele systeem gepubliceerd op GitHub, gegeneraliseerd zodat elke organisatie het kan opzetten.
https://community.esri.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fgithub.com%2Fbrianmcleer%2Freport-a-concern
Deze post is een technische walkthrough van hoe het in elkaar zit en waarom sommige keuzes zijn gemaakt. De repository bevat het volledige implementatiehandboek.
Wat zit er in de doos
Twee Experience Builder 1.21-widgets (publieke indienwizard en medewerkers ticketbeheerder), een enterprise geodatabase-schema met attribuutregels, een kleine Flask-proxy die de publieke feature service afhandelt, zeven Python-scripts die draaien via Windows Taakplanner, toegankelijke HTML-e-mailsjablonen, Taakplanner-taakdefinities en documentatie. Niets in de code is gekoppeld aan onze organisatie. Hostnamen, e-mailadressen, afdelingsnamen en item-id's komen uit twee git genegeerde configuratiebestanden en de widget-instellingenpanelen.
Het datamodel
Alles bevindt zich in één SQL Server enterprise geodatabase. Het systeem van registratie is een punt-featureklasse, Tickets, met een GUID ticket-id, een leesbaar ticketnummer, categorie als subtype (13 codes), een subcategorie tekstveld waarvan het gecodeerde waardedomein per subtype verandert, status, toegewezen afdeling, de grens waar het punt binnen viel, contactvelden van de indiener en twee vlaggen die door scripts worden gecontroleerd: notification_sent en survey_sent. Bijlagen zijn ingeschakeld op Tickets voor foto's.
Eromheen zitten gerelateerde tabellen gekoppeld via ticket-id met samengestelde relatieklassen zodat het verwijderen van een ticket doorwerkt: Ticket_Comments (met een is_public-vlag en een email_sent-vlag), Ticket_Photos_Meta en Survey_Responses. Drie lookup-tabellen sturen de routering aan: Service_Boundaries (polygonen met een is_active-vlag), Category_Boundary_Lookup (welke categorieën geldig zijn binnen welke grens plus een doorverwijsbericht voor niet-geldige) en Ticket_Routing (categorie, optionele subcategorie, grens-id, standaardafdeling, afdeling e-mail). Notification_Log is een append-only audit-tabel waar elk script en de proxy naar schrijven.
Tickets, opmerkingen en enquête-antwoorden zijn versiebeheer en gearchiveerd. De lookup-tabellen en Notification_Log zijn dat bewust niet, wat hieronder belangrijk is.
Indieningsproces van begin tot eind
- De bewoner opent de publieke Experience Builder-app en plaatst een pin. De submit-widget vraagt client-side Service_Boundaries op om te vinden welke actieve polygonen het punt bevatten, daarna Category_Boundary_Lookup zodat alleen geldige categorieën daar getoond worden. Een waterticket kan niet worden ingediend binnen een aangrenzend waterdistrict; de bewoner ziet dan contactgegevens van dat district in plaats van een doodlopende weg.
- De widget verstuurt een applyEdits naar een URL met dezelfde oorsprong onder de app, niet direct naar de feature service. IIS URL Rewrite (site-niveau, zodat herpublicatie van Experience Builder dit niet overschrijft) stuurt dat pad door naar een Flask-proxy op localhost via Application Request Routing. Een tweede site-niveau regel geeft 403 terug bij directe POSTs naar FeatureServer van buitenaf.
- De proxy beperkt het aantal verzoeken per client IP, weigert meer dan één feature per verzoek, controleert geometrie tegen een begrenzingsvak, valideert categorie, beschrijvingslengte, e-mail- en telefoonvorm en stuurt pas dan door naar FeatureServer op localhost. Voor foto's leest hij de eerste bytes en accepteert alleen echte JPEG-, PNG-, WebP- en HEIC-bestanden ongeacht het opgegeven type met een groottebeperking. Elk geaccepteerd of geweigerd verzoek wordt geschreven naar Notification_Log met client IP voor audit zonder bestandslogging.
- Vijf Arcade attribuutregels draaien bij invoeging in de geodatabase: een geofence-beperking (blokkeert buiten grenzen of ongeldige categorieën met bericht uit lookup), routeringsberekening die grens bepaalt, probeert match categorie plus subcategorie plus grens in Ticket_Routing en valt terug op algemene categorierij om toegewezen afdeling te zetten; ticketnummerberekening uit SQL-sequentie vanaf 10000; twee validatiebeperkingen voor verplichte velden en bedrijfsregels. Routering is data, geen code: toevoegen of wijzigen van afdeling of wie watertickets krijgt is rijbewerking.
- Elke vijf minuten vindt het nieuwe-ticket-notificatiescript tickets met notification_sent = 0, mailt bevestiging met statuslink naar bewoner en werkmelding met deeplink naar manager-app naar toegewezen afdeling.
Medewerkerszijde
De manager-widget draait in interne Experience Builder-app achter Portal-aanmelding. Leest Tickets-laag en gerelateerde tabellen uit webmap zonder extra tokens of service-URL's. Medewerkers filteren op status, categorie en afdelingsbadges, openen ticket, wijzigen status (statuswijziging vereist commentaar; terugdateren opgelost-datum schrijft interne auditnotitie), voegen publieke of interne opmerkingen toe, bekijken foto's in lightbox en zien enquêteantwoord als aanwezig. Publieke opmerkingen triggeren e-mail naar bewoner via comments mailer-script. Er is Excel-export met samenvattingsblad en gerelateerde records. Helpgids ingebouwd volgens patroon dat we voor al onze widgets gebruiken.
De scripts en twee fouten die we aanvankelijk maakten
Zeven scripts delen één gemeenschappelijke module voor configuratie, logging, foutmeldingen, e-mail, sjabloonrendering en audit-schrijfacties zodat elk script alleen eigen logica heeft: nieuwe-ticket-notificator; herindelingsnotificator (detecteert wijziging assigned_to en mailt nieuwe afdeling); public comments mailer; survey invitation mailer (start bij Resolved of Closed); Survey123 response pull (schrijft in Survey_Responses en mailt resolver als bewoner vervolg vroeg); weekdagdirecteursrapport over achterstallige tickets per categorie; maandrapport alle afdelingen. Elke fout wordt verzameld en als één alertmail verzonden bij afsluiten; proces sluit af met code 1 zodat Taakplanner dit toont.
Twee lessen zitten in de code verwerkt. Ten eerste dubbele e-mails. Oorspronkelijke notifier stuurde mails binnen batchwijde bewerking en committe vlag aan einde. Als commit faalde (bijvoorbeeld versieconflict omdat medewerker ticket open had), waren alle mails al verzonden maar vlag teruggedraaid; volgende run stuurde alles opnieuw. Nu wordt vlag per ticket gecommit in korte bewerking vóór mailen. Bij commit-fout geen mail; veilige retry volgende run. Bij commit-succes maar mail-fout wordt gelogd maar nooit opnieuw verzonden.
Ten tweede audit-tabel. Notification_Log was eerst versiebeheerder; scripts schreven via cursors wat traag was en afhankelijk van Compress. Nu niet meer geregistreerd voor versiebeheer; elke schrijver doet directe SQL-insert met expliciete OBJECTID als MAX+1 met korte retry zodat geen schrijfactie afhankelijk is van geodatabase rij-id teller of botsingen tussen scripts ontstaan. Gerelateerd: survey mailer las eerst basistabel waardoor resolutie pas na volgende Compress zichtbaar was; enquêtes gingen daardoor één cyclus te laat uit. Nu leest elk script via versiebeheerde featureklasse.
Enquêtecyclus
Als ticket is opgelost krijgt bewoner link naar Survey123-formulier met ticket-id in URL. Pull-script leest nieuwe antwoorden uit ArcGIS Online, schrijft ze in Survey_Responses in geodatabase en mailt medewerker die ticket oploste als bewoner terugbelverzoek deed (oplossing bepaald via editor tracking met fallback afdeling). Manager-widget toont beoordeling en opmerkingen bij ticket.
Beveiliging en privacy
Anonieme gebruikers hebben leesrechten op publieke weergave en kunnen alleen via proxy aanmaken. Site-niveau IIS-headers zetten HSTS, nosniff en frame-opties. Feature service beperkt upload bestandstypen en grootte. Proxy vertrouwt nooit op opgegeven content-type. Retentie wordt geregeld via samenvattingstabellen die tellingen per maand, categorie en afdeling bijhouden zonder ticket-ids of vrije tekst zodat oude tickets met persoonlijke info periodiek verwijderd kunnen worden terwijl statistieken behouden blijven.
Opzetten
Het runbook in docs/deployment.md is de volgorde: bouw schema met arcpy-script (eerst dry run), publiceer drie services (publiek schrijven, publiek lezen, medewerkers), voeg attribuutregels toe, plaats twee widgets in your-extensions en bouw apps, zet IIS-regels op site-niveau neer, start proxy op in gekloonde ArcGIS Pro Python-omgeving, vul config.py en rac_secrets.py in, importeer Taakplanner-taken. Elk script heeft testmodus aan dus er wordt geen echte bewoner gemaild tot je dit uitschakelt. Troubleshooting-doc bevat symptoom-naar-oplossing tabel gemaakt tijdens migratie tussen servers: IIS HTML 500 betekent proxy luistert niet; 404 betekent ARR niet geïnstalleerd; CORS-fout betekent widget-URL niet zelfde oorsprong als pagina; Windows Server 2025 Taakplanner-fout betekent taak wijst naar basis ArcGIS Pro Python i.p.v kloon.
Widget-zips zijn toegevoegd aan elke GitHub-release. Issues en pull requests zijn welkom in repository.