From dfe383cc51f80136a5c07ee02045ede6f6810ac0 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Thu, 16 Apr 2026 23:21:05 -0700 Subject: [PATCH] fix: node detail panel Details/Analytics links don't navigate (#779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #778 ## Problem The Details and Analytics links in the node side panel don't navigate when clicked. This is a regression from #739 (desktop node deep linking). **Root cause:** When a node is selected, `selectNode()` uses `history.replaceState()` to set the URL to `#/nodes/{pubkey}`. The Details link has `href="#/nodes/{pubkey}"` — the same hash. Clicking an anchor with the same hash as the current URL doesn't fire the `hashchange` event, so the SPA router never triggers navigation. ## Fix Added a click handler on the `nodesRight` panel that intercepts clicks on `.btn-primary` navigation links: 1. `e.preventDefault()` to stop the default anchor behavior 2. If the current hash already matches the target, temporarily clear it via `replaceState` 3. Set `location.hash` to the target, which fires `hashchange` and triggers the SPA router This handles both the Details link (`#/nodes/{pubkey}`) and the Analytics link (`#/nodes/{pubkey}/analytics`). ## Testing - All frontend helper tests pass (552/552) - All packet filter tests pass (62/62) - All aging tests pass (29/29) - Go server tests pass --------- Co-authored-by: you --- public/nodes.js | 12 ++++++++++++ test-e2e-playwright.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/public/nodes.js b/public/nodes.js index accaf82..439c367 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -1039,6 +1039,18 @@ // #630: Close button for node detail panel (important for mobile full-screen overlay) document.getElementById('nodesRight').addEventListener('click', function(e) { + // #778: Details/Analytics links don't navigate because replaceState + // already set the hash to #/nodes/PUBKEY, so clicking + // is a same-hash no-op. Force navigation by temporarily clearing the hash. + var link = e.target.closest('a.btn-primary[href^="#/nodes/"]'); + if (link) { + e.preventDefault(); + var target = link.getAttribute('href'); + // Always clear and reassign — hashchange won't fire if hash already matches + history.replaceState(null, '', '#/'); + location.hash = target.substring(1); + return; + } if (e.target.closest('.panel-close-btn')) { const panel = document.getElementById('nodesRight'); panel.classList.add('empty'); diff --git a/test-e2e-playwright.js b/test-e2e-playwright.js index fc10d15..4e6f9a6 100644 --- a/test-e2e-playwright.js +++ b/test-e2e-playwright.js @@ -231,6 +231,30 @@ async function run() { assert(hasStatus, 'No status indicator found in node detail'); }); + // Test: Node side panel Details link navigates to full detail page (#778) + await test('Node side panel Details link navigates', async () => { + await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('table tbody tr'); + // Click first row to open side panel + const firstRow = await page.$('table tbody tr'); + assert(firstRow, 'No node rows found'); + await firstRow.click(); + await page.waitForSelector('.node-detail'); + // Find the Details link in the side panel + const detailsLink = await page.$('#nodesRight a.btn-primary[href^="#/nodes/"]'); + assert(detailsLink, 'Details link not found in side panel'); + const href = await detailsLink.getAttribute('href'); + // Click the Details link — this should navigate to the full detail page + await detailsLink.click(); + // Wait for navigation — the full detail page has sections like neighbors/packets + await page.waitForFunction((expectedHash) => { + return location.hash === expectedHash; + }, href, { timeout: 5000 }); + // Verify we're on the full detail page (should have section tabs or detail content) + const hash = await page.evaluate(() => location.hash); + assert(hash === href, `Expected hash "${href}" but got "${hash}"`); + }); + // Test: Nodes page has WebSocket auto-update listener (#131) await test('Nodes page has WebSocket auto-update', async () => { await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });