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).
This commit is contained in:
dborup committed 2026-07-26 08:20:13 +02:00
1 parent 4652162149
commit d1743c6aaa
2 files changed
+57 -1

No files matched your search

+13 -1
View File
@@ -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;
+44
View File
@@ -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