Skip to content

fix(live): wire every persisted view toggle before Live init awaits - #135

Merged
dborup merged 2 commits into
masterfrom
codex/issue-125-live-toggles-before-await
Sep 29, 2026
Merged

dborup merged 2 commits into
masterfrom
codex/issue-125-live-toggles-before-await

Conversation

@dborup

@dborup dborup commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Relates to #125

Summary

#88 moved only the Multibyte toggle ahead of Live init's first await. The other persisted view toggles were restored and wired only after await fetch('/api/config/map') and await loadNodes(): Heat, Inferred hops, Realistic, Color by hash, Favorites, Foreign, Matrix and Rain.

While init ran, those toggles showed their default state instead of the saved one. A click made in that window was neither saved nor kept.

Now every persisted toggle is restored and wired synchronously, right after the markup is written. The effects that need the map are applied once the map exists.

Upstream reference (read only): Kpa-clawbot/CoreScope#1940.

Plan and design

Autonomous run, so the plan is written here instead of waiting for approval (AGENTS.md rule 5).

  1. Test first (commit 2eb7fdbf).
    • test-issue-125-live-toggles-early-e2e.js holds /api/config/map with request interception. That freezes init at its first await right after the panel is painted, the same technique test-live-multibyte-only-e2e.js already uses.
    • test-issue-125-live-toggles-wiring.js is a structural guard.
  2. Fix (commit 6b276d08), all in public/live.js:
    • wireLiveControls(): one table with restore and persist for all nine persisted toggles (Multibyte included). It is called right after app.innerHTML, before any await. It is idempotent per element (data-live-wired). Every mount writes new markup, so each mounted checkbox has exactly one listener.
    • heatEnabled: a module-level mirror for Heat, like the other toggles.
    • syncHeatToggleToMatrix(): the Heat/Matrix interlock. It existed twice on master; now there is one copy. It is applied from the first paint and on every Matrix change.
    • applyLiveControlEffects(): runs after loadNodes() (the queued effects). It builds the heat layer only when Heat is on and Matrix is off, applies the matrix theme, and starts Rain.
    • showHeatMap(): a no-op until the map exists, so a Heat click during init only records the choice. hideHeatMap() is safe without a map.
    • Unchanged: Audio (MeshAudio restore plus its slider panel, and audio needs a user gesture), the geo-filter and region controls (hidden or inert until their own fetches), and geolocation (untouched).

Where the fork differs from upstream

  • Foreign toggle included. Upstream's table has eight toggles; this one adds Foreign, which is persisted the same way in this fork.
  • Heat effect applied in one place. Heat's post-init effect is applied only in applyLiveControlEffects(), together with the Matrix interlock (heatEnabled && !matrixMode), instead of a separate if (heatEnabled) showHeatMap().
  • Idempotency guard. Upstream has none; here each element is wired at most once.

Config and customizer (AGENTS.md rule 8). No new configurable values. The localStorage keys are unchanged.

Acceptance criteria

Criterion Status Evidence
One idempotent Live-control wiring phase before the first startup await Met wireLiveControls(); structural test: the call comes after app.innerHTML and before the first await
Restore state and persistence handlers immediately after the markup exists Met E2E: every toggle shows its saved value while init is held; a click then writes once and is kept
Separate state wiring from map effects; apply queued effects later Met applyLiveControlEffects(); E2E: Heat clicked off during init means no heat layer after init; Matrix and Rain clicked on mean theme and canvas after init
Keep the Heat/Matrix exclusion, filters, WebSocket/replay and SPA remount Met E2E Matrix-saved and Matrix-clicked cases; test-live-multibyte-only-e2e.js 10/10; test-issue-1136-live-region-e2e.js 5/5; test-live-fullscreen-1572-e2e.js 9/9; test-live-mql-leak-1180-e2e.js 2/2; test-live-layout-1178-1179-e2e.js 10/10
Handle audio and geolocation separately Met (left unchanged) Documented in wireLiveControls(); no automatic activation
At most one handler per control after remount Met E2E: after 3 SPA round trips, one click writes exactly once for every toggle
Mutation check that fails if the wiring moves behind the await Met Structural test; moving the call to after await loadNodes() makes it fail (3 passed, 1 failed)

Tests

test-issue-125-live-toggles-early-e2e.js

  • On master: 2 passed, 12 failed. Every toggle shows its default while init is held, and a click then is not saved. The heat layer is built although Heat was switched off. With Matrix saved or clicked on, Heat stays enabled during init.
  • On this branch: 14 passed, 0 failed.
  • Commit 2eb7fdbf had Foreign's default wrong. live.js reads it as !== 'false', so the default is on. That is corrected in the fix commit.

