diff --git a/public/analytics.js b/public/analytics.js index 4510365b..5cc422a9 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -6783,7 +6783,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf '
' + '

' + 'Position-Fix Coverage Gaps by Area' + - (estimatedNodes.length ? '' : '') + + (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 () => {