Skip to content

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

Open
efiten wants to merge 3 commits intoKpa-clawbot:masterfrom
efiten:feat/clickable-paths-771
Open

feat(live): clickable path overlay — packet info popup (closes #771 M2)#923
efiten wants to merge 3 commits intoKpa-clawbot:masterfrom
efiten:feat/clickable-paths-771

Conversation

@efiten
Copy link
Copy Markdown
Contributor

@efiten efiten commented Apr 28, 2026

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)

@efiten efiten mentioned this pull request Apr 28, 2026
Copy link
Copy Markdown
Owner

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — PR #923: Clickable Path Overlay

Reviewer: The Optimizer (Carmack persona)
PR: #923feat(live): clickable path overlay — packet info popup (closes #771 M2)
Verdict: Changes Requested
Merge conflicts: Yes (CONFLICTING state — must resolve before merge)


Carmack Review

The data flow is clean — animation completes, registers a clickable polyline, prune on interval. Good separation. But there are allocation and complexity concerns in what is effectively a hot path (animations complete frequently on an active mesh).

Must-Fix

1. pruneClickablePaths allocates on every call via .filter() + reassignment

Called both inside registerClickablePath (every animation completion) AND on the prune interval. With active meshes producing dozens of animations per second, this creates GC pressure.

Fix: mutate in place. Walk the array once, splice expired entries, then trim from index 0 if over max. One pass, zero allocations.

function pruneClickablePaths() {
  const cutoff = Date.now() - CLICKABLE_PATH_TTL_MS;
  for (let i = clickablePaths.length - 1; i >= 0; i--) {
    if (clickablePaths[i].addedAt < cutoff) {
      try { clickablePaths[i].poly.remove(); } catch (_) {}
      clickablePaths.splice(i, 1);
    }
  }
  while (clickablePaths.length > CLICKABLE_PATH_MAX) {
    try { clickablePaths[0].poly.remove(); } catch (_) {}
    clickablePaths.shift();
  }
}

This also eliminates the awkward paths parameter and the need to reassign clickablePaths at two call sites. The function owns the module-level array directly.

2. Two .map() calls in animation completion callback

const latLngs = hopPositions.map(hp => hp.pos);
const hopNames = hopPositions.map(hp => hp.name || ...);

Two array allocations per animation completion. Fuse into a single loop:

const latLngs = [], hopNames = [];
for (const hp of hopPositions) {
  latLngs.push(hp.pos);
  hopNames.push(hp.name || (hp.key ? hp.key.slice(0,8) : '?'));
}

3. buildClickablePathPopupHtml uses inline styles instead of CSS classes

Not a perf issue per se, but violates the repo's AGENTS.md rule: "All colors MUST use CSS variables. Never hardcode #hex values outside of :root definitions." The color:#fff, color:var(--text-muted,#6b7280) partially complies but style="background:${color}" injects a raw hex. Use a CSS class and set the background via style attribute only for the dynamic color (acceptable), but move static styles to live.css.

4. Popup Date.now() drift in buildClickablePathPopupHtml

The "X seconds ago" is computed at click time relative to tsMs, but tsMs is set at animation-start time (first._ts || Date.now()). If the animation takes 2-3s, the popup's "ago" will be off by the animation duration. Minor but trivially fixable: use the entry's addedAt (registration time = animation end) instead of pktMeta.ts for the "ago" display, or document that it's relative to packet receive time (which is arguably more correct — in that case, add a comment).

5. Test: "shows relative time" assertion is too weak

assert.ok(html.includes('s ago') || html.includes('ago'), 'should show relative time');

This would pass if the function returned garbage containing the word "ago". Assert the actual expected value: with a 10s offset, expect '10s ago'.

6. Missing test: FIFO eviction order

The eviction test checks the final count is 50 but doesn't verify that the oldest entries were evicted (FIFO). Add an assertion that remaining[0].addedAt equals the expected youngest-evicted-entry's successor.

Out of Scope

  • The weight: 12 invisible polyline hit target is generous — on dense maps with many paths this could create overlapping click targets. Worth a follow-up issue for M3 to add z-index management or a click disambiguation popup.

Summary: 6 must-fix items. Core logic is sound but allocations in the hot path need tightening, and tests need sharper assertions. Resolve merge conflicts first.

efiten added a commit to efiten/meshcore-analyzer that referenced this pull request May 2, 2026
…oops, extract CSS, sharpen tests

- pruneClickablePaths: operates on module-level array directly (no params, no return);
  both call sites updated
- Animation completion: fuse two .map() calls into single loop (one allocation)
- buildClickablePathPopupHtml: move static styles to live.css classes; keep only
  dynamic background color inline; add comment that tsMs is packet receive time
- Tests: assert exact '10s ago' string; rewrite prune tests to use exposed
  _liveClickablePaths array; add FIFO eviction order assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
efiten and others added 3 commits May 6, 2026 10:29
…loses Kpa-clawbot#771 M2)

After a path animation completes, registers an invisible clickable polyline
(weight 12, opacity 0) on a dedicated clickablePathsLayer for 30s. Clicking
shows a Leaflet popup with type badge, hop chain, relative time, and a link
to the packets page. Popup auto-dismisses after 20s. Max 50 paths retained
(FIFO eviction); TTL pruning runs on the existing _pruneInterval.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
page.$() captures an element handle at query time. When the nodes page
WebSocket auto-refresh fires between the querySelector and the .click()
call, the table is re-rendered and the element is detached, causing
"Element is not attached to the DOM".

Replace both occurrences with page.click(selector), which re-queries
the DOM at click time and retries until the element is stable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oops, extract CSS, sharpen tests

- pruneClickablePaths: operates on module-level array directly (no params, no return);
  both call sites updated
- Animation completion: fuse two .map() calls into single loop (one allocation)
- buildClickablePathPopupHtml: move static styles to live.css classes; keep only
  dynamic background color inline; add comment that tsMs is packet receive time
- Tests: assert exact '10s ago' string; rewrite prune tests to use exposed
  _liveClickablePaths array; add FIFO eviction order assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten efiten force-pushed the feat/clickable-paths-771 branch from 9b4ebd0 to 29e2a46 Compare May 6, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Live path requests

2 participants