An evidence-led examination of what a fixed City of Phoenix missing-persons public-data snapshot can—and cannot—tell us.
Live site: phx-data.com
The original application made ten live API requests on every visit and kept the entire interface behind one loading state. Those requests repeatedly scanned the same PostgreSQL data to recalculate results that never changed.
This repository is the redesigned canonical monorepo. PostgreSQL now produces a validated, versioned snapshot at build time; Next.js prerenders the complete report from that snapshot. SQL remains reviewable and reproducible without putting a database cold start in front of every visitor.
| Before | Now |
|---|---|
| Create React App client shell | Next.js App Router with static prerendering |
| 10 blocking browser requests | No browser request for initial report content |
| Repeated live aggregate scans | Materialized analysis view + exported snapshot |
| Every chart presented equally | Three guided findings and one focused explorer |
| “Still missing” presented as status | Precise “no located date” field-state language |
| Raw demographic counts | Within-group outcome comparison with denominator warning |
| Default broken CRA test | Unit, component, API, desktop, and mobile coverage |
| Two small disconnected repos | One typed workspace for web and data |
apps/web/ Next.js report, static API routes, and components
packages/data/
generated/dashboard.json Versioned public data contract
scripts/ PostgreSQL export command
sql/ Normalization and analytical queries
src/ Types, validation, and selectors
scripts/ One-time legacy snapshot capture
tests/e2e/ Desktop and mobile browser journeys
docs/ Architecture and decision records
See the architecture document for the request path, data pipeline, tradeoffs, and performance budget.
Requires Node.js 22 or newer.
npm install
npm run devOpen http://localhost:3000.
No database or environment variable is required to run, test, or deploy the site. The checked-in snapshot is the application’s public data contract.
npm run check # lint, typecheck, coverage, production build
npm run test:e2e # Chromium desktop + mobile journeysCoverage thresholds are enforced at 80% for statements, lines, and functions. The current selector and component suite exceeds those thresholds.
Regeneration is deliberately separate from the web request path.
cp .env.example .env
DATABASE_URL=postgresql://... npm run data:export -- --refresh
npm run check--refresh rebuilds the normalized materialized view before exporting. Without
it, the command uses the existing view. The exporter reconciles the outcome
bucket total against the KPI record total before replacing the JSON file.
The scripts/capture-legacy-snapshot.mjs command exists only to preserve the
last results produced by the retired Express deployment during migration.
GET /api/snapshot— the complete, immutable data contractGET /api/health— deployment and data-mode status
Both routes are prerendered. The report imports the data package directly rather than making an HTTP request to its own API.
- Rows are treated as report records, not deduplicated people.
- “No located date” means exactly that; it is not an independently verified present-day case status.
- Missing dates and negative date intervals stay in the denominator as unavailable / invalid.
- Demographic categories reproduce normalized source fields. They are not necessarily self-identified.
- Without population denominators, the data cannot establish whether a Phoenix demographic group is more likely to be reported missing.
- This is a fixed snapshot covering January 2016 through July 2025. It is not a live public-safety service.
The source dataset is published through Phoenix Open Data. This independent project is not affiliated with the City of Phoenix or a law enforcement agency.
