Skip to content

feat(meshcore-map): node-type icons, filter & legend on the source map#3576

Merged
Yeraze merged 1 commit into
mainfrom
feat/meshcore-map-node-types
Jun 20, 2026
Merged

feat(meshcore-map): node-type icons, filter & legend on the source map#3576
Yeraze merged 1 commit into
mainfrom
feat/meshcore-map-node-types

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Extends the node-type work from #3546 (which shipped only on the /analysis Map Analysis workspace) to the standard per-source MeshCore map. That map previously drew a generic purple "MC" badge for every node, so operators couldn't distinguish infrastructure (repeaters/room servers) from end-user nodes at a glance, nor focus the map by type. This brings it to parity with the analysis view.

Changes

  • src/utils/mapIcons.ts — new roleGlyphMarkerSvg(category, color, size) helper: the role glyph (roleGlyphInnerSvg) on a white backing circle, as a complete <svg> string. Reused by both the map marker and the legend swatches so the icon a user sees and the legend entry can't diverge. Returns '' for standard so callers keep their default marker.
  • src/components/MeshCore/MeshCoreMap.tsx:
    • Markers classify each contact via getNodeTypeCategory({ advType }) and render the role glyph (Repeater = tower, Room Server = server rack, Sensor = broadcast, Companion = person). The "MC" badge stays as the fallback for standard/unknown nodes.
    • New Node Types filter in the Map Features panel (per-category checkboxes), persisted to localStorage. Hides markers by category; paths/neighbor lines are left untouched, matching how the analysis view's type filter only affects markers.
    • Opts the legend into the new section (<MapLegend showNodeTypes />).
  • src/components/MapLegend.tsx — opt-in showNodeTypes prop renders a Node Types legend section (Repeater/Room Server/Sensor/Companion glyph swatches). Off by default, so the Meshtastic maps that share this component are unaffected.
  • src/utils/mapIcons.test.ts — unit tests for roleGlyphMarkerSvg.
  • CHANGELOG.md — Features entry.

Reuses the existing, already-tested getNodeTypeCategory / nodePassesTypeFilter from #3546 — no new categorization logic.

Issues Resolved

Relates to #3546 (extends it to the MeshCore source map).

Documentation Updates

CHANGELOG.md updated. No feature/API docs affected.

Testing

  • Full Vitest suite: 6924 passed, 0 failed (2339 suites)
  • New mapIcons.test.ts cases for roleGlyphMarkerSvg (glyph + circle + size; '' for standard)
  • TypeScript: touched files compile cleanly
  • Verified live in the dev container on a MeshCore source (MC-Sandbox, 97 nodes): 70/70 markers rendered the glyph SVG; the Features panel showed all five Node Types checkboxes; unchecking Companion dropped markers 70 → 51 and persisted {"companion":false} to localStorage; the legend showed the Node Types section.
  • Reviewer: confirm the marker glyph reads well against your tile style (white backing circle is intended to keep it legible on light & dark tiles).

🤖 Generated with Claude Code

Extends the issue #3546 node-type work to the per-source MeshCore map,
which previously rendered a generic purple "MC" badge for every node.

- Markers now show role-based glyphs by advert type (Repeater/Room
  Server/Sensor/Companion) via getNodeTypeCategory({ advType }); the "MC"
  badge remains the fallback for standard/unknown nodes.
- New shared mapIcons.roleGlyphMarkerSvg() (white circle + role glyph),
  reused by both the marker and the legend so they can't diverge.
- Map Features panel gains a persisted Node Types filter (per-category
  checkboxes) that hides markers by role; paths/neighbor lines are
  unaffected, matching the Map Analysis workspace.
- MapLegend gains an opt-in showNodeTypes prop rendering a Node Types
  section; off by default so the Meshtastic maps are unchanged.

Verified live on a MeshCore source (70 markers all glyphed; hiding
Companion dropped markers 70 -> 51; filter persisted to localStorage;
legend section present).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj
@Yeraze Yeraze force-pushed the feat/meshcore-map-node-types branch from 38f966f to 459eb4c Compare June 20, 2026 13:57
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(meshcore-map): node-type icons, filter & legend

  • Read changed files
  • Analyze code quality
  • Check for bugs/issues
  • Review test coverage
  • Post final review

Overall this is a clean, well-scoped PR. The implementation is consistent with the pattern established in #3546 and the code is easy to follow. A few observations below.


src/utils/mapIcons.ts

roleGlyphMarkerSvg (lines 60–70) — well-designed. Delegating to roleGlyphInnerSvg and returning '' for unknown/standard keeps callers' fallback logic simple and avoids divergence between the marker and the legend swatch. No issues.

