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: `
${tok}`).addTo(paPathLayer);
+ L.marker([contact.adv_lat, contact.adv_lon], { icon: paHopIcon(i + 1) })
+ .bindPopup(`${contact.name}${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 {