Skip to content

feat(map): MeshCore node-type icons + map filtering (#3546)#3563

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

feat(map): MeshCore node-type icons + map filtering (#3546)#3563
Yeraze merged 1 commit into
mainfrom
feature/meshcore-map-node-types

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Closes #3546.

What

Role-based map markers and a node-type filter on the Map Analysis view, so operators can visually distinguish infrastructure from end-user nodes and focus the map on specific categories.

Works for MeshCore (by advert type) and Meshtastic (router role), in both the MeshMonitor and Official pin styles.

Categories

repeater · roomServer · sensor · companion · standard

  • MeshCore: advert type 1=Companion, 2=Repeater, 3=Room, 4=Sensor → category
  • Meshtastic: ROUTER role (2) folds into repeater; everything else standard

Changes

File Change
src/utils/nodeTypeCategory.ts (new) Single source of truth: getNodeTypeCategory() so the icon shown and the checkbox that hides it can't disagree
src/utils/mapIcons.ts roleGlyphInnerSvg() (tower / server-rack / broadcast / person) + roleCategory param on createNodeIcon, applied in both pin styles. White bg-circle keeps glyphs legible on light & dark tiles
src/components/MapAnalysis/layers/NodeMarkersLayer.tsx Classify each node, hide toggled-off types, pass category to the icon
src/hooks/useMapAnalysisConfig.ts Persisted config.nodeTypes (defaults all-visible; merged into stored configs)
src/components/MapAnalysis/NodeTypeFilterControl.tsx (new) Toolbar popover of per-type checkboxes
src/components/MapAnalysis/MapLegend.tsx "Node Types" legend section
src/server/routes/sourceRoutes.ts Expose MeshCore advType in /api/sources/:id/nodes (was hardcoded away); flows through the unified-node merge
public/locales/en.json map.nodeType.* keys (English fallbacks in code for untranslated locales)

Design note

The feature request lists "Observer" as a fourth type, but the MeshCore protocol has no Observer advert type — the real fourth type is Sensor (4), which the rest of the codebase already uses. This PR maps that category to Sensor. Trivial to relabel if "Observer" is preferred in the UI.

Testing

  • src/utils/nodeTypeCategory.test.ts — 36 cases (MeshCore advTypes, Meshtastic roles, filter isolation)
  • tsc --noEmit clean
  • Full Vitest suite: 6900 passed, 0 failed, 0 suites failed
  • ESLint clean on all changed files

🤖 Generated with Claude Code

https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj

Add role-based map markers and a node-type filter so operators can
visually distinguish infrastructure from end-user nodes and focus the
map on specific categories.

- nodeTypeCategory.ts: single source of truth classifying nodes into
  repeater / roomServer / sensor / companion / standard. MeshCore uses
  advert type (1/2/3/4); Meshtastic ROUTER role folds into repeater.
- mapIcons.ts: roleGlyphInnerSvg() + a roleCategory param on
  createNodeIcon (tower / server-rack / broadcast / person glyphs),
  applied in both the MeshMonitor and Official pin styles. White
  background circle keeps glyphs legible on light and dark tiles.
- NodeMarkersLayer: classifies each node, hides toggled-off types,
  passes the category to the icon.
- useMapAnalysisConfig: persisted config.nodeTypes (defaults all
  visible; merged into stored configs for forward-compat).
- NodeTypeFilterControl: toolbar popover of per-type checkboxes.
- MapLegend: a Node Types section rendering each glyph.
- sourceRoutes: expose MeshCore advType in /api/sources/:id/nodes
  (was hardcoded away); it flows through the unified-node merge.
- en.json: map.nodeType.* keys with English fallbacks in code.

The feature request's "Observer" maps to the real MeshCore Sensor
advert type (there is no Observer type in the protocol).

Tests: nodeTypeCategory.test.ts (36 cases); full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(map): MeshCore node-type icons + map filtering (#3546)

Tasks:

  • Gather context
  • Read changed files
  • Analyze code quality and potential issues
  • Post review feedback

Overall Assessment

This is a well-structured, well-tested PR. The "single source of truth" design principle in nodeTypeCategory.ts is the right call — it prevents icon/filter disagreement. A few observations below, ranging from a potential bug to minor style suggestions.


Issues

🐛 Potential Bug: isMeshCore detection heuristic can misclassify Meshtastic nodes

src/utils/nodeTypeCategory.ts:71

const isMeshCore = !!node.isMeshCore || typeof node.advType === 'number';

If a Meshtastic node somehow arrives with an advType field set to a numeric value (e.g., 0 from a future schema migration, or a join artifact), it will be wrongly treated as MeshCore. The fallback in sourceRoutes.ts:971 shows that MeshCore nodes are always emitted with advType: typeof n.advType === 'number' ? n.advType : 0 — meaning they always emit advType: 0 even when the true type is unknown, and 0 is also the default Meshtastic node role. This could be a problem if both protocols share a node store.

The check typeof node.advType === 'number' effectively means "any node with advType=0 gets treated as MeshCore with category standard" rather than as a Meshtastic node with a role. The explicit isMeshCore: true flag should be the primary guard, with advType only used as a hint when isMeshCore is missing. Consider:

const isMeshCore = !!node.isMeshCore;

and derive advType separately, only when isMeshCore is true. The current test at line 26 explicitly tests and validates the advType-only-heuristic, so this is a documented behavior — but it's worth flagging if MeshCore and Meshtastic nodes ever share a data path.

🐛 someHidden pluralization is brittle

src/components/MapAnalysis/NodeTypeFilterControl.tsx:23

t('map.nodeType.someHidden', '{{count}} type hidden', { count: hidden.length })

When hidden.length > 1, the label reads "2 type hidden" (singular). The en.json key at map.nodeType.someHidden doesn't use i18next's _plural suffix or count-based interpolation for pluralization. This should read "{{count}} types hidden" for the plural, or use i18next's count pluralization feature (map.nodeType.someHidden_one, map.nodeType.someHidden_other).


Code Quality Notes

mapIcons.ts — duplicated tower SVG

src/utils/mapIcons.ts:179–196

The repeater/tower SVG in roleGlyphInnerSvg() (lines 16–24) is duplicated verbatim in the isRouter branch of the MeshMonitor-style markerSvg (lines 183–195), minus the wrapping comment. Now that the router icon goes through roleGlyphInnerSvg('repeater', color), the isRouter code path is only reached when roleCategory === undefined or roleCategory === 'standard' on a Meshtastic ROUTER node.

This means a Meshtastic ROUTER node gets roleCategory = 'repeater' (from getNodeTypeCategory), so the roleInner path fires and the isRouter path is dead code. The isRouter parameter on createNodeIcon is now only reached when roleCategory is 'standard' and isRouter is true — which can't happen because ROUTER maps to 'repeater'. Consider removing isRouter from createNodeIcon's signature (or marking it @deprecated).

MapLegend.tsx — legend shows node-type section when markers are on, regardless of filter state

src/components/MapAnalysis/MapLegend.tsx:95–104

The Node Types legend section is always shown when showMarkers is true, even if all categories are standard (no MeshCore nodes are present). This is a minor UX issue — operators on pure-Meshtastic setups will see a "Node Types" legend section with four icons they'll never see on the map. Consider gating this section on whether any non-standard category is actually present in the visible node set, or accept the always-visible behavior as a feature.

MapAnalysisConfig.nodeTypes type vs. usage

src/hooks/useMapAnalysisConfig.ts:43

nodeTypes: Record<NodeTypeCategory, boolean>;

The type says every key is always present, but nodePassesTypeFilter in nodeTypeCategory.ts:92 uses enabledByCategory[getNodeTypeCategory(node)] !== false which safely handles missing keys. The Partial<Record<NodeTypeCategory, boolean>> parameter type on nodePassesTypeFilter also mismatches the MapAnalysisConfig.nodeTypes non-partial type. This is harmless (TS accepts Record<K,V> where Partial<Record<K,V>> is expected), but the design intent around "missing = visible" is worth documenting on the MapAnalysisConfig interface — Partial<Record<NodeTypeCategory, boolean>> would more accurately represent the intent.

dangerouslySetInnerHTML with dynamic color in MapLegend

src/components/MapAnalysis/MapLegend.tsx:18–24

The color variable is a hardcoded constant ('#6698f5'), so there is no XSS risk here. However, roleGlyphInnerSvg accepts a color parameter which elsewhere accepts the hop-color (a hex value generated in-process). This is fine. Worth noting for future callers that roleGlyphInnerSvg output is embedded in SVG via dangerouslySetInnerHTML — if color ever comes from user input, this would need sanitization.


Positive Highlights

  • Single source of truthgetNodeTypeCategory drives both the visible icon and the filter, so they can never disagree. Clean design.
  • load() merges stored nodeTypes with defaults (src/hooks/useMapAnalysisConfig.ts:95) — adding a new category in a future version won't break existing persisted configs (new category defaults to visible).
  • Pure function + 36 test casesgetNodeTypeCategory is pure and thoroughly tested including edge cases (null role, garbage strings, missing isMeshCore).
  • i18n fallbacksmeta.label English fallbacks on every category meta entry means untranslated locales don't break.
  • Backend change is minimal and safe — just exposing advType on the existing MeshCore node shape in sourceRoutes.ts:971.

Summary

Two items worth addressing before merge:

  1. The pluralization bug on "{{count}} type hidden" (cosmetic but user-visible).
  2. The dead-code isRouter path in createNodeIcon — not a bug, but the duplication should be resolved to prevent confusion.

The isMeshCore heuristic concern is low-risk given the current data paths, but worth a code comment explaining why the typeof advType === 'number' secondary check is intentional.

@Yeraze Yeraze merged commit 8e52ec5 into main Jun 19, 2026
19 checks passed
@Yeraze Yeraze deleted the feature/meshcore-map-node-types branch June 19, 2026 18:56
@Yeraze Yeraze mentioned this pull request Jun 19, 2026
Yeraze added a commit that referenced this pull request Jun 19, 2026
Finalize the 4.11.0 release (from 4.11.0-rc2) and bring documentation up to
date with everything new since 4.10.

Version bump across all five files: package.json, package-lock.json,
helm/meshmonitor/Chart.yaml, desktop/src-tauri/tauri.conf.json,
desktop/package.json.

Documentation:
- CHANGELOG: finalize the Unreleased section as [4.11.0], adding the
  previously-missing entries — headline MeshCore virtual node (#3540),
  MeshCore path-by-repeater-name (#3550), MeshCore map icons + filtering
  (#3563), per-node Hide from Map (#3565), telemetry time-range selector
  (#3530), newer AirQualityMetrics fields (#3517), UI/map unification
  (#3561), plus the bug-fix and documentation changes since 4.10.4.
- virtual-node.md: document the new MeshCore Virtual Node alongside the
  Meshtastic one (default port 5000, per-source enablement, admin-command
  safety, MeshCore app setup, troubleshooting) + a two-variants intro note.
- configuration/index.md, docs/index.md, README.md: mention the MeshCore
  Virtual Node in the Virtual Node feature blurbs.
- CLAUDE.md: refresh stale version header (4.10.0 -> 4.11.0) and migration
  count (84 -> 92, latest 092_add_hide_from_map_to_nodes).

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.

[FEAT] MeshCore node type visualization and filtering on map

1 participant