尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证An interactive civic intelligence platform visualizing Seattle neighborhood conditions, service requests, and trends using public city data.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗An interactive civic intelligence dashboard for exploring what Seattle residents report to the City: where requests are concentrated, which neighborhoods report the most, what kinds of issues dominate, and how volume changes over time. Every number comes live from the City of Seattle's open data.
This is an exercise in agentic development using Claude Code.
Map of Seattle. Three backgrounds you can switch between:
Clustered markers on top show the newest 2,000 matching requests. Click a cluster to zoom in, or a marker to see its details. The legend states each layer's scope.
Filters: date range (presets or custom), request type, current status, neighborhood (Community Reporting Area) and council district. Filters live in the URL, so any view can be shared. Every filter updates the map, metrics, charts and list together.
Summary metrics: total requests, daily average, most reported type, and the share with usable coordinates.
Period-over-period change: the total and every neighborhood/district bar show the change versus the equally long period just before, with the same filters (e.g. "▼ −2.0% vs Aug 21 – Sep 19"). When the previous count is under 20, the absolute change is shown instead of a volatile percentage. Changes use neutral arrows, not red/green, because more reports isn't necessarily worse.
Trends: requests per day, week (Mon–Sun) or month, chosen from the range length. Days with no requests are zero-filled, and buckets cut short by the range are drawn dashed and labelled.
Is the City responding equally?: compares how quickly tracked requests get a "Closed" update across census tracts grouped by the City's Racial and Social Equity Index. Each group is adjusted for its mix of request types and shown with a 95% margin of error. It also breaks results down by request type and lists the neighborhoods furthest from expected.
Response times: for the five request types the City tracks (abandoned vehicles, graffiti, illegal dumping, public litter cans, streetlights), the share that got a "Closed" status update within 7 and 30 days and the median days. Measured on a cohort old enough to judge, so recent requests can't skew it.
Breakdowns: request types, current status, and an area comparison by neighborhood or council district. Neighborhoods can be compared by raw count or per 1,000 residents (2020 Census). Areas with fewer than 5,000 residents are listed separately rather than ranked, because their rates aren't comparable. Each bar is a keyboard-accessible button that toggles that filter.
The count / per-capita choice drives the map shading too.
Recent requests: newest first and paged. Selecting one flies the map to it and opens its detail card. Requests without coordinates are listed and flagged, never placed on the map.
Loading, empty, partial and error states throughout. If a refresh fails, the last good results stay visible with a retry button.
For the story behind the build (data findings, scope decisions and the bugs verification caught), see the case study.
Requires Node.js 22.12+ and npm. Node 24 LTS is recommended and pinned in .nvmrc (nvm use).
git clone https://github.com/joshduplaa/seattle-neighborhood-pulse.git
cd seattle-neighborhood-pulse
npm ci
npm run dev # http://localhost:3000
No API key or database is needed. Optionally, copy .env.example to .env.local and set SOCRATA_APP_TOKEN to raise Socrata's throttling limits.
| Command | What it does |
|---|---|
npm run dev | Development server (copies the MapLibre worker first) |
npm run build / npm start | Production build and server |
npm test | Unit and component tests (Vitest + React Testing Library) |
npm run typecheck | tsc --noEmit (strict, noUncheckedIndexedAccess) |
npm run lint | ESLint (Next.js core-web-vitals + TypeScript rules) |
npm run check | Typecheck, lint and test together |
| Dataset | Customer Service Requests, City of Seattle (public domain) |
| Dataset ID | 5ngg-rpne |
| API | Socrata SODA 2 / SoQL: https://data.seattle.gov/resource/5ngg-rpne.json (API docs) |
| Scope | Requests submitted through the Find It, Fix It app, the Customer Service Request portal, phone, email and other channels, for request types that are or were offered to the public |
| Size and freshness | ~2.53 million rows, Jan 2013 → present; refreshed once a day (as of 2026-10-20) |
All dataset constants live in src/lib/socrata/config.ts.
| Dataset | Customer Service Request Tracking Data (43nw-pkdq), City of Seattle |
| Coverage | ~446,500 requests since 2023-01-01: Abandoned Vehicle, Graffiti Report, Illegal Dumping, Public Place Litter & Recycling, Streetlight Repair |
| Fields used | servicerequestnumber, servicerequesttype, createddate, updateddate, statuscategory (Routed to Department / In Progress / Closed) |
How it's measured:
Closed update came within 7 days and within 30 days, plus the median days. Requests never closed stay in the denominator.… GROUP BY servicerequestnumber |> SELECT date_diff_d(u, c) …); no per-request rows are downloaded.TRACKED_TYPES).| Equity data | Racial and Social Equity Composite Index Current, City of Seattle (2026 index) |
| Geography | Seattle's 179 census tracts (2020), each with COMPOSITE_QUINTILE from Highest Equity Priority (most disadvantaged) to Lowest; 34–38 tracts per quintile |
| Stored as | src/data/equity-tracts.json, regenerated with node scripts/fetch-equity-tracts.mjs (server-side only) |
How the equity analysis is calculated:
| Dataset | Community Reporting Areas (2020) – Redistricting Data 1990–2020, City of Seattle |
| Hosted at | Seattle GeoData (ArcGIS): services.arcgis.com/ZOyb2t4B0UYuYNYH/arcgis/rest/services/CRA_90_20_PL/FeatureServer/0 |
| Used | 53 area polygons (simplified to ~10 m) and F2020_PL_data_TOT_POP, the 2020 Census P.L. 94-171 total population (sums to 737,015, Seattle's 2020 count) |
| Stored as | Static files in src/data/, regenerated with node scripts/fetch-cra-data.mjs |
All 53 area names match the requests dataset's community_reporting_area values exactly, ignoring case (verified on 2026-10-20). Boundaries and 2020 population change rarely, so they ship with the app instead of being fetched at runtime.
servicerequestnumber, webintakeservicerequests (request type), departmentname, createddate, methodreceivedname, servicerequeststatusname, location (address), latitude/longitude, latitude_longitude (point), zipcode, councildistrict, policeprecinct and community_reporting_area.createddate is a floating timestamp (no UTC offset) in Seattle local time. The app treats it as a local calendar date and never shifts it through a timezone.Closed, Open, Reported, New, Transferred to Other Dept, Duplicate (Closed), Duplicate (Open), Closed as Duplicate, Closed -Incomplete Information, Canceled. They are shown verbatim, not regrouped.Browser (client components) Next.js server Seattle Open Data
────────────────────────── ────────────────── ─────────────────
useFilterState ── URL search params
useDashboardData ── GET /api/dashboard ──▶ route.ts
├─ parseFilters (validate/clamp)
└─ getDashboardDataCached ── TTL cache (15 min, in-flight de-dupe)
└─ getDashboardData ── 8 SoQL queries in parallel ──▶ /resource/5ngg-rpne.json
├─ normalize (zod)
└─ aggregate (zero-fill, buckets, totals)
src/
app/
page.tsx, layout.tsx page shell, header, nav, footer
api/dashboard/route.ts the only server endpoint: validation, error mapping, gzip
lib/
socrata/config.ts dataset IDs, URLs, field names, limits
population.ts 2020 population, per-1,000 rates, quantile classes
equity.ts, geo.ts equity-of-response analysis; point-in-polygon lookup
socrata/soql.ts SoQL query builders (escaping, half-open date range)
socrata/client.ts fetch with timeout, retries, typed SocrataError
socrata/normalize.ts zod validation and normalization of raw rows
aggregate.ts time-series bucketing, totals, top-N folding
dashboard.ts assembles one coherent DashboardData response
dashboard-cache.ts, cache.ts in-process TTL cache
filters.ts, dates.ts, format.ts, types.ts
components/
map/ MapLibre map (client-only), layers, legend, overlays
charts/ TrendChart (Recharts), RankedBars (semantic HTML)
dashboard/ filters, metrics, panels, recent list, detail, provenance
hooks/ URL filter state, data fetching with abort
data/ CRA boundaries + 2020 population (generated by scripts/fetch-cra-data.mjs)
Aggregate on the server, never in the browser. Each filter change triggers ten SoQL queries server-side:
snap_to_grid density grid of about 550 m cells;The browser receives about 76 KB gzipped, never millions of rows.
Explicit data scopes. Totals, charts, breakdowns and the density layer cover all matching requests. Markers and the recent list are a labelled sample of the newest 2,000. The total is derived from the type facet, and tests check that the facets and the time series sum to the same number.
One filter model everywhere. parseFilters normalizes the URL on the client and the query on the server identically. Invalid input is corrected and reported, never silently trusted. SoQL literals are escaped.
Caching. The source updates once a day, so results are cached in-process for 15 minutes, keyed by the normalized filters, and concurrent identical requests share one upstream call. Failures are never cached. Responses also carry Cache-Control: s-maxage=300, stale-while-revalidate=600 for CDNs.
'use cache' was tried and dropped: errors thrown inside it are serialized and lose their class, which broke the typed error mapping (timeout → 504, rate limit → 503, and so on).Presets end yesterday. The City publishes once a day, so "today" is almost always incomplete and would look like a sudden drop. Custom ranges can include today, and the chart then warns that the last day may be incomplete.
Ranked bars are HTML, not SVG. Category, status and area breakdowns double as filter controls, so they are real buttons with aria-pressed and wrap long Seattle place names. Recharts draws the time series, where a crosshair and tooltip matter.
Map. MapLibre GL 6 with the key-less OpenFreeMap Positron basemap (OpenStreetMap data). MapLibre 6 loads its web worker relative to its own module, which bundlers relocate, so copies the worker into before and , and the app calls .
npm ci, npm run check and npm run build on Node 24 for every pull request and every push to main.npm run typecheck, npm run lint, npm test and npm run build all pass (CI runs them on every pull request).src/lib/__fixtures__/), malformed input, time-series bucketing and partial buckets, cache behaviour, HTTP client retry and error classification, the API route's error mapping and gzip, and key components.Last-Modified header.It's a standard Next.js app with no database or secrets.
SOCRATA_APP_TOKEN.npm ci && npm run build && npm start (set PORT as needed). The prebuild script copies the MapLibre worker, so always build with npm run build, not next build directly.data.seattle.gov (server) and tiles.openfreemap.org (browser) must be allowed.Data: City of Seattle, Customer Service Requests (public domain). Basemap © OpenStreetMap contributors, OpenFreeMap. This is an independent project, not affiliated with the City of Seattle.
scripts/copy-maplibre-worker.mjspublic/vendor/devbuildsetWorkerUrl()Visual system. A neutral fog canvas and slate ink, one data hue (Puget Sound teal #00789a), a one-hue sequential ramp for density, and ferry orange reserved for the selected request. The data colors were checked for colour-vision-deficiency separation and contrast.