Potential minor issue — default: in roleGlyphInnerSvg (mapIcons.ts:47): The default branch already returns '', but the TypeScript type system should catch any unhandled NodeTypeCategory variants. If the union ever grows (e.g. a new firmware advert type), roleGlyphInnerSvg silently returns '' instead of surfacing a compile-time exhaustiveness error. Consider using a satisfies check or a typed exhaustive switch if this is expected to evolve. Not a blocker, but worth noting.


src/components/MeshCore/MeshCoreMap.tsx

getNodeTypeCategory called twice per contact (MeshCoreMap.tsx:197 and MeshCoreMap.tsx:287): visibleContacts filters using nodePassesTypeFilter({ advType: c.advType }, ...), which internally calls getNodeTypeCategory. Then, inside the visibleContacts.map(...) render loop, makeIcon is called with getNodeTypeCategory({ advType: c.advType }) again. With 97 nodes this is negligible, but if the contact list ever grows significantly the category could be derived once during the visibleContacts memo. Minor perf observation, not a bug.

makeIcon is a plain function recreated on every render (MeshCoreMap.tsx:71): makeIcon itself isn't memoized, but since it's a pure function with no closures over component state this is fine — L.divIcon is cheap. No issue.

localStorage.getItem / JSON.parse in useState initializer (MeshCoreMap.tsx:126–132): The try/catch around JSON.parse is correct. One nit: if JSON.parse returns a non-object (e.g. someone manually set the key to "null" or a string), the state would be typed as Partial<Record<...>> but be null at runtime, causing nodeTypeFilter[category] to throw. A typeof raw === 'object' && raw !== null check after parsing would be more defensive. Low-probability issue.

Filter applies only to markers, not paths/neighbor lines (MeshCoreMap.tsx:193–198): This matches the documented intent and the analysis map's behavior, so it's correct. The inline comment makes it clear.

MapContainer key resets the map on center/zoom changes (MeshCoreMap.tsx:269): This is existing behavior, not introduced by this PR.


src/components/MapLegend.tsx

dangerouslySetInnerHTML with roleGlyphMarkerSvg output (MapLegend.tsx:151): The SVG string is constructed entirely from the color constant (NODE_TYPE_LEGEND_COLOR = '#cba6f7') and hard-coded geometry from roleGlyphInnerSvg. There is no user-supplied data flowing into the HTML, so XSS risk is negligible here. Still, worth noting for future maintainers: if color ever comes from user input, this would need sanitization.

NODE_TYPE_LEGEND_COLOR is a module-level constant (MapLegend.tsx:12): Hard-coded to match the MeshCore map's accent. If a future feature lets users customize the MeshCore marker color, this constant would drift. Consider deriving it from overlayColors or a prop if that's ever needed — low priority.

showNodeTypes opt-in prop — well-designed. The Meshtastic maps are unaffected by default.


src/utils/mapIcons.test.ts

Coverage is good for the new function (mapIcons.test.ts:172–194): Tests cover all four glyph categories, the size parameter, and the standard early-return.

Missing edge case — unknown/arbitrary category value: roleGlyphMarkerSvg calls roleGlyphInnerSvg which returns '' for any unrecognized value via the default branch. A test with an out-of-range category (e.g. cast to NodeTypeCategory) would close that loop, though it's not critical given TypeScript's type safety.

No test for the color parameter being reflected in the output: The existing test asserts svg.toContain(COLOR) (line 181), which is correct.


Summary

Area Status
Code quality ✅ Clean, consistent with existing patterns
Potential bugs ⚠️ Minor: JSON.parse result not validated as object before use (low risk)
Performance ℹ️ getNodeTypeCategory called twice per visible contact (negligible at current scale)
Security dangerouslySetInnerHTML input is fully static — no user data involved
Test coverage ✅ Adequate for the new helper; minor edge case missing

No blocking issues. The PR is ready to merge as-is. The items above are suggestions for future hardening.

@Yeraze Yeraze merged commit fbeb6ec into main Jun 20, 2026
28 of 29 checks passed
@Yeraze Yeraze deleted the feat/meshcore-map-node-types branch June 20, 2026 16:10
@Yeraze Yeraze mentioned this pull request Jun 20, 2026
Yeraze added a commit that referenced this pull request Jun 20, 2026
Bump version to 4.11.1 across package.json, package-lock.json,
desktop/package.json, desktop/src-tauri/tauri.conf.json, and
helm/meshmonitor/Chart.yaml. Roll the CHANGELOG [Unreleased] section
(device ClientNotification surfacing + firmware 2.8 favorite/ignore cap
handling #3548, auto-ack 2x2 matrix #3564, MeshCore node-type map icons
#3576) into [4.11.1].


Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant