fix(picker): show multi-state border gauges in the gauge picker - #96
Conversation
gauge_picker.php filtered gauges with an exact `g.state IN ('OR','WA')`,
but a border gauge on a state-line river stores a comma list ('OR,WA')
that equals neither 'OR' nor 'WA'. So the entire Columbia mainstem
(John Day, The Dalles, Bonneville, Vancouver, St. Helens — all 'OR,WA')
showed on the static gauges.html but vanished from gauge_picker.php.
The static build already handles this: web/build/gauges.py splits
gauge.state on the comma so a border gauge renders under every one of
its states. Mirror that in the picker, in three spots + the client:
- AJAX WHERE: match each selected abbrev against the comma-wrapped value
(INSTR(',' || g.state || ',', ',OR,') > 0) instead of `IN (...)`.
- AJAX row mapping: re-join the matched abbrevs as full names
('Oregon,Washington') so the client can split them.
- State pills: split each distinct gauge.state so a border gauge with no
single-state sibling still contributes both pills.
- gauge_picker.js: split the row's state on comma and bucket it under
each requested state (buildAllRows() already dedupes by id).
Extends GaugePickerIntegrationTest with an OR,WA seed: the border gauge
now surfaces a Washington pill and matches an Oregon- or Washington-only
AJAX filter, while a single-state MT gauge still doesn't leak into a
Washington filter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — #96
|
…ge (#97) Fast-follow to #96. That PR made border gauges (gauge.state = 'OR,WA', the whole Columbia mainstem) selectable in the gauge picker, but their destination — custom_gauges.php (?ids=...) — carried the same exact-match state handling the picker had: - _compute_custom_gauges_filters(): isset(STATE_ABBREVS['OR,WA']) is false, so a border gauge contributed no state pill. - row render: STATE_ABBREVS['OR,WA'] ?? '' → empty $state, so the `$state !== '' && $huc8 !== ''` guard emitted no data-state/data-huc8 at all — the row escaped both the state and watershed filters. - the State filter group lacked data-split="csv", so even a comma data-state wouldn't have matched a pill. Mirror the static build (web/build/gauges.py + levels.py:469) in all three spots: split gauge.state on the comma for the pills and the row's data-state ('Oregon,Washington'), and render the State group data-split="csv" so filters.js splits the row value to match each pill. Extends CustomGaugesIntegrationTest with an OR,WA seed gauge: one border gauge now surfaces both Oregon and Washington pills and renders as a filterable row (data-state="Oregon,Washington" data-huc8=...). No schema or data change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: round-6 deep project review (graded B+, ▲ from B−) Sixth deep project review of the entire tracked repo — 6 cold facet auditors (Python, PHP/security, schema/data, tests/CI, ops, docs) + synthesizer hand-re-verification, judging two bands: (A) did round-5's fixes durably stick, and (B) what did #93–#98 + migrations 0069–0071 + the two direct-to-main commits introduce. The recursive integrity check passes cleanly for the first time in the series: every round-5 fix (R1.1/R1.2/R1.3/R1.5/R2.1/R3.x/R4.x) landed as a committed PR and is still present at HEAD, and every mechanized guard is proven non-vacuous by break-it experiment. New code is clean — no CRIT/HIGH: #93 USACE kcfs→cfs (correct, per-series), migrations 0069/0070/0071 (idempotent, FK-clean, Bridgeport DROP cascade residue-free), #96/#97 multi-state pickers, #95/#98 gradient JS. Two MED findings, both recurrences of round-5 classes closed by documentation not mechanization: (1) two direct-to-main commits, one of which broke CI on main (the {}-is-a-dict bug); (2) a nightly snapshot overrode migration 0067's sort_name for gauge 217 with no migration. Root cause is shared — main accepts un-CI-gated direct pushes from both humans and the snapshot bot. Lever: route everything through a CI gate (branch protection + a self-gating/auto-merging snapshot), a snapshot-column drift guard, and teach seed_gauge_display to preserve migration-pinned sort_names. Two facet over-claims dissolved on hand-re-verification (the USACE temperature-docstring drop is a correct fix; check_reaches DOES range-check vertices via validate_lat_lon). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: fold external-review corrections into the round-6 review (#99) The PR #99 external verification pass re-confirmed every finding, severity, and the B+ grade against db34ae0 (recommendation: merge), and flagged one inaccurate evidence line plus three off-by-one citations. Corrected: - MED #1: drop the `git branch --contains` "reachable only from main" claim — feature branches later cut from main now contain 9b428bb / 6007c21, so containment no longer distinguishes them. The direct-to- main conclusion stands on the durable evidence (linear f3ed673..HEAD, no merge commit, missing (#NN) suffix). - citations: ci.yml:114→115, SourceUrlTest.php:83-84→84-85, check_reaches.py:212→213. Added an External-review note recording the pass + the one below-LOW item it surfaced (the 0069/0070 header comments' now-stale PENDING_RECONCILIATION wording). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
The entire Columbia mainstem shows on the static gauges.html but is missing from gauge_picker.php (the "Build Your Own Gauges Page" picker).
Root cause: every Columbia mainstem gauge is a state-line border gauge whose
gauge.stateis the comma listOR,WA(John Day, The Dalles, Bonneville, Vancouver, St. Helens — all 5, all with live data). The picker filtered with an exact match:so the border gauges matched no state and were dropped — both server-side (the SQL) and client-side (
gauge_picker.jspartitioned AJAX rows withbyState.get(r.state), an exact lookup that also misses"Oregon,Washington").The static build never had this bug:
web/build/gauges.pysplitsgauge.stateon the comma so a border gauge renders under every one of its states (andfilters.jssplitsdata-statethe same way). This PR brings the picker in line.Fix
php/gauge_picker.php(3 spots) +static/gauge_picker.js(1 spot):WHEREg.state IN (?,?)INSTR(',' || g.state || ',', ',OR,') > 0OR-ed per abbrev — matchesOR,OR,WA,WA,OR; not a substring of an unrelated codestate$ABBREV_TO_STATE['OR,WA'] → ''"Oregon,Washington"'OR,WA' → no pillgauge_picker.jsbyState.get(r.state)r.stateon comma, bucket under each requested state (buildAllRows()already dedupes by id)No schema/data change — it's purely how the picker reads the existing
statecolumn.Tests
tests/php/GaugePickerIntegrationTest.phpgains anOR,WAseed gauge and asserts:state: "Oregon,Washington";INSTRis anchored).Local verification (full gate, green)
phpunit GaugePickerIntegrationTest— 8/8 (4 new)🤖 Generated with Claude Code