Skip to content

Repository files navigation

DispatchLoop logo

DispatchLoop — closing the loop on 311

A triage, dispatch, and transparency layer over Chicago 311 service requests (dataset v6vf-nfxy). DispatchLoop ranks open requests by real urgency, assigns them to field crews, and keeps residents honestly informed — especially when plans change. Built against the Feb 2026 OIG audit of the 311 process; the full design is in LOOP-311-DRD.md.

Live demo

Open DispatchLoop

Important

The first visit or sign-in can take around a minute. The demo API may need to wake from its hosted idle state, and DispatchLoop continuously processes a high-volume inflow of public Chicago 311 tickets. Once the API is awake and the latest data is synchronized, navigation is substantially faster.

Demo accounts

Use the shared password demo1234 with any account below.

Experience Email Department
Office staff console distr1@dispatchloop.demo 311 City Services
Mobile crew view crewmar1@dispatchloop.demo Streets & Sanitation
Mobile crew view crew.cdot@dispatchloop.demo Transportation
Mobile crew view crew.water@dispatchloop.demo Water Management
Mobile crew view crew.buildings@dispatchloop.demo Buildings
Mobile crew view crew.animals@dispatchloop.demo Animal Care
Mobile crew view crew.aviation@dispatchloop.demo Aviation
Mobile crew view crew.bacp@dispatchloop.demo Business Affairs
Mobile crew view crew.health@dispatchloop.demo Public Health
Mobile crew view crew.finance@dispatchloop.demo Finance
Mobile crew view crew.external@dispatchloop.demo Outside Agencies

Demo data is shared. Another visitor may update the same assignments while you are exploring the application.

Features

  • Priority-based staff console: ranks open requests with a visible severity rationale, manual override, filters, task history, and full request detail.
  • Department workload view: expandable department-wise task lists sourced directly from Chicago's owner_department data—no duplicate department assignment inside DispatchLoop.
  • Crew dispatch: manual assignment, reassignment, emergency preemption, and department-aware auto-assignment that fills crews toward five active tasks.
  • Mobile crew workflow: expandable task cards, map links, location sharing, compact alerts, completed-work history, and English/Spanish controls.
  • Voice status updates: tap-to-start/tap-to-stop speech interpretation bound to the expanded task, with safe transitions from assigned through completion.
  • Resident tracking: public request lookup, operational progress, honest ETA, approved updates, and opt-in notification records without exposing internal notes.
  • AI-assisted communication: Claude can interpret natural crew language and draft status-aware resident replies. Deterministic keyword/template fallbacks keep core workflows available when AI is not configured.
  • Live Chicago 311 synchronization: incremental SODA ingestion with overlap, cursor tracking, Firestore coordination locks, pruning, and no writes back to City of Chicago systems.

Prototype stance: a read-only analytical layer over real city data with a simulated operational layer on top. Nothing is ever written back to Chicago's systems — "synchronizing" always means refreshing our own copy of the public feed.

Architecture

                     Chicago 311 SODA (read-only)
                                |
                                v
                        FastAPI backend
          +---------------------+---------------------+
          |                     |                     |
   Deterministic            Claude AI           Firebase Admin
   priority / clustering    - resident text            |
   ETA / state machine      - Ask Loop                 v
                            - crew commands        Firestore
                                                   ├── serviceRequests
                                                   │     └── events
                                                   ├── users
                                                   │     └── notifications
                                                   ├── syncRuns
                                                   └── typeBaselines
   Firebase Auth
        |
        v
     Next.js
     ├── STAFF_OFFICE → /console, /ask
     ├── CREW         → /field
     └── public       → /track/[sr]
  • backend/ — Python (FastAPI + Firebase Admin + Cloud Firestore). All data access, domain logic (severity, clustering, honest ETA, ingest, categorization, notifications, Claude integration) and all authorization. Every protected route verifies the caller's Firebase ID token with the Admin SDK and reads their role from users/{uid}.
  • src/ — Next.js 16 App Router. A thin view layer. Firebase Auth signs users in the browser; the ID token is mirrored into an httpOnly cookie so server components can forward it to FastAPI. The frontend's role check is for routing and UI only — it is never the security boundary.

The SVG logo lives at public/screen.svg and renders via src/components/loop-mark.tsx — in the nav on every page, on the login screen, and on the landing page.

Surfaces

Route Who What
/console Office staff Severity-ranked queue with category tags, duplicate coupling, manual/automatic dispatch, emergency preemption, reassignment, priority overrides, deferred work, department workloads, and resident-message approval
/field On-site crews Ordered queue with category/state filters, in-app alerts, live location + nearby requests, natural-language commands; Accept → En route → On site → Work performed / Blocked / Deferred → Complete
/track/[sr] Residents (public) Plain-language status, honest ETA with its basis, approved updates only, deferral and referral explanations, opt-in notifications
/ask Staff Natural-language questions → SoQL → live portal, with the query and row count always shown

Setup

1. Firebase project

Enable Authentication → Email/Password and Cloud Firestore, then create your staff and crew accounts in the Auth console. This project stores no passwords — Firebase owns credentials.

2. Backend

cd backend
py -3.12 -m pip install -r requirements.txt
cp .env.example .env        # fill in FIREBASE_PROJECT_ID / CLIENT_EMAIL / PRIVATE_KEY

Attach a Firestore profile to each Auth user (this is what grants a role):

py -3.12 -m scripts.seed_demo_users \
  --email distr@dispatchloop.demo --name "Trent Dispatch Unit" \
  --role STAFF_OFFICE --department "311 City Services"

py -3.12 -m scripts.seed_demo_users \
  --email crewmar1@dispatchloop.demo --name "Marcus Crew Unit" \
  --role CREW --department "Streets and Sanitation"

py -3.12 -m scripts.seed_demo_users --list     # verify
# or seed the bundled department-spanning demo set (Auth accounts must exist first)
py -3.12 -m scripts.seed_demo_users --from-json scripts/demo_users_all_departments.json

Load the 311 data and start the API:

py -3.12 -m scripts.import_json_to_firestore   # bundled 500-row snapshot
# or, live:  py -3.12 -m scripts.import_soda_to_firestore
py -3.12 -m uvicorn app.main:app --reload --port 8000

3. Frontend

npm install
cp .env.example .env.local  # fill in the NEXT_PUBLIC_FIREBASE_* values
npm run dev                 # http://localhost:3000

npm run backend, npm run backend:import, npm run backend:users and npm run reset:ops are shortcuts for the commands above.

Sign in at /login; you'll land on /console (STAFF_OFFICE) or /field (CREW) based on your Firestore profile. /track needs no account.

While a staff session is open the app reconciles with the city portal every 60 s (the exact $query is in backend/app/domain/soda.py), re-scores the queue, refreshes duplicate proposals and categorizes new requests. If the portal is unreachable it falls back to the bundled snapshot in data/.

Configuration

Frontend (.env.local) — BACKEND_URL, NEXT_PUBLIC_BACKEND_URL, and the six NEXT_PUBLIC_FIREBASE_* values.

Backend (backend/.env) — FIREBASE_PROJECT_ID / FIREBASE_CLIENT_EMAIL / FIREBASE_PRIVATE_KEY (or FIREBASE_SERVICE_ACCOUNT_JSON, or GOOGLE_APPLICATION_CREDENTIALS), optional ANTHROPIC_API_KEY, CLAUDE_MODEL, SODA_APP_TOKEN, FRONTEND_ORIGIN, and the SODA_SYNC_* scheduler settings. CRON_SECRET is only needed if an external scheduler is used instead of the backend scheduler.

Never put FIREBASE_PRIVATE_KEY or FIREBASE_CLIENT_EMAIL in a NEXT_PUBLIC_* variable. Service-account JSON files are gitignored.

