fix(rx-coverage): honour configured defaults and save an independent viewport - #136
Conversation
…ork (#124) test-issue-124-rx-coverage-viewport.js loads the real public/rx-coverage.js in a vm with a fake Leaflet map (records setView/fitBounds), fetches the test releases one by one, fake storage/location/history and a manual clock. On master 9 of 11 fail: the page always starts at 51.0, 4.8 zoom 8 (no URL viewport, no rx-coverage-view, no /api/config/map), nothing is saved on move and lat/lon/zoom never reach the URL, and after destroy or a quick remount the old mount's map, observer extent and leaderboard still land on the new page. The offline-fallback and "requests unchanged" controls pass. Registered in test-all.sh and the CI unit step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
… async work (#124) Initial viewport, first valid source wins: 1. explicit URL lat/lon/zoom, 2. this page's own rx-coverage-view, 3. /api/config/map, 4. the offline fallback 51.0, 4.8 zoom 8 (the page's original start). All three values must be present, numeric and in range (lat -90..90, lon -180..180, zoom 1..19); anything invalid, partial or out of range falls through. The page saves its view on move under rx-coverage-view and never reads or writes the main map's map-view. syncHash() keeps days and rx and adds lat/lon/zoom once the map exists; rx is now URL-encoded. An observer-only rx= link (no explicit viewport) still fits that observer's coverage. A generation counter, bumped by init() and destroy(), makes the config response, the settle timer, move handlers and the extent, coverage and leaderboard responses of an old mount do nothing on a destroyed or replaced page. Coverage filtering and the leaderboard are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
Independent review of
|
| Criterion | Result |
|---|---|
1. A valid URL lat/lon/zoom wins |
Met [F]: unit test 1. Browser: ?days=14&lat=48.85&lon=2.35&zoom=10 with a saved view at 10,10 z5 opened at 48.85, 2.35 z10. Master opened at 51, 4.8 z8. |
2. Valid rx-coverage-view next |
Met [F]: unit test 2. Browser: after a drag, SPA nav away and back restored 55.949, 13.368 z9. Master reset to 51, 4.8. |
3. /api/config/map next |
Met [F]: unit test 3. Browser: fresh context opened at the configured 55.68, 12.57 z9. Master: 51, 4.8 z8. |
| 4. Documented offline fallback | Met [F]: unit test 4 covers a failed fetch, a bad center, an out-of-range center and {}. See finding 5 for when it can apply. |
Only rx-coverage-view, never map-view |
Met [F]: unit test 6. Browser: the RX drag left map-view null. Moving the main map to 40,-3 z6 wrote only map-view, and RX Coverage then reopened at its own saved view. |
| Invalid, partial or out-of-range values fall through | Met [F]: unit test 5 and probe P1. Browser: ?lat=95&lon=2&zoom=10 fell through to the saved view. There is a test gap (finding 3) and an antimeridian self-rejection (finding 1). |
days/rx preserved as the viewport changes |
Met [F]: unit test 7. Browser: after the drag the hash was days=7&lat=...&lon=...&zoom=9. After a leaderboard click it was days=7&rx=<pk>&lat=...&lon=...&zoom=10. |
Observer-only rx= link still fits |
Met [F]: unit test 8. Browser, fresh context: ?rx=<pk> fitted to the seeded observer (56.187, 10.207 z10), the same as master. |
| Delayed config/extent/coverage responses cannot affect a destroyed page | Met in code [F]: probes P2 to P4 and P6 pass. Only partly pinned by the PR's tests (finding 2). Browser: an immediate navigate-away after mount showed no errors and left no #rxMap. |
| Coverage filtering and leaderboard unchanged | Met [F]: unit test 11 checks the exact request URLs. The leaderboard rendered 1 row on both master and the head. drawCoverage/loadBoard request construction is unchanged apart from the guards. |
Test-first and mutants
New test: 2 pass and 9 fail on master ad011021 and on commit A. On the head, 11 pass and 0 fail [F]. The two that pass on master are the fallback control and the requests control, as the PR says.
Mutants were run against the head with test-issue-124-rx-coverage-viewport.js. I then re-ran the survivors against my probe file (probe-124.js, kept in my dir only). The original file was restored and shasum matches git show 1ab283a2:public/rx-coverage.js (7011dc53) [F].
| # | Mutant | PR test | Probe |
|---|---|---|---|
| M1 | URL precedence removed | caught (1, 7) | |
| M2 | saved view before URL | caught (1) | |
| M3 | skip saved view | caught (2, 5) | |
| M4 | zoom range check removed | caught (5) | |
| M5 | null/empty handling in validView removed |
survived (gap) | caught P1 |
| M6 | save under map-view |
caught (2, 5, 6) | |
| M7 | syncHash drops the viewport |
caught (7) | |
| M8 | explicit view still fits the observer | caught (8) | |
| M9 | createMap generation guard removed |
caught (9) | |
| M10 | coverage guard weakened to destroyed |
survived (gap) | caught P3 |
| M11 | extent guard weakened to destroyed |
survived (gap) | caught P2 |
| M12 | leaderboard guard weakened | caught (10) | |
| M13 | destroy() does not bump the generation |
survived | equivalent: destroyed covers the time until the next init(), which bumps the generation itself |
| M14 | moveend handler guard reduced to !map |
survived | near-equivalent: the harness debounce is the identity. An old debounced call after a quick remount would only redo save/sync/draw for the new map |
| M15 | moveend does not save | caught (6) | |
| M16 | settle-timer guard reduced to !map |
survived (gap) | caught P4 (the old timer causes a second coverage draw on the new map) |
| M17 | rx not encoded in the hash |
survived (gap) | caught P5 |
| M18 | init() guard reduced to !destroyed |
survived (gap) | caught P6 (the stale start() renders into the old container and fetches the leaderboard twice) |
Suites run locally
| Suite | master ad011021 |
head 1ab283a2 |
|---|---|---|
test-issue-124-rx-coverage-viewport.js |
2 pass / 9 fail | 11 / 0 |
probe-124.js (mine, P1 to P5) |
1 / 4 | 6 / 0 (P1 to P6) |
test-frontend-helpers.js |
705 / 2 (favStar ×2) |
705 / 2 (same two) |
test-rx-coverage-escape.js |
crashes: "could not locate row-builder end" | same crash (pre-existing) |
test-rx-coverage-config-race.js |
OK | OK |
test-nav-first-load-fit.js / test-nav-dynamic-link-lifecycle.js / test-nav-priority-scheduler.js |
23 / 21 / 15 pass | 23 / 21 / 15 pass |
test-privacy-page.js, test-node-reach-coverage-debounce.js |
34 pass, OK | 34 pass, OK |
test-packet-filter.js / test-aging.js |
92 / 19 pass | 92 / 19 pass |
test-rx-coverage-mobile-nav-e2e.js (Playwright) |
3 / 0 | 3 / 0 |
test-e2e-playwright.js |
6 pass, then fail-fast on "Version info lives on Perf dashboard" [K] | identical |
- Go was not run:
cmd/serverandcmd/ingestorare untouched.node --check public/rx-coverage.jspasses. - The PR adds no Playwright E2E. It relies on the vm unit test and a manual browser check [T].
Browser
- Setup [F]:
- The
e2e-up.shservers ran on :13700 (master) and :13701 (head), each with a localconfig.json(clientRxCoverage.enabled,mapDefaults55.68, 12.57 z9). - The fixture DB has no
client_receptionstable, so I created the ingestor schema in my DB copies and seeded 20 synthetic receptions near 56.1, 10.1 for one observer. - Map instances were captured by wrapping
L.mapin an init script.
- The
- Scenario
scenario.js, run on both servers:- fresh load
- real mouse drag
- SPA nav away and back
- main map moved independently
- leaderboard row click
- quick mount followed by navigate-away
- fresh observer-only link
- explicit URL plus a conflicting saved view
- invalid URL plus reload
- Results are in
scenario-master.jsonandscenario-head.json:- On master, every load opens at 51.0, 4.8 z8 and nothing is saved or synced.
- On the head, every precedence step behaves as specified (see the AC table).
- There were zero console or page errors on both.
- A separate
dateline.jsrun reproduced finding 1.
Performance and security
- Not a hot path [F]. Each debounced
moveend(200 ms) adds onelocalStorage.setItemand onehistory.replaceState. There is one/api/config/mapfetch per mount, and only when there is no URL view or saved view. No per-item calls and no new collections. The generation counter is a single integer. - Timers [F]: one 150 ms settle timer per mount, as before, now guarded by
isLive(gen). No intervals were added. - DOM [F]:
- None of the added lines contains an HTML sink. The one moved
innerHTML(leaderboard error) is static text. - The URL values reach
setViewonly as validated finite numbers.rxis nowencodeURIComponent-ed into the hash. - No hardcoded colours were added.
- None of the added lines contains an HTML sink. The one moved
- No Go, no
map[string]interface{}and no server writes: the backend is untouched.
Not verified
- Real RX coverage data (staging) and touch devices [T], and I did not check them either.
- Actual Leaflet
map.remove()handler teardown for M14. I did not check whether Leaflet cancels the pending debounced call [A]. scripts/check-xss-sinks.sh --diffand eslint were not run: they need a git checkout, and I made no git writes. I grepped the added lines for sinks by hand instead. The PR reports both as clean [T].- The PR's "Not verified" section is honest as far as it goes. It omits finding 5 (online deployments without
mapDefaultsnow open at the server default rather than 51.0, 4.8).
…coverage-viewport # Conflicts: # .github/workflows/deploy.yml # test-all.sh
Test 10 releases the old mount's responses while the new map is still null, so the !map / !covLayer checks catch them and the generation checks in fitToObserver and drawCoverage could be removed with every test green. Test 12 remounts with a saved rx-coverage-view, so the new map and coverage layer exist at once, and then releases the old mount's observer-extent and coverage responses. It fails when either guard is reduced to its null check (verified for both). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FcyXW5RdFzLZhuL1ntAsY
|
Review feedback addressed (commit
The other review findings (antimeridian Generated by Claude Code |
Relates to #124
Plan and design
The user asked for autonomous work, so the plan is written here instead of waiting for sign-off (AGENTS.md rule 5).
Commits:
44c367bf: tests that reproduce the bug (red on master).1ab283a2: the fix inpublic/rx-coverage.js.Claims in the issue, verified against master
[51.0, 4.8], zoom 8./api/config/map, a URL viewport or any saved view.destroy().Initial viewport (first valid source wins)
lat/lon/zoomin the hash.localStorage['rx-coverage-view']./api/config/map(center,zoom).51.0, 4.8, zoom 8. That was the page's original start and the fork's home area, so offline behaviour does not change.Validation. All three values must be present, numeric and in range (lat −90..90, lon −180..180, zoom 1..19). Anything invalid, partial or out of range falls through to the next source.
Saving. On
moveendthe page saves torx-coverage-viewonly. It never reads or writes the main map'smap-view, so the two pages do not recentre each other.URL.
syncHash()keepsdaysandrxand addslat/lon/zoomonce the map exists;rxis now URL-encoded. An observer-onlyrx=link (no explicit viewport) still fits that observer's coverage.Stale async work. A
generationcounter is bumped on each mount and indestroy(). The config, observer extent, coverage and leaderboard responses and the delayedinvalidateSizeall checkisLive(gen), so late responses from a destroyed or replaced mount do nothing.How this differs from upstream
Kpa-clawbot/CoreScope#2033Upstream is read as a reference only; nothing was cherry-picked.
51.0, 4.8, zoom 8 (this fork's area) instead of upstream's37.6, -122.1, zoom 9.rxis URL-encoded in the hash.Acceptance criteria
test-issue-124-rx-coverage-viewport.js)rx-coverage-view/api/config/maprx-coverage-view, nevermap-viewdaysandrxpreserved as the viewport changesrx=link still fits the observerTests
test-issue-124-rx-coverage-viewport.jsloads the realpublic/rx-coverage.jsin a vm. It uses a fake Leaflet, fetches released one by one, fake storage, location and history, and a manual clock. It is registered intest-all.shand thedeploy.ymlunit step.The 2 that pass on master are the offline-fallback and "requests unchanged" controls.
Other checks:
test-rx-coverage-escape.jsfails identically on master and on this branch (pre-existing, unrelated).scripts/check-xss-sinks.sh --diff: clean.Browser check
Local Chromium against a server on
test-fixtureswithclientRxCoverage.enabledandmapDefaultsin a local config:rx-coverage-viewand leftmap-viewuntouched.lat/lon/zoomwas honoured.Perf
Not a hot path. There is one extra
localStoragewrite permoveend, and a few integer comparisons in async callbacks.Not verified
Overlap with other open PRs
.github/workflows/deploy.ymlunit step andtest-all.sh: one line each, next to lines from PR fix(packets): empty observer/type selections on Clear Filters #132 (fix(packets): Clear Filters must reset observer and type selection state #121), PR fix(analytics): treat the distance index's 202 as a transient building state #133 (fix(analytics): treat lazy distance-index 202 responses as transient #120), PR fix(live): wire every persisted view toggle before Live init awaits #135 (fix(live): wire persisted view toggles before initialization awaits #125) and the upcoming PRs for fix(live): recover the shared WebSocket from silent half-open connections #117 and ui: show the running CoreScope version in the navigation drawer #111.public/rx-coverage.js.85bfee49, the master at the time. It merges cleanly with the currentorigin/masterd264716c(checked withgit merge-tree).origin/master, in issue order: the only conflicts are the test-registration lines indeploy.ymlandtest-all.sh, where several PRs add a line after the same anchor. Keep both lines; nothing else overlaps textually.🤖 Generated with Claude Code
https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
Generated by Claude Code