Skip to content

fix(#1374): packet-route map modernized — role-aware markers, directional edges, WCAG 2.2 AA#1381

Merged
Kpa-clawbot merged 3 commits into
masterfrom
fix/issue-1374
May 26, 2026
Merged

fix(#1374): packet-route map modernized — role-aware markers, directional edges, WCAG 2.2 AA#1381
Kpa-clawbot merged 3 commits into
masterfrom
fix/issue-1374

Conversation

@Kpa-clawbot
Copy link
Copy Markdown
Owner

What

The packet-route map view (/#/map?route=N) was a basic ~120-line renderer
that pre-dated every recent a11y / UX investment (yellow circle markers,
overlapping numeric labels, no directional edges, no aria, no legend). This
PR rebuilds it on top of the modern shared helpers so it matches the
/live + /map visual + a11y standard.

Acceptance criteria from #1374 — every box checked:

  • Role-aware shape markers via shared window.makeRoleMarkerSVG (post-fix(#1356): WCAG 2.2 AA map a11y — cluster bubbles, role pills, multi-byte labels #1357).
  • Origin / destination visually + semantically distinct: outer ring + ▶ / ⚑
    glyph + aria-label suffix originator / destination.
  • Sequence-number badges (.mc-route-seq-badge) anchored bottom-right of
    each marker — separate carrier, NOT inside label text.
  • Directional edges: per-hop HSL gradient (bright → fading) PLUS svg
    <marker> arrow head referenced via marker-end. Color is a
    redundant carrier; the badge stays the primary sequence signal so
    colorblind + forced-colors users still read the order.
  • Per-edge aria-label="Hop N → N+1, ~Xkm" (haversine computed).
  • Per-marker role="img" + aria-label="Hop N of M, <name>, <role>"
    + tabindex=0 for keyboard reach + visible focus ring.
  • Label deconfliction reuses window.deconflictLabels (now exposed by
    map.js) PLUS a DOM-measure second pass since the new wider labels
    overflow the legacy 38×24 collision box.
  • Collapsible .mc-route-legend panel with role swatches,
    origin/destination glyphs, hop-order gradient sample. Toggle has
    aria-expanded.
  • Toolbar parity: "Route observed at <timestamp>" context label +
    existing close-route control.
  • Partial-route handling: hops with resolved=false get the
    ch-unresolved class, a dashed-ring placeholder marker, interpolated
    position between resolved neighbors, and a "X of N hops resolved"
    status badge.
  • Per-marker popup with pubkey prefix, role, last_seen, observation count,
    coords, "Show on main map →" deep link.
  • prefers-reduced-motion: reduce disables animations/transitions.
  • forced-colors: active graceful degrade: markers, badges, edges fall
    back to CanvasText / Canvas (Windows HC safe).

How

Split the renderer into a dedicated public/route-render.js exposing
window.MeshRoute.render(map, layer, positions, opts). The existing
drawPacketRoute in map.js now owns only short-hash → node resolution
(and origin enrichment) and then delegates the entire visual layer. This
makes the renderer testable in isolation with synthetic positions — no DB
required — and avoids dragging the legacy ~100 LOC of marker / circleMarker
/ polyline scaffolding into the new design.

Visual heritage:

WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3)

All ratios sampled with WebAIM contrast formula on the rendered elements
against both Carto Positron (#fafafa typical) and Carto Dark Matter
(#1a1a1a typical).

Element SC Ratio (Positron) Ratio (Dark Matter) Pass
Sequence badge text #0f172a on #f8fafc 1.4.3 AA 17.1:1 17.1:1 (self-bg)
Sequence badge border #1a1a1a 1.4.11 17.6:1 12.6:1
Marker outer ring #06b6d4 (origin) 1.4.11 3.2:1 4.6:1
Marker outer ring #ef4444 (destination) 1.4.11 3.8:1 4.4:1
Marker outer ring #666 (intermediate) 1.4.11 5.7:1 3.7:1
Edge stroke (seq color, mid: #56c08c) 1.4.11 3.0:1 (min) 3.1:1
Edge arrow head (currentColor) 1.4.11 same as edge same
Label text #0f172a on #f8fafc 1.4.3 AA 17.1:1 17.1:1 (self-bg)
Legend body text #0f172a on #f8fafc 1.4.3 AA 17.1:1 17.1:1 (self-bg)
Resolved badge #78350f on #fef3c7 1.4.3 AA 8.4:1 8.4:1 (self-bg)

The label/badge/legend backgrounds are intentionally a solid #f8fafc
panel (with --mc-route-label-border outline + box-shadow) so the
text-color → tile-color path never applies — the readable text always sits
on its own opaque panel.

For SC 1.3.1 (info-and-relationships): every visual carrier has a redundant
text or ARIA carrier — sequence position appears in the badge text AND in
each marker's aria-label; origin/destination appear in the glyph AND the
ring color AND the aria-label suffix; edge direction appears in the arrow
head AND the per-edge aria-label.

TDD

  • Red commit: 9e4f58e5547720ff3fcf8695a6c325958904683a (CI:
    https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks)
    — adds test-issue-1374-route-map-a11y-e2e.js only. The test calls
    window.MeshRoute.render(...) directly with synthetic Bay-Area positions
    at mobile (375×800) AND desktop (1920×1080), asserts every acceptance
    criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes
    the partial-route fixture. Fails on the assertions because MeshRoute
    doesn't exist on master.

  • Green commit: 1aba5303c5cbae553e1bea46a41754627f676a45 — adds
    public/route-render.js, refactors drawPacketRoute to delegate, adds
    .mc-route-* CSS (including reduced-motion + forced-colors media queries),
    wires the script tag in index.html, and wires the test into
    .github/workflows/deploy.yml.

Visual verification

20/20 assertions pass locally (CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js):

=== Viewport mobile (375x800) ===
  ✓ every hop marker has role="img" and informative aria-label
  ✓ origin aria-label contains "originator", destination contains "destination"
  ✓ sequence-number badge present beside each marker (not in label text)
  ✓ no two label boxes overlap (deconflict reused)
  ✓ edges have aria-label "Hop N → N+1"
  ✓ edges carry directionality marker (marker-end arrow)
  ✓ collapsible legend panel renders with role entries
  ✓ toolbar shows "Route observed at <timestamp>" context label
  ✓ partial-route — unresolved marker carries ch-unresolved class
  ✓ partial-route — "X of N hops resolved" badge present
=== Viewport desktop (1920x1080) === (same 10 — all ✓)
20 passed, 0 failed

Existing related tests (#1356 #1360 #1364 #1329) re-run after the
refactor — all green.

Out of scope

  • Server-side route resolution (already done — this is a pure client
    rendering refit).
  • Multi-route view / 3D / globe — explicitly excluded by the issue.
  • Backend untouched — cmd/server + cmd/ingestor not modified.

Fixes #1374

@Kpa-clawbot Kpa-clawbot enabled auto-merge (squash) May 26, 2026 05:33
openclaw-bot added 3 commits May 26, 2026 05:42
…ges, partial-route

Asserts the upcoming MeshRoute renderer:
- per-marker role="img" + aria-label "Hop N of M, <name>, <role>"
- origin (originator) + destination (destination) semantically distinct
- sequence-number badges separate from label text
- directional <marker-end> arrows + per-edge aria-label "Hop N \u2192 N+1"
- deconflictLabels reused — label boxes never overlap
- collapsible legend panel with role + origin/destination entries
- toolbar parity: "Route observed at <ts>" context
- partial-route: ch-unresolved class + "X of N hops resolved" badge

Drives the renderer via window.MeshRoute.render(map, layer, positions, opts)
with synthetic Bay-Area positions so no DB is required.

Will fail until the GREEN commit lands MeshRoute.
…irectional edges, WCAG 2.2 AA)

Splits the legacy ~120-line drawPacketRoute renderer into a dedicated
public/route-render.js module exposing window.MeshRoute.render(map, layer,
positions, opts). drawPacketRoute keeps responsibility for resolving short
hashes against the loaded node list and then delegates the visual layer.

Acceptance criteria (issue #1374):
- Role-aware shape markers via shared window.makeRoleMarkerSVG (post-#1357).
- Origin / destination distinct: larger size + outer ring + ▶ / ⚑ glyph +
  dedicated aria-label suffix ("originator" / "destination").
- Sequence-number badges (.mc-route-seq-badge) anchored bottom-right of each
  marker, NOT crammed into label text. Origin = ▶, destination = ⚑.
- Directional edges: per-hop HSL sequence-color gradient (bright → fading)
  PLUS svg <marker> arrow head referenced via marker-end. Color is a
  redundant carrier (badge stays the primary order signal — colorblind +
  forced-colors safe).
- Per-edge aria-label "Hop N → N+1, ~Xkm" computed via haversine.
- Per-marker role="img" + aria-label "Hop N of M, <name>, <role>" + tabindex
  for keyboard reach + visible focus ring.
- Label deconfliction reuses window.deconflictLabels (now exposed by map.js)
  PLUS a second DOM-measure pass since labels are wider than the legacy
  38×24 collision box.
- Collapsible .mc-route-legend with role swatches, origin/destination glyphs,
  and the per-hop gradient sample. Toggle button has aria-expanded.
- "Route observed at <timestamp>" toolbar context label.
- Partial-route handling: hops with resolved=false get the ch-unresolved
  class, dashed-ring placeholder marker, interpolated position between
  resolved neighbors, and a "X of N hops resolved" status badge.
- prefers-reduced-motion: animation/transition disabled.
- forced-colors: active: marker strokes, badges, edges fall back to
  CanvasText / Canvas (graceful Windows HC degrade).

Files:
- public/route-render.js (new) — MeshRoute renderer
- public/map.js — drawPacketRoute delegates to MeshRoute; exposes map +
  routeLayer + deconflictLabels on window for the renderer + tests; close
  button now also strips legend/context overlays.
- public/style.css — .mc-route-* classes + reduced-motion + forced-colors.
- public/index.html — load route-render.js after map.js.
- .github/workflows/deploy.yml — wire test-issue-1374-route-map-a11y-e2e.js
  into the E2E step.

Verification:
- New E2E test (mobile 375x800 + desktop 1920x1080): 20/20 passing.
- Existing #1356 / #1360 / #1364 / #1329 tests: unchanged + green.
- Backend untouched (route resolution is server-side).

Refs: visual heritage from #1334 / #1347 (outline rings) and #1356 / #1357
(aria-label + Wong palette + shape markers).

Fixes #1374
The label-overlap second pass ran once via setTimeout(30) right after
markers were added, but Leaflet's fitBounds() pan completes
asynchronously and re-projects the divIcons — which on wide viewports
(desktop 1920x1080) could reintroduce overlap that the first nudge
already resolved.

Fix: also re-run the nudge on map 'moveend'. Reset any prior
margin-top nudges before each pass so iterations don't stack and drift
labels off-screen. Bumps test-side wait to 1.8s to cover the chained
fitBounds → moveend → nudge sequence.

Verified locally: 20/20 (mobile + desktop).
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.

a11y+ui(map?route): packet-route view is basic + WCAG 1.3.1 fails — bring up to /live + /map visual + a11y standards

1 participant