CivicLoop expanded from 311 into County OS: one platform, every county agency a module on the same engine. This doc covers the surfaces added on top of the 311 core and how to operate them.
Each agency is a module at /[locale]/<module>, gated by requireStaff(locale, "supervisor"), reading via the service-role admin client, county-scoped.
| Module | Route | Core entity (full CRUD) |
|---|---|---|
| Procurement | /procurement |
vendors (+ solicitations, bids, contracts; AI bid scoring) |
| DPIE Permitting | /dpie |
permit_applications |
| Economic Development | /econdev |
businesses (+ opportunities) |
| Public Works (DPWT) | /dpwt |
fleet_vehicles |
| Human Resources | /hr |
job_postings |
| Parks & Recreation | /parks |
park_assets |
| Public Safety / Courts | /safety |
safety_cases |
| Public Schools | /schools |
schools |
| Redevelopment | /redevelopment |
redevelopment_sites |
| Predictive Data Feeds | /feeds |
data_feeds (+ feed_readings) |
| County Assistant | /assistant |
county_kb (knowledge base) |
Each module carries a schema-driven Manage section (components/county-os/record-editor.tsx) with New / Edit / Delete on its core entity. Fields are declared once in src/lib/county-os/schemas.ts (real dropdowns for every enum, money/date/number/tags controls, EN/ES labels). Server actions: src/lib/county-os/crud-actions.ts (allowlisted by table, staff-gated, county-scoped). Nothing is read-only.
Every module + the public portals export as a branded, downloadable, printable report via components/county-os/report-bar.tsx (Print report button on screen; logo + county + timestamp header on print). Global print CSS in globals.css; staff chrome is .no-print. Browser "Save as PDF" = downloadable.
/feeds)Generic time-series store. Register a feed (kind + metric + unit + threshold), upload a CSV (timestamp,value), and the platform fits a least-squares trend, projects 7/30 days, and estimates days-to-threshold. Forecasted breaches raise feed_forecast automation flags.
POST /api/feeds/ingest with header x-feeds-secret: $FEEDS_INGEST_SECRET (503 until the secret is set). Body { "feed": "<slug>", "readings": [{capturedAt, value}] } or { "feed": "<slug>", "csv": "..." }.POST /api/automations (header x-automation-secret: $AUTOMATION_SECRET, 503 until set). Jobs: overdue permits, closing opportunities, expiring contracts, data janitor (auto-close past-deadline opps, flag duplicate vendors), and feed forecasts./procurement): Claude Haiku scores bids vs budget/peers/diversity and auto-shortlists the top 2 for a human to approve.automation_flags / automation_runs; resolved from the dashboard panel.sentry.*.config.ts, src/instrumentation.ts, src/app/global-error.tsx). No-op until SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN is set; no source-map upload (no build dependency on an auth token).POST /api/cron/synthetic (header x-automation-secret) pings the public surfaces + Open311 API, records up/down + latency to synthetic_checks, and raises a synthetic_down flag on a 5xx. Surfaced on the dashboard Platform health panel.node scripts/prod-smoke.mjs./assistant)Named assistant ("Ada", renameable in county_ai_config). Answers from county_kb first and falls back to general guidance (prefixed "General guidance:") when a question isn't covered. The onboarding questionnaire is seeded as KB rows the county edits to fill in real answers - fully managed with the same RecordEditor. Action: src/lib/assistant/ask.ts (Claude Haiku). Needs ANTHROPIC_API_KEY.
components/dashboard/welcome-tour.tsx runs a one-time guided tour on the dashboard (re-openable via "Take a tour"); remembered in localStorage.
node scripts/ui-sweep.mjs (env DEMO_APP_URL). Logs in per role, visits every page, clicks every safe button, fails on any uncaught JS error. This is the regression test for client-reference / server-component crashes.node scripts/make-walkthrough-video.mjs (Gemini TTS + Playwright capture + static ffmpeg). Renders detailed + 90s reel, EN + ES, to public/walkthrough-*.mp4 + .vtt. Hosted at /[locale]/walkthrough. Env: BASE, STAFF_EMAIL, STAFF_PW, GEMINI_API_KEY (falls back to the shared sibling-repo key).node scripts/record-demo.mjs.| Var | Purpose | Behavior if unset |
|---|---|---|
ANTHROPIC_API_KEY |
AI co-pilot, bid scoring, assistant | AI features fail safe |
AUTOMATION_SECRET |
gate /api/automations + /api/cron/synthetic |
503 |
FEEDS_INGEST_SECRET |
gate /api/feeds/ingest |
503 |
SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN |
error tracking | no-op |
SYNTHETIC_BASE_URL |
target for synthetic monitor | defaults to prod URL |
Manual CLI (no auto-deploy): cd web && netlify deploy --build --prod --site=e1d84f28-2e39-4f34-98ab-5749dd77e286. Always pass --site.