Firestore data model

Collection Document Holds
users/{uid} Firebase Auth UID role (STAFF_OFFICE/CREW), name, email, department, active. Subcollection notifications is the crew inbox.
serviceRequests/{srNumber} Chicago sr_number chicago (City mirror, incl. location GeoPoint and raw), dispatch (our operational state), resident (message + contact), cluster (coupling proposal). Subcollection events is the audit trail.
syncRuns/{autoId} auto Reconciliation history: status, counts, timestamps, error.
typeBaselines/{id} srType__ward Empirical close-time distributions behind the honest ETA.

Using the sr_number as the document ID is what makes every import idempotent. docs/firestore-migration-map.md records how each former SQLite table maps here.

Two status axes, never conflated

Chicago 311 status: Open        (the City's, read-only)
DispatchLoop status: EN_ROUTE   (ours, operational)

A crew moving a request to EN_ROUTE does not change anything in the City's system, and the UI shows both.

State machine (validated server-side on every transition):

UNASSIGNED → ASSIGNED → ACCEPTED → EN_ROUTE → ON_SITE → WORK_PERFORMED → COMPLETED
                                                 ├→ BLOCKED  → ON_SITE
                                                 └→ DEFERRED → ASSIGNED (office)

Tests

cd backend && py -3.12 -m pytest tests/ -q

The Admin SDK is mocked, so the suite needs no credentials or emulator. It covers token verification, role enforcement, inactive-user rejection, transition legality, cross-crew denial, import idempotency, the "sync never clobbers dispatch state" guarantee, GeoPoint handling, and the resident-privacy strip.

Demo prep

npm run reset:ops              # clear operational state, keep city data + users
# or: cd backend && py -3.12 -m scripts.reset_ops_firestore --dry-run

Deploy

Vercel hosts the Next.js layer; the FastAPI backend needs a persistent Python host (Fly.io, Railway, Render, a VM). Firestore removes the old requirement for a persistent SQLite filesystem.

Vercel FastAPI host
NEXT_PUBLIC_BACKEND_URL, BACKEND_URL FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY
NEXT_PUBLIC_FIREBASE_* (6 values) ANTHROPIC_API_KEY, CLAUDE_MODEL, SODA_APP_TOKEN
FRONTEND_ORIGIN
SODA_SYNC_ENABLED, SODA_SYNC_INTERVAL_SECONDS, SODA_SYNC_PAGE_SIZE, SODA_SYNC_OVERLAP_SECONDS, SODA_SYNC_LOCK_TTL_SECONDS

The FastAPI process owns the incremental SODA scheduler. Keep SODA_SYNC_ENABLED=true on an always-on backend to run the default 120-second loop. The Firestore coordination lock prevents overlapping sync runs across backend instances. Free hosts that sleep cannot run an in-process scheduler while suspended; the next backend start performs a fresh incremental sync.

Deploy firestore.rules with the Firebase CLI. FastAPI is the mutation boundary — the rules deny all client writes and scope reads to what each role is entitled to.

Design notes

  • Authorization is server-side, always. The browser's role cookie only drives routing; FastAPI re-derives the role from Firestore on every call, so a tampered cookie buys nothing.
  • AI proposes, code decides. Claude parses crew commands into structured intent and drafts resident text, but never writes to Firestore. Every transition is validated against the state machine and the caller's assignment before anything is persisted.
  • priority = w1·hazard + w2·overdue + w3·cluster + w4·context — see backend/app/domain/severity.py. The overdue signal compares days-open to the empirical median close time for that type, which no city tool computes today. A manual override is shown as an override, never as derived.
  • One event, two audiences. Events carry internalNote and publicText. Free-text (AI-drafted) resident messages require office approval; structured deferral and referral text is deterministic and auto-approved. /track strips internal notes server-side.
  • Every Claude-derived value is labeled with its source (claude vs keyword/template fallback) — no AI output is presented as ground truth.

About

DispatchLoop

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages