From 7da6860fdb79b411900b3c2324953ccf7882bdc4 Mon Sep 17 00:00:00 2001 From: you Date: Mon, 23 Mar 2026 00:08:41 +0000 Subject: [PATCH] Fix: per-observer hop resolution for packet list Ambiguous hops in the list now get resolved per-observer via batch server API calls. Cache uses observer-scoped keys (hop:observerId) so the same 1-byte prefix shows different names depending on which observer saw the packet. Flow: global resolve first (fast, covers unambiguous hops), then batch per-observer resolve for ambiguous ones only. --- public/index.html | 2 +- public/packets.js | 48 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/public/index.html b/public/index.html index 80f9a361..d751162c 100644 --- a/public/index.html +++ b/public/index.html @@ -86,7 +86,7 @@ - + diff --git a/public/packets.js b/public/packets.js index add58676..82464b8e 100644 --- a/public/packets.js +++ b/public/packets.js @@ -129,13 +129,16 @@ } } - function renderHop(h) { - return HopDisplay.renderHop(h, hopNameCache[h], { hexMode: showHexHashes }); + function renderHop(h, observerId) { + // Use per-packet cache key if observer context available (ambiguous hops differ by region) + const cacheKey = observerId ? h + ':' + observerId : h; + const entry = hopNameCache[cacheKey] || hopNameCache[h]; + return HopDisplay.renderHop(h, entry, { hexMode: showHexHashes }); } - function renderPath(hops) { + function renderPath(hops, observerId) { if (!hops || !hops.length) return '—'; - return hops.map(renderHop).join(''); + return hops.map(h => renderHop(h, observerId)).join(''); } let directPacketId = null; @@ -418,6 +421,32 @@ } if (allHops.size) await resolveHops([...allHops]); + // Per-observer batch resolve for ambiguous hops (context-aware disambiguation) + const hopsByObserver = {}; + for (const p of packets) { + if (!p.observer_id) continue; + try { + const path = JSON.parse(p.path_json || '[]'); + const ambiguous = path.filter(h => hopNameCache[h]?.ambiguous); + if (ambiguous.length) { + if (!hopsByObserver[p.observer_id]) hopsByObserver[p.observer_id] = new Set(); + ambiguous.forEach(h => hopsByObserver[p.observer_id].add(h)); + } + } catch {} + } + // Batch resolve — one API call per observer (typically 4-5 observers) + await Promise.all(Object.entries(hopsByObserver).map(async ([obsId, hopsSet]) => { + try { + const params = new URLSearchParams({ hops: [...hopsSet].join(','), observer: obsId }); + const result = await api(`/resolve-hops?${params}`); + if (result?.resolved) { + for (const [k, v] of Object.entries(result.resolved)) { + hopNameCache[k + ':' + obsId] = v; + } + } + } catch {} + })); + // Restore expanded group children if (groupByHash && expandedHashes.size > 0) { for (const hash of expandedHashes) { @@ -927,7 +956,7 @@ const groupRegion = headerObserverId ? (observers.find(o => o.id === headerObserverId)?.iata || '') : ''; let groupPath = []; try { groupPath = JSON.parse(headerPathJson || '[]'); } catch {} - const groupPathStr = renderPath(groupPath); + const groupPathStr = renderPath(groupPath, headerObserverId); const groupTypeName = payloadTypeName(p.payload_type); const groupTypeClass = payloadTypeColor(p.payload_type); const groupSize = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0; @@ -959,7 +988,7 @@ const childRegion = c.observer_id ? (observers.find(o => o.id === c.observer_id)?.iata || '') : ''; let childPath = []; try { childPath = JSON.parse(c.path_json || '[]'); } catch {} - const childPathStr = renderPath(childPath); + const childPathStr = renderPath(childPath, child.observer_id); html += ` ${childRegion ? `${childRegion}` : '—'} ${timeAgo(c.timestamp)} @@ -987,8 +1016,7 @@ const typeName = payloadTypeName(p.payload_type); const typeClass = payloadTypeColor(p.payload_type); const size = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0; - const pathStr = renderPath(pathHops); - const detail = getDetailPreview(decoded); + const pathStr = renderPath(pathHops, p.observer_id); const detail = getDetailPreview(decoded); return ` ${region ? `${region}` : '—'} @@ -1131,6 +1159,8 @@ if (serverResolved?.resolved) { for (const [k, v] of Object.entries(serverResolved.resolved)) { hopNameCache[k] = v; + // Also store observer-scoped key for list view + if (pkt.observer_id) hopNameCache[k + ':' + pkt.observer_id] = v; } } } catch {} @@ -1232,7 +1262,7 @@ ${hashSize ? `
Hash Size
${hashSize} byte${hashSize !== 1 ? 's' : ''}
` : ''}
Timestamp
${pkt.timestamp}
Propagation
${propagationHtml}
-
Path
${pathHops.length ? renderPath(pathHops) : '—'}
+
Path
${pathHops.length ? renderPath(pathHops, pkt.observer_id) : '—'}