Skip to content

fix(map): adaptive node-type filter per connected source protocol (#3610)#3614

Merged
Yeraze merged 1 commit into
mainfrom
fix/3610-node-type-filter-adaptive
Jun 22, 2026
Merged

fix(map): adaptive node-type filter per connected source protocol (#3610)#3614
Yeraze merged 1 commit into
mainfrom
fix/3610-node-type-filter-adaptive

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Problem

Closes #3610.

The Map Analysis node-type filter exposed five fixed MeshCore-centric categories (Companion / Repeater / Room Server / Sensor / Standard) regardless of which source types were connected. On a Meshtastic-only instance this was wrong:

  • Companion, Room Server, Sensor mapped to MeshCore advTypes only and were permanently empty.
  • Standard collapsed every non-Router Meshtastic role (CLIENT, CLIENT_MUTE, TRACKER, SENSOR, CLIENT_HIDDEN, ROUTER_LATE, CLIENT_BASE, TAK, …) into one indistinguishable bucket.

Fix — adaptive categories

The filter (and the analysis legend) now adapt to the protocol families actually in scope:

Connected sources Categories shown
Meshtastic-only Granular Meshtastic role categories (Router, Router Late, Tracker, Sensor, Client, Client Mute, Client Hidden, Client Base, TAK, …)
MeshCore-only Existing MeshCore advType categories (unchanged)
Both Union — MeshCore first, then Meshtastic

How it works

  • getNodeTypeCategory() now maps each Meshtastic device role to its own mt* category instead of folding into repeater/standard. Labels reuse the shared DEVICE_ROLES map so they match the node list.
  • categoriesForProtocols({ meshcore, meshtastic }) computes the ordered visible set; useVisibleNodeTypeCategories() derives the connected protocols from the sources in Map Analysis scope (config.sources, or all enabled sources).
  • Map glyphs are preserved via a new categoryGlyphFamily(): Meshtastic ROUTER / ROUTER_LATE / REPEATER draw the repeater tower, SENSOR draws the sensor glyph, client roles fall back to the default pin. roleGlyphInnerSvg/roleGlyphMarkerSvg resolve through it, so the marker a user sees still matches the checkbox.
  • MeshCore-only surfaces are unchanged: MeshCoreMap and the legacy MapLegend now iterate MESHCORE_CATEGORIES explicitly, so the [FEAT] MeshCore node type visualization and filtering on map #3546/feat(map): MeshCore node-type icons + map filtering (#3546) #3563 MeshCore icons/filter behavior is identical.

Persisted-state degradation

Toggles still key on stable category names and default to visible when missing. A stale false toggle for a category that drops out of scope (e.g. a MeshCore category after switching to Meshtastic-only) is inert and never permanently hides a node when the source mix changes. Covered by a dedicated test.

Tests

  • Extended nodeTypeCategory.test.ts: Meshtastic role→category mapping (all 13 roles), MeshCore advType mapping, categoriesForProtocols for Meshtastic-only / MeshCore-only / both / none, categoryGlyphFamily, sourceTypeProtocol, graceful-degradation case.
  • Extended mapIcons.test.ts: Meshtastic categories render via shared glyph family; client roles return empty glyph.
  • Full Vitest suite: success=true, 7101 passed, 0 failed, 0 failed suites (JSON reporter).
  • tsc -p tsconfig.server.json --noEmit and frontend tsc --noEmit: no new errors in changed files (only the pre-existing TelemetryChart / 57 frontend errors remain).

🤖 Generated with Claude Code

)

The Map Analysis node-type filter exposed five fixed MeshCore-centric
categories regardless of which source types were connected. On a
Meshtastic-only instance three options (Companion, Room Server, Sensor)
were permanently empty and every non-Router Meshtastic node collapsed
into a single "Standard" bucket.

The filter now adapts to the connected protocols:
- Meshtastic-only -> granular Meshtastic role categories (Router,
  Router Late, Tracker, Sensor, Client Base, etc.), labelled from the
  shared DEVICE_ROLES map.
- MeshCore-only -> the existing MeshCore advType categories.
- Both connected -> the union (MeshCore first, then Meshtastic).

getNodeTypeCategory now maps each Meshtastic role to its own `mt*`
category; map glyphs reuse the MeshCore silhouettes via a new
categoryGlyphFamily() so a Router still renders as a repeater tower.
The visible category set is derived from the sources in Map Analysis
scope (useVisibleNodeTypeCategories), so MeshCore-only surfaces keep
their original five categories. Persisted toggles key on stable
category names and default to visible, so a stale toggle for a category
that drops out of scope is inert and never permanently hides a node
when the source mix changes.

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

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review — fix(map): adaptive node-type filter per connected source protocol (#3610)

  • Gather context and understand the request
  • Read changed files
  • Review code quality and best practices
  • Check for potential bugs
  • Assess test coverage
  • Post review feedback

Summary

This is a well-structured fix for a real UX problem. The overall approach is sound: a single categorization function drives both the filter UI and map icons, the category set adapts to connected protocols, and the persisted state degrades gracefully when the source mix changes. Here are observations by area.


Code Quality

nodeTypeCategory.ts — clean and well-organized

The two-family design (MeshCoreCategory / MeshtasticCategory) is clear, and NODE_TYPE_CATEGORY_META with labelKey/label pairs is a good pattern for i18n with a safe English fallback.

Minor: categoryGlyphFamily has a redundant default branch

// src/utils/nodeTypeCategory.ts:148-174
switch (category) {
    case 'mtLostAndFound':
      return 'standard';
    default:
      return 'standard';   // ← unreachable; all 18 union members are covered
  }

MeshtasticCategory and MeshCoreCategory together cover all 18 members of NodeTypeCategory, so the default arm is dead code. TypeScript would flag this as an unreachable default with noImplicitOverride + exhaustive checks, but it's harmless. Consider removing it (or keeping it purely as a safety net with a comment).


Potential Bugs

1. isMeshCoreNode conflates advType: 0 with MeshCore identity

// src/utils/nodeTypeCategory.ts:199
export function isMeshCoreNode(node: CategorizableNode): boolean {
  return !!node.isMeshCore || typeof node.advType === 'number';
}

advType: 0 is explicitly checked in the test (line 29 in the test file) as a legitimate MeshCore node mapping to 'standard'. But if a Meshtastic node ever arrives with an advType: 0 field (e.g., from a schema migration or a noisy data source), it would be silently mis-categorized as MeshCore. Given the CategorizableNode interface already has isMeshCore as the canonical flag, it might be safer to gate purely on !!node.isMeshCore and use typeof node.advType === 'number' as a secondary signal only when isMeshCore is absent. The current approach is acceptable given actual data shapes, but worth a comment explaining why advType: 0 is still treated as MeshCore.

2. useVisibleNodeTypeCategories reacts to config.sources reference, not content

// src/components/MapAnalysis/useVisibleNodeTypeCategories.ts:39
}, [sources, config.sources]);

