From ded3255e08d921c04412c5e84b3fb171f13fa87a Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 28 Jul 2026 09:54:31 +0200 Subject: [PATCH] feat: make "View Estimated Nodes on Map" a shareable/bookmarkable URL dborup: clicking the link just landed on a bare "#/map" with no way to share it -- state lived in sessionStorage, gone after the click and never in the URL to begin with. Switched to a deep link, #/map?estimatedNodes=1, same convention as the existing packet-path deep link (#/map?packet=&obs=, loadRouteFromDeepLink). map.js's new loadEstimatedNodesFromDeepLink fetches /api/analytics/areas fresh and draws the result -- no sessionStorage involved, since this list isn't per-request state, just the deployment's current estimate, so the URL alone reproduces it. Copy/share/reload all just work now. Also converted the trigger from a JS-click ' : '') + + (estimatedNodes.length ? 'View Estimated Nodes on Map (' + estimatedNodes.length.toLocaleString() + ')' : '') + '' + '

How many of each area\'s nodes have an actual reported GPS position vs. how many were only placeable via a neighbor-based estimate (same technique used for View Path\'s approximate markers). Click a column header to sort by it.

' + '
' + gapsSection.tableHtml() + '
' + @@ -6793,14 +6793,6 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf densitySection.attach(); bridgeSection.attach(); gapsSection.attach(); - - var viewEstimatedBtn = document.getElementById('areasViewEstimatedNodes'); - if (viewEstimatedBtn) { - viewEstimatedBtn.addEventListener('click', function () { - sessionStorage.setItem('map-estimated-nodes', JSON.stringify({ points: estimatedNodes })); - window.location.hash = '#/map'; - }); - } } catch (e) { el.innerHTML = '
Failed to load area analytics: ' + esc(String(e)) + '
'; } diff --git a/public/map.js b/public/map.js index 5590fa2b..8434245b 100644 --- a/public/map.js +++ b/public/map.js @@ -679,19 +679,15 @@ } catch {} return; } - // Check for an estimated-nodes view from the Areas tab (via - // sessionStorage) — see drawEstimatedNodes. Checked before - // map-route-hops since both use similar payload shapes but mean - // different things (a resolved hop route vs. a flat point list). - const estimatedNodesJson = sessionStorage.getItem('map-estimated-nodes'); - if (estimatedNodesJson) { - sessionStorage.removeItem('map-estimated-nodes'); - try { - const parsed = JSON.parse(estimatedNodesJson); - if (parsed && Array.isArray(parsed.points)) { - drawEstimatedNodes(parsed.points); - } - } catch {} + // Estimated-nodes deep link: #/map?estimatedNodes=1 -- the Areas + // tab's "View Estimated Nodes on Map" button links straight here + // instead of stashing state in sessionStorage (like the old + // map-estimated-nodes key did), so dborup can copy/share/bookmark + // the URL and it still works after a reload -- same shareable-URL + // convention as loadRouteFromDeepLink below (#1418/#1419). + const hashQs = (location.hash || '').split('?')[1]; + if (new URLSearchParams(hashQs || '').get('estimatedNodes')) { + loadEstimatedNodesFromDeepLink(); return; } // Check for route from packet detail (via sessionStorage) @@ -1097,7 +1093,15 @@ closeBtn.addTo(map); const valid = (points || []).filter(function (p) { return p && p.lat != null && p.lon != null; }); - if (valid.length === 0) return; + if (valid.length === 0) { + const container = map.getContainer(); + const label = document.createElement('div'); + label.className = 'mc-estimated-nodes-label'; + label.style.cssText = 'position:absolute;top:10px;left:50px;z-index:1000;background:var(--input-bg,#1e293b);color:var(--text,#e2e8f0);padding:4px 10px;border-radius:4px;font-size:12px'; + label.textContent = 'No nodes currently have an estimated position.'; + container.appendChild(label); + return; + } const color = '#f59e0b'; valid.forEach(function (p) { @@ -1552,6 +1556,28 @@ } } + // Deep link for #/map?estimatedNodes=1 -- fetches the current + // network-wide estimated-position list fresh from the same endpoint + // the Areas tab uses (/api/analytics/areas) and draws it, rather than + // relying on a sessionStorage snapshot from whenever the link was + // clicked. Since this list isn't per-user/per-request state (it's the + // deployment's current estimate), the URL alone is enough to + // reproduce the view -- share/bookmark/reload all just work. + async function loadEstimatedNodesFromDeepLink() { + try { + const resp = await fetch('/api/analytics/areas'); + if (!resp.ok) { + console.warn('[deep-link] /api/analytics/areas returned ' + resp.status); + return; + } + const data = await resp.json(); + const points = (data && Array.isArray(data.estimatedNodes)) ? data.estimatedNodes : []; + drawEstimatedNodes(points); + } catch (e) { + console.warn('[deep-link] estimated-nodes load failed', e); + } + } + async function loadNodes() { try { // Load regions from config + observed IATAs diff --git a/test-analytics-areas-tab.js b/test-analytics-areas-tab.js index 5acf1d9a..4b3c5679 100644 --- a/test-analytics-areas-tab.js +++ b/test-analytics-areas-tab.js @@ -331,19 +331,24 @@ function makeApiStub(resp) { assert.ok(!el.innerHTML.includes('Show all'), 'no toggle should render with only 1 area in positionGaps'); }); - await testAsync('renders a "View Estimated Nodes on Map" button with the estimated-node count when estimatedNodes is non-empty', async () => { + await testAsync('renders a "View Estimated Nodes on Map" LINK (not a JS-click button) with the estimated-node count, pointing at a shareable/bookmarkable URL', async () => { const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse())); const el = fakeEl(); await ctx.window._analyticsRenderAreasTab(el); - assert.ok(el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the View Estimated Nodes button should render'); - assert.ok(el.innerHTML.includes('View Estimated Nodes on Map (1)'), 'the button label should show the estimated-node count'); + assert.ok(el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the View Estimated Nodes link should render'); + assert.ok(el.innerHTML.includes('View Estimated Nodes on Map (1)'), 'the link label should show the estimated-node count'); + // Must be a real so right-click "Copy Link Address" works and + // the URL survives a reload -- dborup asked for this after the first + // version (sessionStorage + a plain button) only ever led to a bare + // "#/map" with no shareable state. + assert.ok(el.innerHTML.includes(' { + await testAsync('does not render the "View Estimated Nodes on Map" link when estimatedNodes is empty', async () => { const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse({ estimatedNodes: [] }))); const el = fakeEl(); await ctx.window._analyticsRenderAreasTab(el); - assert.ok(!el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the button should not render when there are no estimated nodes to show'); + assert.ok(!el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the link should not render when there are no estimated nodes to show'); }); await testAsync('renders the unpositioned-nodes summary note including the no-neighbor-fix subset', async () => {