From d8d3427dc93c4da6ec9b1c6c70bbdee72f84956d Mon Sep 17 00:00:00 2001 From: MarekWo Date: Thu, 23 Jul 2026 07:10:31 +0200 Subject: [PATCH] feat(pathanalyzer): chat route click opens the analyzer map deep-linked Clicking a route in the chat path popup now opens Path Analyzer on the Map view with that message selected and that exact echo path drawn, instead of copying the route to the clipboard. Copy stays available as a small per-route clipboard icon in the popup. Deep link flow: popup click stores {packet_hash, path hex} in window.paDeepLink; the modal show handler builds the iframe URL with ?hash=&path=; the analyzer resolves the message after load (widening the time range once to 7 days if needed), matches the echo by raw path hex (fallback: shortest), and switches to the map. A plain menu open still loads the analyzer without any deep link. Verified live via Playwright: clicked the 3rd (non-shortest) route of a multi-route message - the map opened with exactly that echo selected, including the 3-to-7-day widening retry (message was 4 days old). Co-Authored-By: Claude Opus 4.8 --- app/static/css/style.css | 19 ++++++++++++ app/static/js/app.js | 56 ++++++++++++++++++++++++++++------ app/static/js/path-analyzer.js | 49 ++++++++++++++++++++++++++++- app/templates/index.html | 8 ++++- 4 files changed, 121 insertions(+), 11 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index 56059ca..cd62f62 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1557,6 +1557,25 @@ main { border-bottom: 1px solid rgba(255, 255, 255, 0.15); word-break: break-all; cursor: pointer; + display: flex; + align-items: flex-start; + gap: 0.4rem; +} + +.path-popup .path-route { + flex: 1 1 auto; + min-width: 0; +} + +.path-popup .path-copy { + flex: 0 0 auto; + padding: 0.1rem 0.15rem; + opacity: 0.7; + cursor: pointer; +} + +.path-popup .path-copy:hover { + opacity: 1; } .path-popup .path-entry:active { diff --git a/app/static/js/app.js b/app/static/js/app.js index 15d2844..76b1c52 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -1187,7 +1187,7 @@ function updateMessageMetaDOM(wrapper, meta) { : segments.join('\u2192'); const pathsData = encodeURIComponent(JSON.stringify(paths)); const routeLabel = paths.length > 1 ? `Route (${paths.length})` : 'Route'; - metaParts.push(`${routeLabel}: ${shortPath}`); + metaParts.push(`${routeLabel}: ${shortPath}`); } const metaInfo = metaParts.join(' | '); @@ -1329,7 +1329,7 @@ function createMessageElement(msg) { : segments.join('\u2192'); const pathsData = encodeURIComponent(JSON.stringify(msg.paths)); const routeLabel = msg.paths.length > 1 ? `Route (${msg.paths.length})` : 'Route'; - metaParts.push(`${routeLabel}: ${shortPath}`); + metaParts.push(`${routeLabel}: ${shortPath}`); } const metaInfo = metaParts.join(' | '); @@ -1693,7 +1693,7 @@ async function blockContactFromChat(senderName) { /** * Show paths popup on tap (mobile-friendly, shows all routes) */ -function showPathsPopup(element, encodedPaths) { +function showPathsPopup(element, encodedPaths, packetHash) { // Remove any existing popup const existing = document.querySelector('.path-popup'); if (existing) existing.remove(); @@ -1717,16 +1717,42 @@ function showPathsPopup(element, encodedPaths) { const hops = segments.length; const entry = document.createElement('div'); entry.className = 'path-entry'; - entry.innerHTML = `${fullRoute}SNR: ${snr} | Hops: ${hops}`; - entry.title = 'Tap to copy route'; - entry.addEventListener('click', (e) => { + + const body = document.createElement('span'); + body.className = 'path-route'; + body.innerHTML = `${fullRoute}SNR: ${snr} | Hops: ${hops}`; + entry.appendChild(body); + + const copyBtn = document.createElement('i'); + copyBtn.className = 'bi bi-clipboard path-copy'; + copyBtn.title = 'Copy route'; + copyBtn.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(commaRoute).then(() => { - const orig = entry.innerHTML; - entry.innerHTML = 'Copied!'; - setTimeout(() => { entry.innerHTML = orig; }, 1000); + copyBtn.className = 'bi bi-clipboard-check path-copy'; + setTimeout(() => { copyBtn.className = 'bi bi-clipboard path-copy'; }, 1000); }); }); + entry.appendChild(copyBtn); + + if (packetHash && p.path && segments.length > 0) { + entry.title = 'Show this route on the Path Analyzer map'; + entry.addEventListener('click', (e) => { + e.stopPropagation(); + popup.remove(); + openPathInAnalyzer(packetHash, p.path); + }); + } else { + // No packet hash (or direct message): keep the copy behavior + entry.title = 'Tap to copy route'; + entry.addEventListener('click', (e) => { + e.stopPropagation(); + navigator.clipboard.writeText(commaRoute).then(() => { + copyBtn.className = 'bi bi-clipboard-check path-copy'; + setTimeout(() => { copyBtn.className = 'bi bi-clipboard path-copy'; }, 1000); + }); + }); + } popup.appendChild(entry); }); @@ -1757,6 +1783,18 @@ function showPathsPopup(element, encodedPaths) { }); } +/** + * Open the Path Analyzer modal deep-linked to one message + echo path. + * The modal's show handler (index.html) reads window.paDeepLink and builds + * the iframe URL from it. + */ +function openPathInAnalyzer(packetHash, pathHex) { + const modalEl = document.getElementById('pathAnalyzerModal'); + if (!modalEl) return; + window.paDeepLink = { hash: packetHash, path: pathHex }; + bootstrap.Modal.getOrCreateInstance(modalEl).show(); +} + /** * Load connection status */ diff --git a/app/static/js/path-analyzer.js b/app/static/js/path-analyzer.js index 1a692d6..2fbd252 100644 --- a/app/static/js/path-analyzer.js +++ b/app/static/js/path-analyzer.js @@ -88,6 +88,8 @@ let paCurrentView = 'messages'; // 'messages' | 'stats' | 'routes' | 'map' let paContacts = []; // /api/contacts/cached?format=full let paStatsSort = { key: 'relayed', dir: -1 }; let paRoutesSort = { key: 'echoes', dir: -1 }; +let paDeepLink = null; // ?hash=..&path=.. from a chat path popup +let paContactsReady = Promise.resolve(); async function paLoadContacts() { try { @@ -978,6 +980,45 @@ async function paLoadMessages() { } else { paRender(); } + + if (paDeepLink) paApplyDeepLink(); +} + +// Deep link from the chat path popup: jump to the map view with the +// linked message selected and the linked echo path drawn. +async function paApplyDeepLink() { + const dl = paDeepLink; + const hash = (dl.hash || '').toLowerCase(); + const msg = paMessages.find(m => (m.packet_hash || '').toLowerCase() === hash); + + if (!msg) { + // The chat can show messages older than the default window - widen + // to the max range once before giving up + const daysSel = document.getElementById('paDaysSelect'); + if (!dl.retried && daysSel.value !== '7') { + dl.retried = true; + daysSel.value = '7'; + paLoadMessages(); // re-enters paApplyDeepLink when done + return; + } + paDeepLink = null; + showNotification('This message is no longer in the analyzer data (max 7 days).', 'warning'); + return; + } + + paDeepLink = null; + await paContactsReady; // map needs contacts to resolve hop positions + + let echoIdx = msg.echoView.findIndex(e => + e.hops > 0 && (e.path || '').toLowerCase() === (dl.path || '').toLowerCase()); + if (echoIdx === -1) echoIdx = paShortestEchoIdx(msg); + if (echoIdx === null) { + showNotification('This message has no routed echoes to draw.', 'warning'); + return; + } + + paMapSelection = { msgId: msg.id, echoIdx: echoIdx }; + paSwitchView('map'); } // ================================================================ @@ -1024,6 +1065,12 @@ function paClearFilters() { document.addEventListener('DOMContentLoaded', () => { loadUiSettings(); + + const qs = new URLSearchParams(window.location.search); + if (qs.get('hash')) { + paDeepLink = { hash: qs.get('hash'), path: qs.get('path') || '' }; + } + document.getElementById('paDaysSelect').addEventListener('change', paLoadMessages); document.getElementById('paRefreshBtn').addEventListener('click', paLoadMessages); @@ -1068,6 +1115,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - paLoadContacts(); + paContactsReady = paLoadContacts(); paLoadMessages(); }); diff --git a/app/templates/index.html b/app/templates/index.html index 40dfeaf..03aa6a4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -350,7 +350,13 @@ pathAnalyzerModal.addEventListener('show.bs.modal', function () { const pathAnalyzerFrame = document.getElementById('pathAnalyzerFrame'); if (pathAnalyzerFrame) { - pathAnalyzerFrame.src = '/path-analyzer'; + // Deep link from a chat path popup (openPathInAnalyzer): + // preselect the message + echo on the map view + const dl = window.paDeepLink; + window.paDeepLink = null; + pathAnalyzerFrame.src = dl + ? `/path-analyzer?hash=${encodeURIComponent(dl.hash)}&path=${encodeURIComponent(dl.path)}` + : '/path-analyzer'; } }); }