From 9574a7ebee2637e35b4b397fb8aad84be05a6255 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 19 Jul 2026 19:36:11 +0200 Subject: [PATCH] feat(pathanalyzer): jump-to-map from echo rows, message preview in map sidebar - Each routed echo line in the Messages table gets a map icon that switches to the Map view with that message + echo pre-selected and its path drawn; the sidebar scrolls the selection into view - Map sidebar entries show the message content under the sender: one truncated line normally, full text when the message is selected; sidebar entry markup switched from innerHTML to DOM building so network-supplied sender/content strings are never parsed as HTML - Fix: path drawing (fitBounds) now runs after invalidateSize in the same deferred step - drawing immediately after unhiding the map container computed bounds against a stale size and left the map at world zoom when jumping from the table Verified live via Playwright: jump selects the right echo (route chips match the sidebar entry), path draws at zoom 10, previews truncate on unselected entries and expand on the selected one. Co-Authored-By: Claude Fable 5 --- app/static/js/path-analyzer.js | 66 +++++++++++++++++++++++++------- app/templates/path-analyzer.html | 16 ++++++++ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/app/static/js/path-analyzer.js b/app/static/js/path-analyzer.js index d7458c5..c224c26 100644 --- a/app/static/js/path-analyzer.js +++ b/app/static/js/path-analyzer.js @@ -163,7 +163,7 @@ function paCopyText(text, label) { ); } -function paBuildEchoLine(echo) { +function paBuildEchoLine(msg, echo, echoIdx) { const line = document.createElement('div'); line.className = 'pa-echo-line'; line.title = 'Click to copy route'; @@ -203,6 +203,18 @@ function paBuildEchoLine(echo) { meta.textContent = `SNR: ${snr} | ${echo.received_at || ''}`; line.appendChild(meta); + if (echo.hops > 0) { + const mapBtn = document.createElement('i'); + mapBtn.className = 'bi bi-map pa-echo-mapbtn'; + mapBtn.title = 'Show this path on the map'; + mapBtn.addEventListener('click', (e) => { + e.stopPropagation(); + paMapSelection = { msgId: msg.id, echoIdx: echoIdx }; + paSwitchView('map'); + }); + line.appendChild(mapBtn); + } + line.addEventListener('click', () => { paCopyText(echo.tokens.join(','), 'Route'); }); @@ -284,9 +296,9 @@ function paRenderTable() { const detailTd = document.createElement('td'); detailTd.className = 'pa-echo-cell'; detailTd.colSpan = 9; - for (const echo of msg.echoView) { - detailTd.appendChild(paBuildEchoLine(echo)); - } + msg.echoView.forEach((echo, echoIdx) => { + detailTd.appendChild(paBuildEchoLine(msg, echo, echoIdx)); + }); detailTr.appendChild(detailTd); body.appendChild(detailTr); @@ -603,19 +615,37 @@ function paRenderMapView() { } for (const msg of filtered) { + const selected = msg.id === paMapSelection.msgId; const entry = document.createElement('div'); - entry.className = 'pa-map-msg' + (msg.id === paMapSelection.msgId ? ' pa-selected' : ''); - entry.innerHTML = - `
- ${msg.sender || '—'} - ${paFormatTime(msg).slice(5, 16)} -
`; + entry.className = 'pa-map-msg' + (selected ? ' pa-selected' : ''); + + const head = document.createElement('div'); + head.className = 'd-flex justify-content-between gap-2'; + const senderEl = document.createElement('strong'); + senderEl.className = 'text-truncate'; + senderEl.textContent = msg.sender || '—'; + const timeEl = document.createElement('span'); + timeEl.className = 'text-muted text-nowrap'; + timeEl.textContent = paFormatTime(msg).slice(5, 16); + head.appendChild(senderEl); + head.appendChild(timeEl); + entry.appendChild(head); + + // Content preview: one truncated line; full text when selected + if (msg.content) { + const preview = document.createElement('div'); + preview.className = 'pa-map-msg-preview' + (selected ? '' : ' text-truncate'); + preview.textContent = msg.content; + preview.title = msg.content; + entry.appendChild(preview); + } + entry.addEventListener('click', () => { paMapSelection = { msgId: msg.id, echoIdx: null }; paRenderMapView(); }); - if (msg.id === paMapSelection.msgId) { + if (selected) { msg.echoView.forEach((echo, idx) => { if (echo.hops === 0) return; const eEl = document.createElement('div'); @@ -638,9 +668,17 @@ function paRenderMapView() { } paSetView('map'); - // Leaflet cannot size itself while the container was display:none - setTimeout(() => paMap.invalidateSize(), 60); - paDrawSelectedEcho(); + // Leaflet cannot size itself while the container was display:none - + // and fitBounds in paDrawSelectedEcho needs the corrected size, so + // drawing must happen after invalidateSize in the same deferred step + setTimeout(() => { + paMap.invalidateSize(); + paDrawSelectedEcho(); + }, 60); + + // Keep the selected message visible (e.g. after jumping from the table) + const selectedEl = list.querySelector('.pa-map-msg.pa-selected'); + if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' }); const counter = document.getElementById('paCounter'); counter.textContent = paFiltersActive() diff --git a/app/templates/path-analyzer.html b/app/templates/path-analyzer.html index e2a1c04..8662f87 100644 --- a/app/templates/path-analyzer.html +++ b/app/templates/path-analyzer.html @@ -273,6 +273,22 @@ color: var(--text-secondary, #6c757d); } + .pa-echo-mapbtn { + cursor: pointer; + padding: 0 0.3rem; + color: var(--text-secondary, #6c757d); + } + + .pa-echo-mapbtn:hover { + color: #6f42c1; + } + + .pa-map-msg-preview { + font-size: 0.75rem; + color: var(--text-secondary, #6c757d); + margin-top: 0.1rem; + } + .pa-direct { font-size: 0.75rem; font-style: italic;