feat(live): clickable path overlay — packet info popup (closes #771 M2) (#923)

After a path animation completes, keeps an invisible clickable polyline
on the map for 30s. Clicking it shows a compact Leaflet popup with type
badge, hop chain, relative time, and a link to the full packets page.
Popup auto-dismisses after 20s.

## Changes
- `clickablePathsLayer`: new Leaflet layer for invisible hit-target
polylines
- `buildClickablePathPopupHtml()`: pure function generating popup HTML
(type badge, hop chain, time, hash link)
- `pruneClickablePaths()`: TTL (30s) + FIFO eviction (max 50); runs on
existing `_pruneInterval`
- `registerClickablePath()`: adds invisible polyline with click → popup
handler
- `animatePath()`: accepts optional `pktMeta` (`hash`, `ts`); calls
`registerClickablePath` on completion
- Teardown clears `clickablePathsLayer` and `clickablePaths`

## Tests
7 new unit tests; 77 pass, 0 regressions.

Closes #771 (M2 of 3)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
efiten
2026-05-21 05:56:58 +02:00
committed by GitHub
parent d0d1657b5c
commit 5cc7332583
4 changed files with 155 additions and 5 deletions
+3
View File
@@ -261,6 +261,8 @@ async function run() {
// Test 5: Node detail loads (reuses nodes page from test 2)
await test('Node detail loads', async () => {
await page.waitForSelector('table tbody tr:not([id^=vscroll])');
// Use page.click() instead of an element handle to avoid detached-element races
// when the WebSocket auto-refresh re-renders the table between querySelector and click.
await page.click('table tbody tr:not([id^=vscroll])');
// Wait for detail pane to appear
await page.waitForSelector('.node-detail');
@@ -275,6 +277,7 @@ async function run() {
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
await page.waitForSelector('table tbody tr:not([id^=vscroll])');
// Use page.click() to avoid detached-element race with WebSocket auto-refresh.
await page.click('table tbody tr:not([id^=vscroll])');
await page.waitForSelector('.node-detail');
// Find the Details link in the side panel