From 395abc2585aa4e0a88223e2b8bbad4e638ae8098 Mon Sep 17 00:00:00 2001 From: you Date: Fri, 20 Mar 2026 06:44:18 +0000 Subject: [PATCH] feat: standalone packet detail page at #/packet/ID - New route #/packet/123 shows full packet detail on its own page - Back link to packets list - Copy Link button now generates #/packet/ID URLs - Reuses existing renderDetail() for consistent display --- public/app.js | 5 +++++ public/packets.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index 08d570f0..f58cf8fe 100644 --- a/public/app.js +++ b/public/app.js @@ -283,6 +283,11 @@ function navigate() { basePage = 'node-analytics'; } + // Special route: packet/123 → standalone packet detail page + if (basePage === 'packet' && routeParam) { + basePage = 'packet-detail'; + } + // Update nav active state document.querySelectorAll('.nav-link[data-route]').forEach(el => { el.classList.toggle('active', el.dataset.route === basePage); diff --git a/public/packets.js b/public/packets.js index 99e9dbd3..e35c7b95 100644 --- a/public/packets.js +++ b/public/packets.js @@ -817,7 +817,7 @@ const copyLinkBtn = panel.querySelector('.copy-link-btn'); if (copyLinkBtn) { copyLinkBtn.addEventListener('click', () => { - const url = `${location.origin}/#/packets/id/${copyLinkBtn.dataset.packetId}`; + const url = `${location.origin}/#/packet/${copyLinkBtn.dataset.packetId}`; navigator.clipboard.writeText(url).then(() => { copyLinkBtn.textContent = '✅ Copied!'; setTimeout(() => { copyLinkBtn.textContent = '🔗 Copy Link'; }, 1500); @@ -1125,4 +1125,31 @@ } registerPage('packets', { init, destroy }); + + // Standalone packet detail page: #/packet/123 + registerPage('packet-detail', { + init: async (app, routeParam) => { + const id = Number(routeParam); + app.innerHTML = `
Loading packet #${id}…
`; + try { + const data = await api(`/packets/${id}`); + if (!data?.packet) { app.innerHTML = `

Packet not found

Packet #${id} doesn't exist.

← Back to packets
`; return; } + const hops = []; + try { const ph = JSON.parse(data.packet.path_json || '[]'); hops.push(...ph); } catch {} + const newHops = hops.filter(h => !(h in hopNameCache)); + if (newHops.length) await resolveHops(newHops); + const container = document.createElement('div'); + container.style.cssText = 'max-width:800px;margin:0 auto;padding:20px'; + container.innerHTML = `
← Back to packets
`; + const detail = document.createElement('div'); + container.appendChild(detail); + renderDetail(detail, data); + app.innerHTML = ''; + app.appendChild(container); + } catch (e) { + app.innerHTML = `

Error

${e.message}

← Back to packets
`; + } + }, + destroy: () => {} + }); })();