From d1743c6aaaab2449dae7f60a9a18538155eefdb4 Mon Sep 17 00:00:00 2001 From: dborup Date: Sun, 26 Jul 2026 08:20:13 +0200 Subject: [PATCH] fix: View Path "N approximate" counted chain appearances, not distinct nodes A shared entry-point repeater near the sender commonly appears in many branches' chains (one relay a dozen stations all heard the packet through). The status count summed every chain appearance instead of deduping by node identity, so a single approximate- position node showing up in 11 branches reported "11 approximate" -- dborup spotted this on a real packet and asked whether it could be right; it wasn't, it was the same node counted 11 times. Dedupes by publicKey (falling back to name when a point has none). --- public/packet-path-map.js | 14 ++++++++++++- test-packet-path-map.js | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/public/packet-path-map.js b/public/packet-path-map.js index dbc7d66e..7b2fca55 100644 --- a/public/packet-path-map.js +++ b/public/packet-path-map.js @@ -192,6 +192,12 @@ var bounds = []; var missingTotal = 0; var approxTotal = 0; + // The same physical node (e.g. a shared entry-point repeater near the + // sender) commonly appears in many branches' chains -- dedupe by + // identity so "N approximate" counts distinct stations, not chain + // appearances (#1... a packet heard by 12 stations through one shared + // repeater was showing "11 approximate" for what was really 1 node). + var approxSeen = {}; // Draw secondary branches first so the primary (deepest) one ends up on top. var ordered = plotted.slice().sort(function (a, b) { return (a.primary ? 1 : 0) - (b.primary ? 1 : 0); }); ordered.forEach(function (p) { @@ -199,7 +205,13 @@ var lineColor = p.primary ? accent : muted; var line = []; p.chain.forEach(function (pt) { - if (pt.approx) approxTotal++; + if (pt.approx) { + var approxKey = pt.publicKey || pt.name; + if (!approxSeen[approxKey]) { + approxSeen[approxKey] = true; + approxTotal++; + } + } bounds.push([pt.lat, pt.lon]); line.push([pt.lat, pt.lon]); var color = pt.isObserver ? observerColor : lineColor; diff --git a/test-packet-path-map.js b/test-packet-path-map.js index 75f7b1ee..ea73cdd6 100644 --- a/test-packet-path-map.js +++ b/test-packet-path-map.js @@ -320,6 +320,50 @@ function makeSandbox(apiImpl) { } catch (e) { failed++; console.log(' ❌ approximate (neighbor-borrowed) positions render hollow/dashed and are called out in status: ' + e.message); } })(); + await (async () => { + try { + // A shared entry-point node with an approximate position commonly + // appears in MANY branches' chains (e.g. one repeater near the + // sender that a dozen stations all relayed through). The status + // count must dedupe by identity -- 1 distinct approximate node + // showing up in 3 branches is "1 approximate", not "3". + const ctx = makeSandbox(() => Promise.resolve({ + hash: 'deadbeef', + branches: [ + { + hops: 2, + points: [{ publicKey: 'pk-shared', name: 'SharedRepeater', lat: 56.0, lon: 10.0, approx: true }], + observer: { name: 'ObserverA', lat: 56.1, lon: 10.1 }, + }, + { + hops: 1, + points: [{ publicKey: 'pk-shared', name: 'SharedRepeater', lat: 56.0, lon: 10.0, approx: true }], + observer: { name: 'ObserverB', lat: 56.2, lon: 10.2 }, + }, + { + hops: 1, + points: [{ publicKey: 'pk-shared', name: 'SharedRepeater', lat: 56.0, lon: 10.0, approx: true }], + observer: { name: 'ObserverC', lat: 56.3, lon: 10.3 }, + }, + ], + })); + + ctx.L = { + map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }), + tileLayer: () => ({ addTo() { return this; } }), + circleMarker: () => ({ addTo() { return this; }, bindTooltip() { return this; }, on() { return this; } }), + polyline: () => ({ addTo() { return this; } }), + }; + + await ctx.window.PacketPathMap.open('deadbeef'); + const status = ctx.document.getElementById('packetPathStatus'); + assert.ok(status.textContent.includes('1 approximate'), 'status should count the shared node once (1 approximate), not once per branch it appears in, got: ' + status.textContent); + assert.ok(!status.textContent.includes('3 approximate'), 'status should NOT count 3 -- that would be counting chain appearances, not distinct nodes, got: ' + status.textContent); + passed++; + console.log(' ✅ "N approximate" dedupes a shared node across branches instead of counting each chain appearance'); + } catch (e) { failed++; console.log(' ❌ "N approximate" dedupes a shared node across branches instead of counting each chain appearance: ' + e.message); } + })(); + await (async () => { try { // branch.secondsAfterFirst (0 for the earliest arrival, positive