diff --git a/src/index.js b/src/index.js index 91faa31..c37fc53 100644 --- a/src/index.js +++ b/src/index.js @@ -616,54 +616,68 @@ app.get('/api/v1/traceroutes', async (req, res) => { return trace; }); - // Build edges from the forward (towards) path using snr_towards - // The forward path is: to (initiator) → route[0] → route[1] → ... → from (responder?) - // snr_towards holds SNR per hop along that path. - // We only care about neighbor-like edges between consecutive hops with their SNR and updated_at. - const edgeKey = (a, b) => `${String(a)}->${String(b)}`; // directional; map layer can choose how to render + // Build edges from both forward (towards) and reverse (back) paths. + // Forward path: to → route[] → from, using snr_towards + // Reverse path: from → route_back[] → to, using snr_back + const edgeKey = (a, b) => `${String(a)}->${String(b)}`; const edges = new Map(); - for (const tr of normalized) { - const path = []; - if (tr.to != null) path.push(Number(tr.to)); - if (Array.isArray(tr.route)) { - for (const hop of tr.route) { - if (hop != null) path.push(Number(hop)); - } - } - if (tr.from != null) path.push(Number(tr.from)); + function upsertEdgesFromPath(trace, pathNodes, pathSnrs) { + for (let i = 0; i < pathNodes.length - 1; i++) { + const hopFrom = pathNodes[i]; + const hopTo = pathNodes[i + 1]; + const snr = typeof (pathSnrs && pathSnrs[i]) === 'number' ? pathSnrs[i] : null; - const snrs = Array.isArray(tr.snr_towards) ? tr.snr_towards : []; - - for (let i = 0; i < path.length - 1; i++) { - const fromNode = path[i]; - const toNode = path[i + 1]; - // snr_towards aligns to hops; guard if missing - const snr = typeof snrs[i] === 'number' ? snrs[i] : null; - - const key = edgeKey(fromNode, toNode); + const key = edgeKey(hopFrom, hopTo); const existing = edges.get(key); if (!existing) { edges.set(key, { - from: fromNode, - to: toNode, + from: hopFrom, + to: hopTo, snr: snr, - updated_at: tr.updated_at, - channel_id: tr.channel_id ?? null, - gateway_id: tr.gateway_id ?? null, + updated_at: trace.updated_at, + channel_id: trace.channel_id ?? null, + gateway_id: trace.gateway_id ?? null, + traceroute_from: trace.from, // original initiator + traceroute_to: trace.to, // original target }); - } else { - // Deduplicate by keeping the most recent updated_at - if (new Date(tr.updated_at) > new Date(existing.updated_at)) { - existing.snr = snr; - existing.updated_at = tr.updated_at; - existing.channel_id = tr.channel_id ?? existing.channel_id; - existing.gateway_id = tr.gateway_id ?? existing.gateway_id; - } + } else if (new Date(trace.updated_at) > new Date(existing.updated_at)) { + existing.snr = snr; + existing.updated_at = trace.updated_at; + existing.channel_id = trace.channel_id ?? existing.channel_id; + existing.gateway_id = trace.gateway_id ?? existing.gateway_id; + existing.traceroute_from = trace.from; + existing.traceroute_to = trace.to; } } } + for (const tr of normalized) { + // Forward path + const forwardPath = []; + if (tr.to != null) forwardPath.push(Number(tr.to)); + if (Array.isArray(tr.route)) { + for (const hop of tr.route) { + if (hop != null) forwardPath.push(Number(hop)); + } + } + if (tr.from != null) forwardPath.push(Number(tr.from)); + const forwardSnrs = Array.isArray(tr.snr_towards) ? tr.snr_towards : []; + upsertEdgesFromPath(tr, forwardPath, forwardSnrs); + + // Reverse path + const reversePath = []; + if (tr.from != null) reversePath.push(Number(tr.from)); + if (Array.isArray(tr.route_back)) { + for (const hop of tr.route_back) { + if (hop != null) reversePath.push(Number(hop)); + } + } + if (tr.to != null) reversePath.push(Number(tr.to)); + const reverseSnrs = Array.isArray(tr.snr_back) ? tr.snr_back : []; + upsertEdgesFromPath(tr, reversePath, reverseSnrs); + } + res.json({ traceroute_edges: Array.from(edges.values()), }); diff --git a/src/public/index.html b/src/public/index.html index 8692d02..49427e9 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -3119,11 +3119,15 @@ distance = `${km} kilometers`; } const terrainImageUrl = getTerrainProfileImage(fromNode, toNode); + const targetNode = edge.traceroute_from ? findNodeById(edge.traceroute_from) : null; + const initiatorNode = edge.traceroute_to ? findNodeById(edge.traceroute_to) : null; return `Traceroute hop` + `
from [${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}` + ` to [${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}` + `
SNR: ${snrDb != null ? snrDb + 'dB' : '?'}` + `
Distance: ${distance}` + + (initiatorNode ? `
Traceroute from: [${escapeString(initiatorNode.short_name)}] ${escapeString(initiatorNode.long_name)}` : '') + + (targetNode ? `
Traceroute to: [${escapeString(targetNode.short_name)}] ${escapeString(targetNode.long_name)}` : '') + `

Terrain images from HeyWhatsThat.com` + `
`; })(); @@ -3283,11 +3287,15 @@ distance = `${km} kilometers`; } const terrainImageUrl = getTerrainProfileImage(fromNode, toNode); + const targetNode = edge.traceroute_from ? findNodeById(edge.traceroute_from) : null; + const initiatorNode = edge.traceroute_to ? findNodeById(edge.traceroute_to) : null; return `Traceroute hop` + `
from [${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}` + ` to [${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}` + `
SNR: ${snrDb != null ? snrDb + 'dB' : '?'}` + `
Distance: ${distance}` + + (initiatorNode ? `
Traceroute from: [${escapeString(initiatorNode.short_name)}] ${escapeString(initiatorNode.long_name)}` : '') + + (targetNode ? `
Traceroute to: [${escapeString(targetNode.short_name)}] ${escapeString(targetNode.long_name)}` : '') + `

Terrain images from HeyWhatsThat.com` + `
`; })(); @@ -3789,11 +3797,17 @@ const terrainImageUrl = getTerrainProfileImage(fromNode, toNode); - const tooltip = `Traceroute hop` + // This is backwards. It's because the traceroute packet is sent from the target node. + const targetNode = edge.traceroute_from ? findNodeById(edge.traceroute_from) : null; + const initiatorNode = edge.traceroute_to ? findNodeById(edge.traceroute_to) : null; + + const tooltip = `Traceroute hop` + `
from [${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}` + ` to [${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}` + `
SNR: ${snrDb != null ? snrDb + 'dB' : '?'}` + `
Distance: ${distance}` + + (initiatorNode ? `
Traceroute from: [${escapeString(initiatorNode.short_name)}] ${escapeString(initiatorNode.long_name)}` : '') + + (targetNode ? `
Traceroute to: [${escapeString(targetNode.short_name)}] ${escapeString(targetNode.long_name)}` : '') + (edge.updated_at ? `
Updated: ${moment(new Date(edge.updated_at)).fromNow()}` : '') + (edge.channel_id ? `
Channel: ${edge.channel_id}` : '') + `

Terrain images from HeyWhatsThat.com`