If config.sources is a new array reference on every render (common with Zustand slices or useState initializers that spread), this useMemo will re-run every render even when the IDs haven't changed. Worth checking how MapAnalysisContext produces config.sources; if it's an inline [], a shallow-equal comparison would silently cause excessive recalculation. This wouldn't cause incorrect behavior — just unnecessary work — but it's worth auditing.

3. createNodeIcon still has the legacy isRouter path that duplicates the repeater SVG

// src/utils/mapIcons.ts:202-219
} : isRouter ? `
    <svg ...>
      

Pre-#3610, isRouter drove the repeater glyph. Now roleCategory does for Meshtastic nodes. But isRouter is still evaluated as a fallback when roleCategory is falsy or 'standard'. If a caller passes isRouter: true without roleCategory, they get the repeater tower; if they also pass roleCategory: 'mtRouter', they get the glyph path. Callers that haven't been updated yet may produce the repeater tower through the old path, bypassing categoryGlyphFamily. Not a regression (it was like this before), but the two paths are now subtle to reason about. A follow-up to deprecate isRouter in favor of always supplying roleCategory would clean this up.


Design / Approach

sourceTypeProtocol — catch-all defaults to Meshtastic

// src/utils/nodeTypeCategory.ts:256-258
export function sourceTypeProtocol(type: string | undefined | null): 'meshcore' | 'meshtastic' {
  return type === 'meshcore' ? 'meshcore' : 'meshtastic';
}

This is intentional (unknown type → Meshtastic, the more common case), but a future 'serial', 'mqtt', or other non-meshcore source type would silently become Meshtastic. This is fine for now, but a comment noting the intent would help future maintainers.

categoriesForProtocols({ meshcore: false, meshtastic: false }) falls back to the full set

The rationale in the comment is correct (while loading, show everything so the control isn't empty), but this means a user whose sources are all disabled (not just loading) also sees the full set rather than an empty/disabled filter. This is the right UX trade-off given persisted-state semantics, but it's worth noting the ambiguity.


Performance

The useMemo in useVisibleNodeTypeCategories is well-placed — the categorization only recomputes when sources or config.sources changes. The filter in nodePassesTypeFilter is a simple map lookup (O(1)) and is safe to call per node per render. No performance concerns.


Security

No security concerns. SVG content rendered via dangerouslySetInnerHTML in MapLegend.tsx and MeshCoreMap.tsx is generated entirely from the static roleGlyphInnerSvg/roleGlyphMarkerSvg functions — no user data flows into it.


Test Coverage

Test coverage is excellent for the utility layer:

  • All 13 Meshtastic roles, all 5 MeshCore advTypes, all edge cases for getNodeTypeCategory
  • categoriesForProtocols for all four protocol combinations
  • categoryGlyphFamily for all three glyph families
  • Graceful degradation (stale toggle test at line 108)
  • roleGlyphMarkerSvg now covers Meshtastic categories via their glyph family

Gap: no unit test for useVisibleNodeTypeCategories — the React hook is only indirectly tested through the component. A test that mocks useDashboardSources with sources of known types and asserts the returned category list would catch regressions in the source-scoping logic. This isn't blocking but would be a good follow-up.

Gap: NodeTypeFilterControl has no test that exercises the adaptive category list or the "Show all" reset behavior.


Minor Nits

  • src/components/MapAnalysis/MapLegend.tsx:64-66: The legendCategories filter correctly excludes 'standard'-family glyphs from the legend, but the comment says "client/standard buckets render as the default pin" — mtTracker, mtTak, etc. also render as pins but aren't called out. The comment could be slightly more precise.
  • src/utils/nodeTypeCategory.ts:119-139: The alignment-heavy record literal (key: 'repeater', labelKey: ...) with varying trailing spaces may cause noise in future diffs if editors normalize whitespace. Consider letting the formatter handle it.

Verdict

Solid, well-scoped fix. The architecture is clean, the tests are thorough for the utility layer, and the graceful-degradation story for persisted state is correct. The isRouter legacy path in createNodeIcon and the missing hook test are the most notable follow-up items but are not blocking for merge.

@Yeraze Yeraze merged commit b792e63 into main Jun 22, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/3610-node-type-filter-adaptive branch June 22, 2026 00:16
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.

Map Analysis node type filter uses MeshCore labels on Meshtastic-only instances

1 participant