Skip to content

Implement DMV Water Watch MVP (Phase 0–2): map, connectors, grading, onboarding - #2

Merged
XPGeek merged 5 commits into
mainfrom
claude/zen-sanderson-2f78f7
May 18, 2026
Merged

Implement DMV Water Watch MVP (Phase 0–2): map, connectors, grading, onboarding#2
XPGeek merged 5 commits into
mainfrom
claude/zen-sanderson-2f78f7

Conversation

@XPGeek

@XPGeek XPGeek commented May 18, 2026

Copy link
Copy Markdown
Owner

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:

  1. ad0547c Phase 0–2 MVP — Next.js 14 static export, 5 connectors (USGS/NOAA/EPA real APIs; Anacostia Riverkeeper + DOEE sondes fixture-backed), deterministic grading rubric per GRADING.md, build pipeline, GitHub Actions workflows, full contributor docs (CONTRIBUTING.md, TESTING.md).
  2. 3cacd69 Phase 2 spikes + onboarding — Investigated Swim Guide + DOEE EQuIS data feeds (findings in connector READMEs). Expanded site catalog from 10 → 24 with confirmed station names. Added GETTING_STARTED.md and six GitHub issue templates for non-technical contributors.
  3. 3530e77 simplify — Multi-agent review (reuse / quality / efficiency) drove a net -56 line cleanup: extracted groupStationsBySite shared helper, split the MapLibre marker effect so selecting a pin doesn't churn all 50 markers, replaced TOCTOU existsSync checks, parallelized NOAA + EPA per-station fetches, switched the service worker to stale-while-revalidate.

What's in the box

Layer Files
Frontend app/ (Next.js App Router), MapLibre + OSM raster (token-free), bottom sheet / sidebar, activity toggle, methodology / about / sources pages, PWA manifest + service worker
Connectors connectors/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 grading/v1.ts — pure deterministic implementation of GRADING.md rubric
Pipeline pipeline/build.ts joins connector records → sites → grades → artifacts in public/data/
Site catalog data/sites.json — 24 inner-DMV launches
Workflows .github/workflows/ci.yml (per-PR), .github/workflows/connectors.yml (scheduled cron)
Issue templates .github/ISSUE_TEMPLATE/ — suggest-site, report-incorrect-grade, verify-site, bug-report, feature-request
Docs README.md, CONTRIBUTING.md, TESTING.md, GETTING_STARTED.md

Verification

  • npm test49 tests pass (32 connector + 17 grading)
  • npm run typecheck — clean
  • npm run validate:sites — 24/24 sites pass
  • npm run pipeline — 24 sites, 63 records, 6 green / 5 yellow / 1 red / 12 unknown
  • npm 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)

  • Real Anacostia Riverkeeper integration — Swim Guide /api/v1/beach token pending from Swim Drink Fish (outreach plan in docs/outreach.md § 3.1)
  • Real DC DOEE sondes integration — needs interactive DevTools spike on dcdoeepub.equisonline.com
  • Production deploy on Cloudflare Pages + R2 (env vars per CONTRIBUTING.md § 9)
  • Lighthouse perf + a11y audit against NFR-1 through NFR-11
  • Legal review of disclaimer copy
  • Site catalog expansion 24 → ~50 via the issue templates

Test plan

  • npm install && npm run pipeline && npm run dev → map renders on http://localhost:3000 with colored pins
  • Tap a pin (e.g., Buzzard Point) — detail card shows grade, reason, signal breakdown, sparkline
  • Activity toggle (bottom-left) switches paddle ↔ swim and re-grades thresholds; DC sites disable swim
  • Deep link /site/buzzard-point renders standalone
  • /methodology, /about, /sources pages render
  • First-visit disclaimer interstitial appears (clear localStorage to retest)
  • On a real mobile device or DevTools mobile viewport: no horizontal scroll, no zoom distortion
  • CI workflow runs (.github/workflows/ci.yml) — type-check, validate, test, pipeline smoke, Next.js build all green

Reviewer notes

  • The Anacostia Riverkeeper and DOEE sondes connectors are intentionally fixture-backed in this PR. The Phase 2 spike confirmed real-source paths (Swim Guide API needs a token, DOEE EQuIS portal needs an interactive DevTools session); the README in each connector documents the exact swap-in path so a future PR is a single-file change.
  • The grading rubric is the heart of the product. If you change anything in grading/v1.ts, please add an ADR (per GRADING.md § 8) and update the worked-example tests.
  • For onboarding non-technical contributors (paddlers, site curators), point them at GETTING_STARTED.md and the issue templates rather than CONTRIBUTING.md.

🤖 Generated with Claude Code

XPGeek and others added 5 commits May 18, 2026 16:40
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>
@XPGeek
XPGeek merged commit 6579cc5 into main May 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant