Clickable hop links + View Route on Map from packet detail

- Hops in packet table are now clickable links to node detail (#/nodes/<hop>)
- Packet detail panel shows 'View route on map' link for packets with hops
- Map page reads ?highlight= query param and draws dashed route polyline
- Route shows numbered markers: green=origin, amber=hops, red=destination
- Map auto-fits to route bounds
This commit is contained in:
you
2026-03-18 22:36:43 +00:00
parent 58ab38d5f5
commit df87f24b7f
3 changed files with 80 additions and 4 deletions
+40 -1
View File
@@ -87,7 +87,46 @@
};
onWS(wsHandler);
loadNodes();
loadNodes().then(() => {
// Check for highlight route param (from packet detail)
const hashQuery = location.hash.split('?')[1];
if (hashQuery) {
const params = new URLSearchParams(hashQuery);
const highlight = params.get('highlight');
if (highlight) drawPacketRoute(highlight.split(','));
}
});
}
function drawPacketRoute(hopKeys) {
// Resolve hop keys to positions
const positions = [];
for (const hop of hopKeys) {
const node = nodes.find(n =>
n.public_key.toLowerCase().startsWith(hop.toLowerCase())
);
if (node && node.lat != null && node.lon != null && !(node.lat === 0 && node.lon === 0)) {
positions.push({ lat: node.lat, lon: node.lon, name: node.name || hop });
}
}
if (positions.length < 2) return;
// Draw route polyline
const coords = positions.map(p => [p.lat, p.lon]);
const routeLine = L.polyline(coords, {
color: '#f59e0b', weight: 3, opacity: 0.8, dashArray: '8 4'
}).addTo(markerLayer);
// Add numbered markers at each hop
positions.forEach((p, i) => {
L.circleMarker([p.lat, p.lon], {
radius: 8, fillColor: i === 0 ? '#22c55e' : i === positions.length - 1 ? '#ef4444' : '#f59e0b',
fillOpacity: 0.9, color: '#fff', weight: 2
}).addTo(markerLayer).bindTooltip(`${i + 1}. ${p.name}`, { permanent: true, direction: 'top', className: 'route-tooltip' });
});
// Fit map to route
map.fitBounds(L.latLngBounds(coords).pad(0.2));
}
async function loadNodes() {
+6 -3
View File
@@ -65,8 +65,10 @@
function renderHop(h) {
const name = hopNameCache[h];
if (name) return '<span class="hop hop-named" title="' + h + '">' + escapeHtml(name) + '</span>';
return '<span class="hop">' + h + '</span>';
const display = name ? escapeHtml(name) : h;
const pubkey = name ? Object.entries(hopNameCache).find(([k,v]) => v === name)?.[0] || h : h;
// Try to find full pubkey from nodeData
return `<a class="hop hop-link ${name ? 'hop-named' : ''}" href="#/nodes/${encodeURIComponent(h)}" title="${h}" onclick="event.stopPropagation()">${display}</a>`;
}
function renderPath(hops) {
@@ -441,8 +443,9 @@
<dt>Route Type</dt><dd>${routeTypeName(pkt.route_type)}</dd>
<dt>Payload Type</dt><dd><span class="badge badge-${payloadTypeColor(pkt.payload_type)}">${typeName}</span></dd>
<dt>Timestamp</dt><dd>${pkt.timestamp}</dd>
<dt>Path Hops</dt><dd>${decoded.path_len ?? pathHops.length}</dd>
<dt>Path</dt><dd>${pathHops.length ? renderPath(pathHops) : '—'}</dd>
</dl>
${pathHops.length ? `<a class="detail-map-link" href="#/map?highlight=${encodeURIComponent(pathHops.join(','))}&packet=${pkt.hash || pkt.id}" onclick="event.stopPropagation()">🗺️ View route on map</a>` : ''}
${hasRawHex ? `<div class="hex-legend">${buildHexLegend(ranges)}</div>
<div class="hex-dump">${createColoredHexDump(pkt.raw_hex, ranges)}</div>` : ''}
+34
View File
@@ -1108,3 +1108,37 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
transition: background 0.1s;
}
.node-filter-option:hover { background: var(--surface-2, rgba(255,255,255,0.08)); }
/* Clickable hop links */
.hop-link {
color: var(--primary, #3b82f6);
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}
.hop-link:hover { color: var(--accent, #60a5fa); text-decoration: underline; }
/* Detail map link */
.detail-map-link {
display: inline-block;
margin: 8px 0;
padding: 6px 12px;
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.25);
color: #fbbf24;
border-radius: 6px;
font-size: 0.82rem;
font-weight: 600;
text-decoration: none;
transition: background 0.15s;
}
.detail-map-link:hover { background: rgba(245, 158, 11, 0.25); }
/* Route tooltip on map */
.route-tooltip {
background: rgba(0,0,0,0.8) !important;
color: #fbbf24 !important;
border: 1px solid rgba(245,158,11,0.3) !important;
font-weight: 600;
font-size: 0.75rem;
}