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' });