diff --git a/app/static/js/path-analyzer.js b/app/static/js/path-analyzer.js index a8dd7be..a548610 100644 --- a/app/static/js/path-analyzer.js +++ b/app/static/js/path-analyzer.js @@ -502,6 +502,19 @@ function paRenderStats() { let paMap = null; let paBaseLayer = null; // repeater contact markers let paPathLayer = null; // drawn path for the selected echo + +// Path drawing color - distinct from the purple base markers so the +// route stands out (origin stays green, ambiguous candidates amber) +const PA_PATH_COLOR = '#dc3545'; + +function paHopIcon(n) { + return L.divIcon({ + className: 'pa-hop-icon', + html: `
${n}
`, + iconSize: [22, 22], + iconAnchor: [11, 11], + }); +} let paMapSelection = { msgId: null, echoIdx: null }; let paPicks = {}; // token -> public_key chosen by the user (collision disambiguation) @@ -560,16 +573,19 @@ function paDrawSelectedEcho() { if (origin) { L.circleMarker([origin.adv_lat, origin.adv_lon], { radius: 7, color: '#198754', weight: 2, fillOpacity: 0.8 - }).bindPopup(`${origin.name}
Origin (sender)`).addTo(paPathLayer); + }).bindPopup(`${origin.name}
Origin (sender)`) + .bindTooltip(origin.name, { permanent: true, direction: 'right', offset: [8, 0], className: 'pa-hop-label' }) + .addTo(paPathLayer); points.push({ latlng: [origin.adv_lat, origin.adv_lon], gapBefore: false }); } echo.tokens.forEach((tok, i) => { const { contact, candidates } = paResolveHop(tok); if (contact) { - L.circleMarker([contact.adv_lat, contact.adv_lon], { - radius: 7, color: '#6f42c1', weight: 2, fillOpacity: 0.9 - }).bindPopup(`${contact.name}
Hop ${i + 1}: ${tok}`).addTo(paPathLayer); + L.marker([contact.adv_lat, contact.adv_lon], { icon: paHopIcon(i + 1) }) + .bindPopup(`${contact.name}
Hop ${i + 1}: ${tok}`) + .bindTooltip(contact.name, { permanent: true, direction: 'right', offset: [13, 0], className: 'pa-hop-label' }) + .addTo(paPathLayer); points.push({ latlng: [contact.adv_lat, contact.adv_lon], gapBefore: gapPending }); gapPending = false; } else { @@ -586,7 +602,7 @@ function paDrawSelectedEcho() { // Polyline: solid between consecutive resolved hops, dashed across skipped ones for (let i = 1; i < points.length; i++) { L.polyline([points[i - 1].latlng, points[i].latlng], { - color: '#6f42c1', weight: 3, opacity: 0.8, + color: PA_PATH_COLOR, weight: 3, opacity: 0.85, dashArray: points[i].gapBefore ? '6 8' : null }).addTo(paPathLayer); } @@ -602,6 +618,24 @@ function paDrawSelectedEcho() { function paBuildLegend(echo) { const legend = document.createElement('div'); legend.className = 'pa-legend'; + + // Path-level reset for manual candidate assignments + if (echo.tokens.some(tok => paPicks[tok])) { + const resetRow = document.createElement('div'); + resetRow.className = 'text-end'; + const resetBtn = document.createElement('button'); + resetBtn.className = 'btn btn-sm btn-outline-warning py-0 pa-reset-picks'; + resetBtn.innerHTML = ' Reset picks'; + resetBtn.title = 'Clear all manual repeater assignments on this path'; + resetBtn.addEventListener('click', (e) => { + e.stopPropagation(); + echo.tokens.forEach(tok => { delete paPicks[tok]; }); + paRenderMapView(); + }); + resetRow.appendChild(resetBtn); + legend.appendChild(resetRow); + } + echo.tokens.forEach((tok, i) => { const row = document.createElement('div'); row.className = 'pa-legend-hop'; @@ -613,6 +647,18 @@ function paBuildLegend(echo) { const name = document.createElement('span'); name.textContent = '— ' + contact.name; row.appendChild(name); + if (paPicks[tok]) { + // Manually assigned - allow undoing just this hop + const undo = document.createElement('i'); + undo.className = 'bi bi-x-circle pa-unpick'; + undo.title = 'Undo this manual assignment'; + undo.addEventListener('click', (e) => { + e.stopPropagation(); + delete paPicks[tok]; + paRenderMapView(); + }); + row.appendChild(undo); + } } else if (candidates.length > 1) { const amb = document.createElement('span'); amb.className = 'pa-ambiguous'; diff --git a/app/templates/path-analyzer.html b/app/templates/path-analyzer.html index ffc72bf..01ed5db 100644 --- a/app/templates/path-analyzer.html +++ b/app/templates/path-analyzer.html @@ -166,6 +166,43 @@ color: #212529; } + .pa-unpick { + cursor: pointer; + color: var(--text-secondary, #6c757d); + } + + .pa-unpick:hover { + color: #dc3545; + } + + .pa-reset-picks { + font-size: 0.72rem; + } + + /* Numbered hop markers + name labels on the drawn path */ + .pa-hop-badge { + width: 22px; + height: 22px; + border-radius: 50%; + background-color: #dc3545; + color: #fff; + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + font-weight: 700; + } + + .pa-hop-label { + font-size: 0.7rem; + padding: 0 4px; + background: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: none; + } + /* Packet hash inside the expanded detail row - narrow screens only, where the Hash column is hidden */ .pa-detail-hash {