Implement DMV Water Watch MVP (Phase 0–2): map, connectors, grading, onboarding - #2
Merged
Conversation
Scaffolds the DMV Water Watch app end-to-end so a contributor (or agent) can clone, install, run, and see ~10 live water-quality grades on the inner-DMV map. Connectors (connectors/): - usgs-nwis (real API) — reference implementation - noaa-precip (real API) — 48h precipitation totals - epa-hmw (real ATTAINS API) — chronic impairment badge - anacostia-riverkeeper (fixture-backed; Phase 2 spike pending) - doee-sondes (fixture-backed; Phase 2 spike pending) Shared http/units/log helpers; structured ConnectorError for graceful pipeline degradation. Grading (grading/v1.ts): - Pure, deterministic implementation of GRADING.md rubric. - 17 tests cover all 5 worked examples and § 4 edge cases. Pipeline (pipeline/): - Runs every connector in parallel, joins records to sites, runs grading twice (paddle + swim), emits manifest/sites.geojson/grades/history/ sources artifacts to public/data/ and data/snapshots/dev/. Frontend (app/, lib/): - Next.js 14 App Router static export. - MapLibre + OSM raster tiles (token-free default; Mapbox-ready via env). - Site detail card with grade hero, signal breakdown, 30-day sparkline, source attribution, share button. - Activity toggle (paddle/swim) with FR-20 re-thresholding. - Methodology, About, Sources pages; first-visit disclaimer interstitial; stale-data banner; PWA manifest + service worker. Workflows: - ci.yml — type-check, validate, test, pipeline smoke, Next.js build. - connectors.yml — hourly/6-hourly/weekly cron; optional R2 sync. Docs: - CONTRIBUTING.md — 14-section developer guide including agent guidance. - TESTING.md — smoke test, layered verification, troubleshooting matrix. - README.md updated for the implemented state. Verified: 49 unit tests + grading rubric tests pass; type-check clean; Next.js builds 17 static pages; static-served and dev-served routes return 200 across /, /site/[id], /methodology, /sources, /about, and all /data/* artifacts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-source spike findings (documented in connector READMEs): - Anacostia Riverkeeper publishes weekly results via Swim Guide. The /api/v1/beach endpoint exists (returns 401 without auth) — replacement path is to request a token from Swim Drink Fish per docs/outreach.md § 3.1. Eight Swim Guide ARK stations confirmed and mapped into data/sites.json with their canonical names. - DOEE sondes flow through an EQuIS portal at dcdoeepub.equisonline.com. Export pattern requires interactive DevTools inspection (documented as the next spike step). - Both connector READMEs lay out the exact code-level swap-in path so the next contributor can finish the integration without re-discovery. Site catalog expanded from 10 to 24 inner-DMV launches: Anacostia Park, Diamond Teague, Hickey Run, National Arboretum, Northeast Branch at Campus Drive, Lower Beaverdam Creek, Lake Artemesia, Hains Point, Daingerfield Island, Oronoco Bay, Jones Point, Fort Foote, Pennyfield Lock, Swain's Lock. EPA Assessment Unit IDs added for DC Anacostia + Potomac sites (the connector degrades gracefully if an AU doesn't resolve in ATTAINS). Fixture updates for ARK + DOEE so the new sites get realistic grades out of the box. Tests updated for the new station IDs; 49 tests still pass. UI polish: - Refresh-error banner when client-side data fetch fails. - Map background grays out before tiles load. - Empty-state message when zero sites are loaded. Non-technical contributor onboarding: - GETTING_STARTED.md — plain-English intro for site curators, paddlers, outreach contacts. Explains what the project is, who it's for, and how to help without writing code. - GitHub issue templates: suggest-site, report-incorrect-grade, verify-site, bug-report, feature-request. Each is a form with validation so contributors don't need to know what fields to fill in. - README + CONTRIBUTING updated to point newcomers to GETTING_STARTED. Verified: 49 tests pass, 24/24 sites validate, pipeline emits 63 records, Next.js builds 31 static pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-agent review (reuse / quality / efficiency) on the Phase 0-2
diffs surfaced a handful of net-positive cleanups; this commit applies
them. Behavior unchanged — all 49 tests still pass.
Code reuse:
- Extract `groupStationsBySite()` to connectors/shared/sites.ts. The
identical 7-line station→sites Map-build loop was duplicated across
all 5 connectors; collapsed to one call site each.
Quality / correctness:
- app/components/Map.tsx: rename component default export `Map` → `SiteMap`
to remove the collection-vs-component naming ambiguity.
- Map.tsx: split the marker effect so selecting a pin no longer rebuilds
all 50 marker DOM elements — selection now mutates `style.transform` on
the two affected elements only. Marker entries are stored as
{ marker, el } so updates can refresh the SVG without destroying the
marker.
- Map.tsx: stash `onSelect` and `onUserLocate` in refs so the marker
effect doesn't churn when MapShell passes new inline callbacks.
- Map.tsx: drop the brittle `e.target?._lastKnownPosition?.coords`
access in the geolocate handler. Use the standard GeolocationPosition
shape that the event carries.
- grading/v1.ts: replace the nested `value === 0 ? ... : value === 2 ? ...`
ternary for chronic impairment with a `CHRONIC_TO_STATUS` Record lookup.
- grading/v1.ts: drop the "Step 1 / Step 2…" banner comments that just
narrate what the next two lines do. Drop the unused `_activity`
parameter from `combine()`.
- app/components/DetailCard.tsx: collapse the duplicated
`standalone ? <h1+header> : <h2+header>` block to a single header
with a dynamic heading tag.
- connectors/anacostia-riverkeeper/index.ts and doee-sondes/index.ts:
drop the TOCTOU `existsSync` pre-check before `readFileSync`. Wrap
the read in try/catch on ENOENT instead.
- Connector top-of-file block comments explaining "Phase 2 spike
pending" replaced with single-line `TODO(phase-2):` markers; the
prose lives in each connector's README.
Efficiency:
- connectors/noaa-precip/index.ts and epa-hmw/index.ts: fan out the
per-station HTTP calls with Promise.allSettled. NOAA goes from ~4
sequential ~300ms calls to one wall-clock window. Per-station retry
and recoverable-error handling preserved.
- public/sw.js: switch /data/* from network-first to
stale-while-revalidate so navigations don't pay a round-trip when a
recent build is already cached. App shell uses the same strategy.
Skipped findings (recorded for posterity):
- haversineKm extraction to a shared module — single call site.
- `pickFreshest` consolidation with connector "latest by" helpers —
signatures meaningfully differ (parameter filter vs station key).
- `runOneConnector` 5-positional-arg signature — internal-only, types
distinct enough that order errors are unlikely.
- Workflow cron cadence — running every connector on every cadence is
cheap and the alternative (parameterizing the pipeline by cadence)
adds complexity without saving meaningful CI minutes.
Verified: 49 tests pass, type-check clean, Next.js builds 31 pages,
pipeline emits same 24 sites / 63 records / 6-5-1-12 grade distribution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sites, Lighthouse CI Everything in Phase 3 that doesn't need an external API key or human intervention. Drop in a Swim Guide token and a Cloudflare account and this is ready to publish. Polish: - React error boundary at app/error.tsx (and app/global-error.tsx for root-layout crashes). Renders a non-modal recovery card per UX § 5.3. - Sentry wiring via lib/sentry.ts: lazy-loads @sentry/browser only when NEXT_PUBLIC_SENTRY_DSN is set; falls back to console.error otherwise so local dev needs no token. - Per-site Open Graph + Twitter card meta via generateMetadata() on /site/[id]. Shared links render with the site's actual grade + reason instead of generic homepage meta. - Recharts-based 30-day history bar chart replaces the colored-div row in Sparkline.tsx. Dynamically imported so Recharts isn't in the initial bundle. - PWA manifest: maskable + any icon entries, app shortcuts to /methodology and /sources, full categories. - Apple touch icon + mask-icon via Next.js metadata.icons API. Data: - data/sites.json expanded 24 → 34: the-wharf-dc, georgetown-waterfront, gravelly-point, lady-bird-johnson-park, carderock-recreation, old-anglers-inn, glen-echo-park, lock-7-cabin-john, piscataway-park, fort-washington-park. All inside the bounding box; the remaining ~16 sites toward 50 will come via the issue templates. Ops: - docs/runbook.md: P0/P1/P2 severities, common-incident playbook (map blank, stale data, schema-change response, Mapbox throttle, R2 unreachable, CF outage), postmortem template. - Lighthouse CI workflow step in ci.yml using @lhci/cli; assertions in lighthouserc.json (a11y ≥ 95 hard error, perf ≥ 85 warn). Lighthouse measurements (production export, headless Chrome): - Desktop: Performance 93, Accessibility 96, Best Practices 100, SEO 100. - Mobile: Performance 72, Accessibility 96. - The mobile perf gap is MapLibre bundle weight (~250 KB gzip). Real- world LTE outperforms LH's simulation. Documented as a tracked Phase 4 follow-up; CI will catch regressions from here. Fixes from the audit: - Map.tsx: MapLibre's Marker overrides aria-label with "Map marker" during addTo(). Reset our descriptive label after addTo() so screen readers announce "Buzzard Point Marina, safe for paddle" instead. - globals.css: underline OSM attribution link (color-only differentiation was failing WCAG 1.4.1). - DetailCard.tsx: dynamic-import Sparkline so the ~70 KB Recharts chunk stays out of the initial bundle. Verified: 49 tests pass, type-check clean, sites validator OK on 34/34, pipeline emits 34 sites / 64 records, Next.js builds 41 pages (34 site detail + 7 static), Lighthouse CI config gated on a11y ≥ 95. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-agent review (reuse / quality / efficiency) on the Phase 3 polish diff surfaced four real fixes. Skipped findings recorded below. Fixes: - lib/grade-style.ts: add GRADE_ORDINAL (green=4, yellow=3, unknown=2, red=1). Move from Sparkline.tsx where it was a private constant — it's a domain-level grade ordering and belongs with the other Record<Grade, T> exports. Comment captures the why (unknown ranks above red because gray reads as "no info", not "bad"). - lib/data-source.ts: memoize loadInitialData() in a module-scope promise. generateStaticParams + generateMetadata + SitePage each call it; 34 sites × 2 page generators × 4 JSON files = ~272 reads per `next build`, now ~4. - Sparkline.tsx: replace `points[0]!` non-null assertion with `[only] = points` destructure + explicit null guard. - package.json + ci.yml: move @lhci/cli from `npx --yes @lhci/cli@0.14` to a devDependency. Pins the version, avoids the ~5 MB download per CI run (npm-cache already covers devDeps; `~/.npm/_npx` did not). Skipped findings (false positives or net-neutral): - error.tsx / global-error.tsx duplication — required by Next.js semantics (global-error owns its own <html>/<body> and can't rely on Tailwind being loaded). - captureError + connectors' createLogger merge — different runtimes (browser vs Node), different output shapes. - Dynamic-import skeleton helper — 2 callers with different needs. - Sentry module-level mutability — Sentry.init() is idempotent; HMR not a real concern when no DSN in dev. - Recharts Tooltip formatter cast — Recharts API forces it; custom content component would be cleaner but not blocking. - tracesSampleRate: 0 — explicit signal, keep. - error.tsx loop risk — Sentry dedupes; not a loop. Verified: 49 tests pass, type-check clean, Next.js builds 41 pages, build-time JSON parses drop from O(N) to O(1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stand up the DMV Water Watch app end-to-end so a contributor (or agent) can clone, install, run the pipeline, and see ~24 live water-quality grades on the inner-DMV map. Implements Phases 0–2 of the skeleton roadmap; Phase 3 (production deploy, legal review, real ARK + DOEE integrations) is queued.
Three commits:
ad0547cPhase 0–2 MVP — Next.js 14 static export, 5 connectors (USGS/NOAA/EPA real APIs; Anacostia Riverkeeper + DOEE sondes fixture-backed), deterministic grading rubric perGRADING.md, build pipeline, GitHub Actions workflows, full contributor docs (CONTRIBUTING.md,TESTING.md).3cacd69Phase 2 spikes + onboarding — Investigated Swim Guide + DOEE EQuIS data feeds (findings in connector READMEs). Expanded site catalog from 10 → 24 with confirmed station names. AddedGETTING_STARTED.mdand six GitHub issue templates for non-technical contributors.3530e77simplify — Multi-agent review (reuse / quality / efficiency) drove a net-56 linecleanup: extractedgroupStationsBySiteshared helper, split the MapLibre marker effect so selecting a pin doesn't churn all 50 markers, replaced TOCTOUexistsSyncchecks, parallelized NOAA + EPA per-station fetches, switched the service worker to stale-while-revalidate.What's in the box
app/(Next.js App Router), MapLibre + OSM raster (token-free), bottom sheet / sidebar, activity toggle, methodology / about / sources pages, PWA manifest + service workerconnectors/usgs-nwis(real),connectors/noaa-precip(real),connectors/epa-hmw(real),connectors/anacostia-riverkeeper(fixture-backed, swap path documented),connectors/doee-sondes(fixture-backed)grading/v1.ts— pure deterministic implementation ofGRADING.mdrubricpipeline/build.tsjoins connector records → sites → grades → artifacts inpublic/data/data/sites.json— 24 inner-DMV launches.github/workflows/ci.yml(per-PR),.github/workflows/connectors.yml(scheduled cron).github/ISSUE_TEMPLATE/— suggest-site, report-incorrect-grade, verify-site, bug-report, feature-requestREADME.md,CONTRIBUTING.md,TESTING.md,GETTING_STARTED.mdVerification
npm test— 49 tests pass (32 connector + 17 grading)npm run typecheck— cleannpm run validate:sites— 24/24 sites passnpm run pipeline— 24 sites, 63 records, 6 green / 5 yellow / 1 red / 12 unknownnpm run build:next— 31 static pages built, 104 KB First Load JS (well under the 200 KB NFR-3 budget)Phase 3 queued (not in this PR)
/api/v1/beachtoken pending from Swim Drink Fish (outreach plan indocs/outreach.md§ 3.1)dcdoeepub.equisonline.comCONTRIBUTING.md§ 9)Test plan
npm install && npm run pipeline && npm run dev→ map renders on http://localhost:3000 with colored pins/site/buzzard-pointrenders standalone/methodology,/about,/sourcespages render.github/workflows/ci.yml) — type-check, validate, test, pipeline smoke, Next.js build all greenReviewer notes
grading/v1.ts, please add an ADR (perGRADING.md§ 8) and update the worked-example tests.GETTING_STARTED.mdand the issue templates rather thanCONTRIBUTING.md.🤖 Generated with Claude Code