test-issue-125-live-toggles-wiring.js

  • On master: 0 passed, 4 failed. On this branch: 4 passed, 0 failed.
  • The fix commit also makes it ignore full-line comments that mention "await".

Existing suites, same results on master and this branch

Suite Result
test-live.js 110/0
test-live-multibyte-filter.js 9/0
test-live-region-filter.js 13/0
test-live-legend-helper.js 5/0
test-live-anims.js 3 passed, 2 failed (the same on master, and not in CI)

Static checks

  • eslint@8 --quiet public/live.js: 0 errors.
  • scripts/check-xss-sinks.sh --diff origin/master: exit 0.

Browser. All E2E runs used Playwright Chromium against a local server on the migrated fixture, at 1400×900. The sandbox blocks unpkg.com, so a local-only Playwright preload served Leaflet 1.9.4, leaflet.heat 0.2.0 and Chart.js from npm copies of the same versions. That preload is not part of this PR.

Registration. The wiring test is added to test-all.sh and the CI unit step. The E2E test is added to the CI Playwright step next to test-live-multibyte-only-e2e.js. Workflow triggers, permissions, jobs and Kpa-clawbot guards are unchanged.

Performance. Not a hot path. The same listeners are attached as before, only earlier.

Not verified

  • Real mobile devices and staging.
  • Audio and geolocation behaviour (unchanged).

Overlap with my other open PRs

🤖 Generated with Claude Code

https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8


Generated by Claude Code

dborup and others added 2 commits September 29, 2026 08:34
…125)

#88 moved only "Multibyte only" ahead of Live init's awaits. Heat,
inferred hops, Realistic, Color by hash, Favorites, Foreign, Matrix and
Rain are still restored and wired after await fetch('/api/config/map')
and await loadNodes().

test-issue-125-live-toggles-early-e2e.js holds /api/config/map (the
test-live-multibyte-only-e2e.js technique) and checks each toggle while
init is held and after it, plus the map effects (heat layer, matrix
theme, rain canvas), the Heat/Matrix interlock and one listener after
SPA round trips. Against master: 2 passed, 12 failed (every toggle
shows its default during init and drops a click made then; the heat
layer is built although Heat was switched off; Matrix saved or clicked
ON leaves Heat enabled during init).

test-issue-125-live-toggles-wiring.js is the structural guard (fails if
the wiring moves behind an await, drops a toggle, or a second change
listener appears): 0 passed, 4 failed on master.

Registered in test-all.sh, the CI unit step and the Playwright step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
…125)

wireLiveControls() restores and wires Heat, inferred hops, Realistic,
Color by hash, Favorites, Foreign, Multibyte, Matrix and Rain from one
table, synchronously right after the markup is written and before
init()'s first await. A click during init is saved at once and kept.

- Heat gets a module-level mirror (heatEnabled) like the others.
- The Heat/Matrix interlock is one function, syncHeatToggleToMatrix(),
  applied from the first paint and on every Matrix change.
- applyLiveControlEffects() runs after loadNodes(): it builds the heat
  layer only when Heat is on and Matrix off, applies the matrix theme
  and starts the rain. showHeatMap() is a no-op until the map exists,
  so a Heat click during init only records the choice.
- Each mounted checkbox is wired once (data-live-wired); init() writes
  new markup on every mount.
- Audio and the geo-filter/region controls are unchanged (audio needs a
  user gesture; the others stay hidden or inert until their fetches).

Test fixes: Foreign's default is ON (live.js reads it as !== 'false'),
and the structural test ignores full-line comments when looking for
the first await.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8

dborup commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

CI-note: Den første Playwright-kørsel fejlede i test-charts-fluid-1058-e2e.js (viewport 2560 / wrapper 2520px → side-by-side (≥2 cols): 1 kolonne). Testen dækker analytics-layout, som denne PR ikke rører (kun public/live.js og tests). Lokalt bestod den 15/15 mod origin/master d264716c og 15/15 mod denne branch, og samme test bestod i PR #134's kørsel på samme base. Jeg genkørte det fejlede job én gang, og nu er alle checks grønne. Fejlen er intermitterende; jeg kalder den ikke baseline, fordi den ikke kunne reproduceres på master.


Generated by Claude Code

@dborup

dborup commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Independent review of 6b276d08

Verdict: APPROVE with nits. This is a recommendation only; merging is the owner's call.

Reviewed head: 6b276d08140e61a873aaca70657afef56ccc517e (unchanged before and after the review). Work done on a git archive of the PR head, commit A (2eb7fdbf) and origin/master.

