diff --git a/public/map.js b/public/map.js index 370d117..445c408 100644 --- a/public/map.js +++ b/public/map.js @@ -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() { diff --git a/public/packets.js b/public/packets.js index 2a1a4f4..7c20922 100644 --- a/public/packets.js +++ b/public/packets.js @@ -65,8 +65,10 @@ function renderHop(h) { const name = hopNameCache[h]; - if (name) return '' + escapeHtml(name) + ''; - return '' + h + ''; + 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 `${display}`; } function renderPath(hops) { @@ -441,8 +443,9 @@
Route Type
${routeTypeName(pkt.route_type)}
Payload Type
${typeName}
Timestamp
${pkt.timestamp}
-
Path Hops
${decoded.path_len ?? pathHops.length}
+
Path
${pathHops.length ? renderPath(pathHops) : '—'}
+ ${pathHops.length ? `🗺️ View route on map` : ''} ${hasRawHex ? `
${buildHexLegend(ranges)}
${createColoredHexDump(pkt.raw_hex, ranges)}
` : ''} diff --git a/public/style.css b/public/style.css index cf50304..04a6f1f 100644 --- a/public/style.css +++ b/public/style.css @@ -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; +}