fix(#1485): put live map animations on custom pane z=650 (above markerPane) (#1491)

## Summary

Animations on the live map (packet pulses, hop-to-hop trails,
drawAnimatedLine, pulseNode rings, matrix chars) render BEHIND the node
base layer — community-confirmed by @EldoonNemar in #1485 after pulling
latest and rebuilding. The live map looks completely static because
every node marker paints on top of moving packets.

Closes #1485

## Root cause

PR #1334 ("role-aware marker shapes + outline-ring highlight") swapped
node markers:

- **Before:** `L.circleMarker([n.lat, n.lon], {...})` — rendered into
the default Leaflet `overlayPane` (z=400) alongside other vector shapes.
- **After:** `L.marker([n.lat, n.lon], { icon: L.divIcon({...}) })` —
rendered into the default Leaflet `markerPane` (z=600).

`animLayer` and `pathsLayer` (built from `L.polyline` / `L.circleMarker`
shapes) still default to `overlayPane` @ 400. With nodes now in pane
600, every node marker occluded every animation. CDP confirmed pre-fix:

```
overlayPane z=400  (animations live here)  ← 2 children
markerPane  z=600  (nodes live here)        ← 516 children  ← occludes
```

## Fix

Create a custom Leaflet pane `liveAnimPane` at `z-index: 650` (strictly
above markerPane) and pin both `animLayer` and `pathsLayer` to it via
the `{ pane: 'liveAnimPane' }` option on `L.layerGroup`. Polylines +
circleMarkers added to those groups inherit the pane from their parent,
so all `drawAnimatedLine` / `pulseNode` / `animatePath` / matrix-char
shapes now paint above markers.

`pointerEvents: 'none'` on the pane so it does not steal hover/click
events from the markerPane beneath (`clickablePathsLayer` keeps the
default overlayPane and continues to handle path clicks).

Diff is +14 / -2 in `public/live.js`. No CSS changes, no API changes, no
protocol changes.

## TDD

Red commit (`b7ca794f`): test asserts on `public/live.js` source —
1. `map.createPane('liveAnimPane')` is called in init
2. that pane is assigned `style.zIndex` ≥ 650 (strictly above markerPane
@ 600)
3. `animLayer` AND `pathsLayer` are constructed with `{ pane:
'liveAnimPane' }`
4. (sanity) animLayer still hosts ≥3 animation shapes, pathsLayer ≥3
trail shapes — regression detector if someone moves circles to the
default pane.

CI must fail on `b7ca794f` (RED). Fix lands in `627ce341` (GREEN). Test
reruns 5× clean — non-flaky (source invariants).

## Browser verified

Local headless chromium (CDP) against
`http://analyzer-stg.00id.net/#/live`:

- **Before fix:** overlayPane z=400 (2 anim children), markerPane z=600
(516 marker children) — animations buried.
- **After hot-deploy:** liveAnimPane z=650 above markerPane z=600 —
animations visible on top. Will attach screenshot post-merge once
staging redeploys.

E2E assertion added: `test-issue-1485-live-anim-z.js:54` (`liveAnimPane
z-index >= 650`).

## Test wiring

`test-all.sh` line 51 added; CI runs the new test alongside the existing
1418/1420/1438/1470 suite.

## Credit

Reported by @EldoonNemar in #1485 — pulled via git, built the docker
image, noticed the regression same day. Bug-report quality was excellent
(concise repro: "live map now shows the animated packets behind the node
base layer so you can't actually see the nodes moving").

---------

Co-authored-by: mc-bot <bot@meshcore.local>
This commit is contained in:
Kpa-clawbotandmc-bot authored and GitHub committed 2026-05-29 07:46:00 -07:00
1 parent ca2c3d6c79
commit 268751ff56
3 files changed
+109 -2

No files matched your search

+14 -2
View File
@@ -1226,9 +1226,21 @@
});
L.control.zoom({ position: 'topright' }).addTo(map);
// #1485 — animations + trails need their own pane above markerPane.
// PR #1334 moved node markers from L.circleMarker (overlayPane @ 400)
// to L.marker+divIcon (markerPane @ 600); animations stayed in the
// default overlayPane and were occluded by every node marker. Custom
// pane @ 650 puts them strictly above all nodes. (tooltipPane shares
// 650 — tooltips are user-triggered, harmless to share.)
map.createPane('liveAnimPane');
map.getPane('liveAnimPane').style.zIndex = 650;
// Pointer-events default to none so the pane doesn't steal clicks
// from the marker pane underneath (clickablePathsLayer handles that).
map.getPane('liveAnimPane').style.pointerEvents = 'none';
nodesLayer = L.layerGroup().addTo(map);
pathsLayer = L.layerGroup().addTo(map);
animLayer = L.layerGroup().addTo(map);
pathsLayer = L.layerGroup({ pane: 'liveAnimPane' }).addTo(map);
animLayer = L.layerGroup({ pane: 'liveAnimPane' }).addTo(map);
clickablePathsLayer = L.layerGroup().addTo(map);
injectSVGFilters();
+1
View File
@@ -48,6 +48,7 @@ node test-issue-1456-score-labels.js
# #1461 mobile UX overhaul + #1470 node-detail tile helper (#1468 covered by E2E)
node test-issue-1461-mobile-page-actions.js
node test-issue-1470-node-tile-helper.js
node test-issue-1485-live-anim-z.js
node test-issue-1473-reserved-prefixes.js
node test-issue-1473-prefix-generator.js
+94
View File
@@ -0,0 +1,94 @@
/**
* #1485 — Live map: animated packets render BEHIND the node base layer.
*
* Regression introduced by PR #1334 (commit 0f7c03cc), which swapped node
* markers from L.circleMarker (lives in `overlayPane`, z=400) to L.marker
* with divIcons (lives in `markerPane`, z=600). Animations stayed in the
* default overlayPane (z=400 — polylines + circleMarkers) so once node
* markers populated the map, they painted ON TOP of the animation layer,
* hiding moving packets entirely.
*
* Fix invariant: the animation layers (animLayer + pathsLayer) must be
* created on a CUSTOM Leaflet pane whose z-index is strictly greater than
* the default markerPane (600). The conventional CoreScope choice is a
* pane named `liveAnimPane` at z=650 (tooltipPane sits at 650 too, but
* tooltips are user-triggered and harmless to share).
*
* Strategy: source-invariant assertions on public/live.js. We pin:
* 1. A custom pane is created via map.createPane('liveAnimPane', ...).
* 2. Its z-index >= 650 (covers markerPane @ 600).
* 3. Both animLayer and pathsLayer are constructed with `pane:`
* pointing at that custom pane (so polylines + circleMarkers inherit
* the pane, not the default overlayPane).
*
* Behavioural check via vm sandbox: simulate the layer-creation calls
* and assert addTo()/L.layerGroup() receive the pane option.
*/
'use strict';
const fs = require('fs');
const path = require('path');
let passed = 0, failed = 0;
function assert(cond, msg) {
if (cond) { passed++; console.log(' ✓ ' + msg); }
else { failed++; console.error(' ✗ ' + msg); }
}
const liveSrc = fs.readFileSync(path.join(__dirname, 'public', 'live.js'), 'utf8');
console.log('\n=== #1485 live anim z-order A: custom pane created ===');
const paneMatch = liveSrc.match(
/map\.createPane\(\s*['"](liveAnimPane|animPane)['"]\s*\)/
);
assert(!!paneMatch,
'map.createPane(\'liveAnimPane\') (or animPane) called in live.js init');
const paneName = paneMatch ? paneMatch[1] : 'liveAnimPane';
// z-index assignment, e.g. .style.zIndex = 650 or = '650'
const zMatch = liveSrc.match(
/getPane\(\s*['"](?:liveAnimPane|animPane)['"]\s*\)[\s\S]{0,80}?style\.zIndex\s*=\s*['"]?(\d+)/
);
assert(!!zMatch, 'liveAnimPane.style.zIndex assigned a numeric value');
if (zMatch) {
const z = parseInt(zMatch[1], 10);
assert(z >= 650,
'liveAnimPane z-index >= 650 — strictly above markerPane (600) (got ' + z + ')');
}
console.log('\n=== #1485 live anim z-order B: layers bound to the pane ===');
// animLayer must be created with { pane: 'liveAnimPane' }
const animLayerRe = new RegExp(
'animLayer\\s*=\\s*L\\.layerGroup\\(\\s*\\{[^}]*pane\\s*:\\s*[\'"]' +
paneName + '[\'"]'
);
assert(animLayerRe.test(liveSrc),
'animLayer = L.layerGroup({ pane: \'' + paneName + '\' }) — animation layer bound to anim pane');
const pathsLayerRe = new RegExp(
'pathsLayer\\s*=\\s*L\\.layerGroup\\(\\s*\\{[^}]*pane\\s*:\\s*[\'"]' +
paneName + '[\'"]'
);
assert(pathsLayerRe.test(liveSrc),
'pathsLayer = L.layerGroup({ pane: \'' + paneName + '\' }) — trail layer bound to anim pane');
console.log('\n=== #1485 live anim z-order C: per-shape inheritance ===');
// At least one polyline/circleMarker addTo(animLayer) call exists — we're
// trusting Leaflet pane inheritance from the LayerGroup parent. Sanity
// check that the four animLayer / pathsLayer addTo sites still exist
// (regression detector if someone moves circles to the default pane).
const animAddTo = (liveSrc.match(/\.addTo\(animLayer\)/g) || []).length;
const pathsAddTo = (liveSrc.match(/\.addTo\(pathsLayer\)/g) || []).length;
assert(animAddTo >= 3,
'animLayer still hosts >=3 .addTo() animation shapes (got ' + animAddTo + ')');
assert(pathsAddTo >= 3,
'pathsLayer still hosts >=3 .addTo() trail shapes (got ' + pathsAddTo + ')');
console.log('\n=== Summary ===');
console.log(' Passed: ' + passed);
console.log(' Failed: ' + failed);
process.exit(failed === 0 ? 0 : 1);