fix(map): make spiderfy work + stop auto-collapse on Map Analysis & Unified maps (#3685)#3687
Conversation
…s & Unified maps Two bugs left spiderfy non-functional on the Map Analysis view and the Unified/MeshCore (Dashboard) map, even though #3618 added the controller. 1) Registration timing (spiderfy never fired). React invokes a <Marker ref> callback during the commit phase, BEFORE useMarkerSpiderfier's init effect creates the OverlappingMarkerSpiderfier. Markers present at first mount (Map Analysis / Unified data is already cached on revisit) therefore tried to register while the instance was still null and were silently dropped, so clicking their pile never fanned out. The per-source NodesTab map only 'worked' by timing luck (its markers mount on a later render after async data loads). Fix: buffer markers added before init and flush them into the spiderfier the moment it's created. 2) Auto-collapse after a few seconds (#3685). On every poll the node lists rebuilt fresh icon objects and [lat,lng] position arrays. react-leaflet calls marker.setLatLng()/setIcon() whenever those prop references change, and doing so on a spiderfied marker snaps it back to its anchor, collapsing the fan. Fix: cache position+icon by value (per spiderfier key) in DashboardMap and NodeMarkersLayer so an unchanged marker keeps identical refs across refreshes and the fan persists — the same churn the NodesTab map avoids via a memoized marker. Adds a regression test that registers a marker in a layout effect (pre-init timing, like a real <Marker ref>) and asserts it lands in the spiderfier. Full suite: 7338 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Claude finished @Yeraze's task —— View job PR Review
This is a well-motivated and clearly explained fix for two distinct bugs. Overall the implementation is solid. Here are my observations: useMarkerSpiderfier.tsCorrectness — pending buffer: Minor: duplicate code path in addMarker (lines 222–249): Minor:
DashboardMap.tsx & NodeMarkersLayer.tsx
Icon signature in DashboardMap (line 445) omits const iconSig = `${hops}|${shortName ?? ''}|${isRouter ? 1 : 0}|${mapPinStyle}`;The
const markerKey = String(
node.sourceId != null && node.nodeNum != null
? `${node.sourceId}:${node.nodeNum}`
: nodeId ?? node.nodeNum,
);If Cache entries are cleaned up on marker unmount (lines 204–207 in DashboardMap, lines 96–99 in NodeMarkersLayer) — good. useMarkerSpiderfier.test.tsxTest is correctly structured and directly reproduces the bug: Coverage gaps (minor):
SummaryThe two bugs are correctly diagnosed and fixed. The buffering approach in No blocking issues found. The minor points above (duplicate branch in |
react-leaflet forwards its <Marker> ref via useImperativeHandle(ref, () => instance) with NO dependency array, so React bounces the parent ref callback null→instance on EVERY re-render — not just mount/unmount. The bridge's null branch treated that as a removal and called removeMarker on a still-present, often spiderfied marker; OMS auto-unspiderfies when a spiderfied marker is removed, so any selection change (Map Analysis) or polled-data refresh (Unified) instantly collapsed the fan (#3685). Fix: register on an instance only (addMarker is idempotent) and ignore the null bounce. Genuine removals are reconciled by a new effect keyed off the rendered key set, so a node that ages out / is filtered still unregisters. Applied to both NodeMarkersLayer (Map Analysis) and DashboardMap (Unified). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
Update — root-caused the Map Analysis collapse and fixed it (live-verified)Live testing against a dev container revealed that the registration + stable-ref changes were necessary but not sufficient: spiderfy registered correctly everywhere, but the fan still collapsed — on Map Analysis instantly on click, on Unified on the next data poll. Real root cause: react-leaflet forwards its Fix (commit 1a17e28): register on an instance only ( Live verification (dev container, real DOM clicks):
The PR title now accurately reflects the behavior on both surfaces. |
|
Claude finished @Yeraze's task —— View job PR Review
This PR correctly diagnoses and fixes three interrelated bugs in the spiderfy stack. The root cause analysis is accurate, the fixes are well-targeted, and the live-verification results are compelling. Here's the full review: useMarkerSpiderfier.tsPending buffer (lines 112–158) — correct and well-structured.
Idempotency guard short-circuits correctly (line 209):
DashboardMap.tsxNull-bounce fix (lines 196–209) — correct. Reconciliation effect (lines 318–329):
const markerKey = String(
node.sourceId != null && node.nodeNum != null
? `${node.sourceId}:${node.nodeNum}`
: nodeId ?? node.nodeNum,
);If both
Cache cleanup on removal (lines 324–327) — good.
NodeMarkersLayer.tsxNull-bounce fix (lines 88–103) — correctly mirrors DashboardMap.
Reconciliation effect includes useMarkerSpiderfier.test.tsxTest correctly reproduces the timing bug: Deduplication test (lines 95–105) is useful: Missing test: pending-marker removal. Missing test: null-bounce resilience. SummaryThe three bugs are correctly identified and the fixes are sound. The pending-buffer approach in Items worth a follow-up:
No blocking issues. The live-verified behavior is the strongest signal here. |
Two Map-Analysis-specific issues surfaced in live testing:
1. MeshCore markers never spiderfied. NodeMarkersLayer keyed markers by
`${sourceId}:${nodeNum}`, but every MeshCore node has nodeNum 0, so all
MeshCore nodes in a source collapsed onto one key (`…:0`) — only one
registered with the spiderfier, so their piles never fanned. Meshtastic/MQTT
nodes have real nodeNums and were unaffected. Key MeshCore nodes by their
unique nodeId instead (matching DashboardMap, which already did this).
Verified live: a co-located MeshCore group that registered 1 marker before
now registers all of them and fans on click.
2. Detail popup rendered under the markers. MapAnalysisCanvas wraps the layer
in <Pane name="markers" zIndex=600>, so the <Popup> inherited that pane
instead of Leaflet's default popupPane (z700) and markers painted over it.
Pin the popup to popupPane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
The dedicated per-MeshCore-source map (MeshCoreMap, shown under a MeshCore source's Nodes view) had no spiderfier at all, so co-located contacts piled up and couldn't be individually selected. Add the same SpiderfierController bridge the other maps use, with the robust marker registration (ignore react-leaflet's null ref-bounce, reconcile genuine removals from the rendered key set, keep position/icon refs stable). Markers key on publicKey (already unique here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Claude finished @Yeraze's task —— View job PR Review
This is a solid, well-motivated fix for three interrelated spiderfy bugs. The root cause analysis is thorough and the comments in the code are some of the best I've seen for explaining react-leaflet internals. Here's my detailed review: useMarkerSpiderfier.tsBug 1 fix (pending buffer) — correct. Idempotency guard (line 209) — critical for Bug 3 fix.
DashboardMap.tsxBug 3 fix (null-bounce, lines 196–209) — correct and well-explained.
Cache cleanup on removal (lines 324–327) — good. NodeMarkersLayer.tsx
MeshCore key fallback (line 116). Reconciliation effect includes MeshCoreMap.tsxConsistent bridge pattern — good.
useMarkerSpiderfier.test.tsxTest correctly reproduces the timing bug. Deduplication test (lines 95–105) — validates the idempotency guard. Missing coverage:
SummaryNo blocking issues. The three bugs are correctly diagnosed and the fixes are sound. The pending-buffer approach in Items worth addressing (ordered by risk):
|
Adding the spiderfier to MeshCoreMap means MeshCoreSourcePage.test (which renders it with a real react-leaflet map) now constructs an OverlappingMarkerSpiderfier. The package's `main` is a UMD bundle whose named export isn't a constructor once Vitest externalizes it, so the test threw 'OverlappingMarkerSpiderfier is not a constructor' on CI. Alias the package to its ESM `dist/omsleaflet.js` (real class export) for Vitest only — the app build already uses the ESM entry, so it's unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Claude finished @Yeraze's task —— View job PR Review
This is a well-structured fix addressing three interrelated bugs in the spiderfy stack. The root cause analysis is solid and the code is well-commented. Here are my findings: useMarkerSpiderfier.ts — Pending Buffer (Bug 1 fix)Correct and well-implemented. The Dead code in // Before
if (isSamePosition) {
try { spiderfierRef.current.removeMarker(existingMarker); ... } catch (e) { }
} else {
try { spiderfierRef.current.removeMarker(existingMarker); ... } catch (e) { console.warn(...); }
}
// After: single block, always warn (or never warn)This is cosmetic only — no behavioral difference.
O(n) reverse-lookup in DashboardMap.tsx — Null-Bounce Fix (Bug 3)Null-bounce guard is correct (lines 196–209). The comment explaining why
// Safer approach
const renderedKeySet = useMemo(() =>
new Set(nodesWithPosition.map(({ node }) => computeMarkerKey(node))),
[nodesWithPosition]);
Cache cleanup in reconciliation effect (lines 324–327) — good. Both NodeMarkersLayer.tsx — Consistent bridge patternWell-implemented and consistent with DashboardMap. The
MeshCoreMap.tsx — Consistent pattern applied
Key/spiderfier alignment (lines 345–346): Both MeshCore public key safety in useMarkerSpiderfier.test.tsx — Test qualityWell-designed test that directly targets the bug. Using Missing: pending-removal test. A marker added pre-init that is then removed before the spiderfier initializes should NOT appear in the OMS after flush. The code path exists (lines 277–282 of the hook) but has no test. Adding this would guard a subtle edge case: it('does not flush a marker removed before init', () => {
// add marker in layout effect, remove immediately before passive effect runs
// assert oms.markers.length === 0
});Missing: null-bounce resilience test (Bug 3). As noted in the PR, this is harder to unit-test due to react-leaflet internals. A comment in the test file acknowledging this gap would help future maintainers understand what's intentionally uncovered. vitest.config.tsESM alias fix is correct and well-explained. The comment at lines 56–64 accurately describes the UMD vs. ESM constructor issue and why the alias is needed. This is a clean solution that doesn't affect the app build. SummaryThe three bugs are correctly diagnosed and the fixes are sound. Live-verification results with a real dev container are the strongest signal here. Items worth addressing (ordered by risk):
No blocking issues found. |
Summary
Spiderfy (clicking a pile of overlapping markers to fan them apart) was non-functional on the Map Analysis view and the Unified/MeshCore (Dashboard) map, and on the Unified map it would auto-collapse a few seconds after fanning out. PR #3618 added the shared
SpiderfierController, but two bugs prevented it from actually working on those surfaces.Bug 1 — registration never happened (spiderfy didn't fire)
React invokes a
<Marker ref>callback during the commit phase, which runs beforeuseMarkerSpiderfier's inituseEffectthat constructs theOverlappingMarkerSpiderfier. Any marker present at first mount — which is the norm on Map Analysis / Unified, whose node data is already cached on revisit — tried to register whilespiderfierRef.currentwas stillnulland was silently dropped. Clicking its pile then never fanned out.The per-source NodesTab map only appeared to work by timing luck: its markers mount on a later render after async data loads, by which point the spiderfier already exists.
Fix:
useMarkerSpiderfiernow buffers markers added before init and flushes them into the spiderfier the moment it's created (src/hooks/useMarkerSpiderfier.ts). Surface-agnostic, so all three maps benefit.Bug 2 — fan auto-collapsed on refresh (#3685)
On every poll the marker lists rebuilt fresh
iconobjects and[lat,lng]positionarrays. react-leaflet callsmarker.setLatLng()/setIcon()whenever those prop references change, and doing so on a spiderfied marker snaps it back to its anchor — collapsing the fan.Fix:
DashboardMapandNodeMarkersLayernow cacheposition+iconby value (per spiderfier key), so an unchanged marker keeps identical refs across refreshes and the fan persists. This is the same churn the NodesTab map avoids via a memoized marker.Testing
src/hooks/useMarkerSpiderfier.test.tsx) registers a marker in auseLayoutEffect— reproducing the pre-init commit-phase timing of a real<Marker ref>— and asserts it lands in the spiderfier. Before the fix it was dropped. (The prior tests all mocked the spiderfier out, which is why this shipped green.)Fixes #3685. Restores the spiderfy behavior intended by #3612 on Map Analysis and the Unified/MeshCore map.
🤖 Generated with Claude Code