Labels: [F] freshly verified by me · [T] taken from the PR text · [A] assumption · [K] known limitation.

Findings

  1. P3, test gap. A saved Rain ON is not checked after init. Removing if (matrixRain) startMatrixRain(); from applyLiveControlEffects() (public/live.js:1222) keeps both new tests green (E2E 14/0, wiring 4/0) [F].

    • Reviewer probe with live-matrix-rain=true saved: the head shows #matrixRainCanvas after init; the mutant does not [F].
    • The E2E only covers Rain clicked on during init.
    • Suggestion: one saved-ON case that asserts the canvas after init.
  2. nit. Heat is now read from localStorage once, at script load. It uses the module-level heatEnabled, public/live.js:53, where master re-read the key on every mount. If another tab changes meshcore-live-heatmap while this SPA stays open, a remount no longer picks it up. The other toggles already worked this way, so this makes Heat consistent rather than adding a new class of issue [F] (code reading).

  3. nit (pre-existing, same as master). The Heat checkbox and its stored value can disagree under Matrix. With Matrix on, the Heat box is forced unchecked while heatEnabled and localStorage stay true. Switching Matrix off re-enables the box but leaves it unchecked and the layer hidden, and the next load shows Heat on again. The PR keeps master's behaviour exactly; mentioned only because syncHeatToggleToMatrix() is now the single place to fix it.

  4. Observation. Three of my mutants are equivalent in practice, so they are not findings:

    • The idempotency guard (data-live-wired) never triggers, because every mount writes new markup.
    • Dropping !matrixMode from the heat branch in applyLiveControlEffects() is masked by the syncHeatToggleToMatrix() call right after it.
    • Dropping the !map guard in showHeatMap() did not throw in my Heat-ON-during-init probe on this fixture.

    The guards are cheap and defensible.

Metadata

Acceptance criteria (issue #125)

Criterion Result
One idempotent wiring phase before the first startup await Met [F]: wireLiveControls() right after app.innerHTML; wiring test; mutant M1 (moved after await loadNodes()) fails the E2E 2/12 and the wiring test 3/1
Restore state and handlers as soon as the markup exists Met [F]: E2E with /api/config/map held; all nine toggles show the saved value, and a click is written once and kept
Separate state from map effects; apply queued effects later Met [F]: applyLiveControlEffects() after loadNodes(); probe with Heat saved OFF and clicked ON during init gives a heat layer after init and 0 page errors
Keep Heat/Matrix exclusion, filters, WebSocket/replay, SPA remount Met [F]: interlock mutants M4 and M8 are caught; probe with Heat+Matrix saved ON gives no heat layer; the existing Live E2Es are green (below)
Audio and geolocation handled separately Met [F]: left untouched, as the issue allows
At most one handler per control after remount Met [F]: E2E "three SPA round trips keep one listener per toggle"
Mutation check that fails if wiring moves behind the await Met [F]: M1 above

Test-first and mutants

  • [F] test-issue-125-live-toggles-wiring.js: master 0/4, commit A 0/4, head 4/0.
  • [F] test-issue-125-live-toggles-early-e2e.js against a local server with real Leaflet: master 2 passed, 12 failed; head 14 passed, 0 failed.
  • [F] Eight mutants: M1, M4, M7 (Heat handler does not update its mirror) and M8 are caught. M6 survives (finding 1). M2, M3 and M5 are equivalent in practice (finding 4).

Suites run locally

  • [F] Unit, same on master and the head: test-live.js 110/0, test-live-multibyte-filter.js 9/0, test-live-region-filter.js 13/0, test-live-legend-helper.js 5/0. test-live-anims.js gives 3/2 on both (pre-existing, not in CI).
  • [F] E2E on the head, against a local Go server on a freshened and migrated test-fixtures/e2e-fixture.db: test-live-multibyte-only-e2e.js 10/0, test-issue-1136-live-region-e2e.js 5/0, test-live-fullscreen-1572-e2e.js 9/0, test-live-mql-leak-1180-e2e.js 2/0, test-live-layout-1178-1179-e2e.js 10/0.

Performance and security

  • [F] Not a hot path. The same listeners as before, attached earlier; applyLiveControlEffects() runs once per mount.
  • [F] No new DOM sinks, and no Go, API or DB changes. mode=ro and the map[string]interface{} rule are not affected.

Not verified

  • Real mobile devices and staging. The E2E and my probes ran at 1400×900 only.
  • WebSocket/replay behaviour with live traffic during a held init. It was covered only indirectly by the existing Live E2Es.
  • I did not run scripts/check-xss-sinks.sh or eslint myself [T]. The PR's "Not verified" list is honest.

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