Skip to content

feat(map-analysis): rich multi-source node popups matching the Unified map#3692

Merged
Yeraze merged 2 commits into
mainfrom
feat/mapanalysis-popups-multisource
Jun 24, 2026
Merged

feat(map-analysis): rich multi-source node popups matching the Unified map#3692
Yeraze merged 2 commits into
mainfrom
feat/mapanalysis-popups-multisource

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Brings the Map Analysis node-marker popups up to parity with the Unified/Dashboard map, and makes the page properly multi-source-aware.

1. Popups look like the Unified map

Map Analysis markers now render the same rich DashboardNodePopup (the .node-popup-* card — name/short name header, role, hops, hardware, battery, SNR, altitude, position, last-heard) instead of the bare inline <strong>name</strong> / Source / Hops popup.

2. Multi-source nodes list all their sources

DashboardNodePopup renders the "Seen by N sources" list for unified-merged nodes. Map Analysis uses the same useDashboardUnifiedData merge (which attaches node.sources), so a node reported by several sources now shows each one, with rows that link to that source's view (mirrors DashboardPage.handleNodeSourceSelect).

3. Source selection handles multi-source nodes

The top-of-page source filter now keeps a node visible when any of its reporting sources is enabled — it checks node.sources rather than only the node's primary sourceId. Previously a multi-source node whose primary source wasn't selected would vanish even if one of its other sources was enabled.

Testing

  • MapAnalysisCanvas.test: new assertion that the popup lists every reporting source ("Seen by 2 sources" + both names); wrapped in MemoryRouter and mocks useDisplaySettings for the new popup.
  • Full Map Analysis + MeshCoreSourcePage suites pass; tsc clean.

Builds on #3687 (now merged).

🤖 Generated with Claude Code

…d map

Map Analysis node markers now render the same DashboardNodePopup used by the
Unified/Dashboard map (rich .node-popup card) instead of the bare inline popup,
including the 'Seen by N sources' list for nodes reported by multiple sources.
Source rows navigate to that source's view (mirrors DashboardPage).

The top-of-page source selection now keeps a node visible when ANY of its
reporting sources is enabled — the filter checks node.sources rather than just
the primary sourceId, so unified-merged multi-source nodes no longer vanish
when only one of their sources is selected.

Test: MapAnalysisCanvas asserts the popup lists every reporting source; wrapped
in MemoryRouter and mocks useDisplaySettings for the new popup.

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

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(map-analysis): rich multi-source node popups matching the Unified map

Tasks:

  • Gather context and read changed files
  • Analyze code quality and best practices
  • Check for potential bugs or issues
  • Review test coverage
  • Post final review

Overview

This is a clean, well-scoped PR that achieves its stated goals. The approach — reusing DashboardNodePopup and useDashboardUnifiedData rather than duplicating logic — is the right architectural call. A few items worth flagging:


Bug / Correctness Issues

1. Source filter uses a shadowed variable (sourceIds) — NodeMarkersLayer.tsx:169

// Line 138 — outer scope:
const sourceIds = sourceList.map((s) => s.id);

// Line 169 — inside .filter(), SHADOWS the outer `sourceIds`:
const sourceIds = node.sources && node.sources.length > 0
  ? node.sources.map((s) => s.sourceId)
  : (node.sourceId ? [node.sourceId] : []);
return sourceIds.some((id) => config.sources.includes(id));

The inner const sourceIds shadows the outer one, which is fine at runtime but will trigger ESLint's no-shadow rule if it's enabled in this project (worth checking). A rename to nodeSourceIds (or similar) would eliminate any ambiguity and make the intent immediately obvious.

2. Hop-count lookup uses only the node's primary sourceIdNodeMarkersLayer.tsx:198

const sourceId = n.sourceId ?? '';
const hopVal = hopByKey.get(`${sourceId}:${Number(n.nodeNum)}`);

For a unified-merged node with multiple sources, n.sourceId is the primary source. If the hop-count data came back for one of the secondary sources instead, hopVal will be undefined and the popup will show no hops. This is a pre-existing gap (not introduced here), but since this PR now prominently exposes hops in the popup for all multi-source nodes it may become more noticeable. Worth a follow-up issue if not already tracked.


Code Quality

3. config.sources vs sourceIds naming could be clearer

Within filteredNodes the name sourceIds (inner) refers to the node's own reporting sources, while config.sources is the user's selection filter. A comment (or rename) would help the next reader.

4. Source-filter fallback for MeshCore nodes

MeshCore nodes have isMeshCore: true and typically no nodeId for Meshtastic. The filter at line 169 falls back to node.sourceId when node.sources is empty. If node.sourceId is also undefined (which is possible for some MeshCore entries), the node will always be filtered out when config.sources is non-empty — even though config.sources contains its source. Worth verifying against a real MeshCore node.


Test Coverage

The new test in MapAnalysisCanvas.test.tsx is solid. A few observations:

  • Good: The test exercises the most important new behaviour — "Seen by 2 sources" renders inside the popup, and both source names appear.
  • Missing: No test for the updated source filter logic (the node.sources.some(...) path). It would be straightforward to add a second test that mocks a node with sources: [{sourceId: 'b', ...}] but sourceId: 'a', sets config.sources = ['b'], and asserts the marker is still rendered.
  • The useDisplaySettings mock added to SettingsContext is correctly placed — good catch that DashboardNodePopup pulls from it.

Minor Nits

  • The comment on line 245–250 is helpful but a little long for inline JSX. A single-line summary would keep the JSX readable.
  • useMapAnalysisCtx is destructured at line 52 but config.sources is referenced in several separate useMemo/filter blocks — no issue, just noting that a future reader may want to destructure config itself for clarity.

Summary

✅ Correct approach — reuses existing unified data hooks and popup component.
✅ Source-filter fix (checking node.sources instead of only primary sourceId) is the right behaviour.
✅ Test suite covers the main new rendering path.
⚠️ Shadowed sourceIds variable is a minor style/lint hazard worth renaming.
⚠️ Hop-count lookup is primary-source-only for multi-source nodes (pre-existing; may be more visible now).
⚠️ Filter coverage test for the multi-source path is missing.

Overall the PR is in good shape and ready to merge after considering the above.

useDashboardUnifiedData only stamps the per-node `sources` array when given
full DashboardSource objects; bare source-id strings get none. NodeMarkersLayer
was passing string ids, so the popup's 'Seen by N sources' list and the
multi-source visibility filter had nothing to work with. Pass the full source
objects (which also carry protocol/type for correct MeshCore/Meshtastic badges).

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

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(map-analysis): rich multi-source node popups matching the Unified map

Tasks:

  • Gather context and read changed files
  • Analyze code quality and best practices
  • Check for potential bugs or issues
  • Review test coverage
  • Post final review

Overview

This is a clean, well-scoped PR that achieves its stated goals. The approach — reusing DashboardNodePopup and useDashboardUnifiedData rather than duplicating logic — is the right architectural call. The second commit addresses the node.sources population issue from the prior review. Here is an updated assessment:


Bug / Correctness Issues

1. Shadowed sourceIds variable — NodeMarkersLayer.tsx:172

// Line 138 — outer scope:
const sourceIds = sourceList.map((s) => s.id);

// Line 172 — inside .filter(), SHADOWS the outer `sourceIds`:
const sourceIds = node.sources && node.sources.length > 0
  ? node.sources.map((s) => s.sourceId)
  : (node.sourceId ? [node.sourceId] : []);
return sourceIds.some((id) => config.sources.includes(id));

no-shadow is not in the ESLint config, so this won't fail CI — but it is a readability hazard. Renaming the inner variable to nodeSourceIds would make the intent obvious at a glance.

2. Hop-count lookup uses only the node's primary sourceIdNodeMarkersLayer.tsx:200-201

const sourceId = n.sourceId ?? '';
const hopVal = hopByKey.get(`${sourceId}:${Number(n.nodeNum)}`);

For unified-merged nodes, n.sourceId is the primary source. If hop data only arrived for a secondary source, hopVal will be undefined and the popup will show no hops. This is a pre-existing gap not introduced here, but the new rich popup makes it more visible. A follow-up issue would be appropriate if not already tracked.

3. MeshCore node filter fallback

The filter at line 172–175 falls back to node.sourceId when node.sources is empty. For MeshCore nodes where both node.sources and node.sourceId might be absent, the node will silently fail the filter even when its source is in config.sources. Worth verifying against a live MeshCore source.


Code Quality

The comment block at lines 248–253 (inside JSX) is a bit verbose — a single-line summary referencing the issue numbers would keep the JSX scannable. Minor nit.


Test Coverage

The new test at MapAnalysisCanvas.test.tsx:120-127 is solid and covers the most important new behaviour. Two observations:

  • Good: Tests that DashboardNodePopup correctly renders "Seen by 2 sources" with both source names in a multi-source scenario. The useDisplaySettings mock is correctly added to satisfy DashboardNodePopup's hook dependency.
  • Missing: No test for the updated source filter logic (the node.sources.some(...) path at line 175). A test that sets config.sources = ['b'] while node.sourceId = 'a' (primary not selected) but node.sources includes b would confirm the fix actually works. This is the core behavioral change in the PR and currently has no regression coverage.

Summary

✅ Correct approach — reuses unified data hooks and popup component.
✅ Sources now passed as full objects so node.sources is populated (fix from second commit).
✅ Source-filter correctly checks all node.sources rather than primary sourceId only.
✅ Test covers the new "Seen by N sources" rendering path.
⚠️ Inner sourceIds shadows outer — rename to nodeSourceIds for clarity (no lint impact currently).
⚠️ Hop-count lookup is primary-source-only for multi-source nodes (pre-existing gap, now more visible).
⚠️ Source-filter behavioral fix has no unit test coverage.

The PR is in good shape. The shadowed variable rename is trivial and could be done before merge; the hop-count gap and missing filter test are reasonable to track as follow-ups.


@Yeraze Yeraze merged commit 8d480bd into main Jun 24, 2026
20 checks passed
@Yeraze Yeraze deleted the feat/mapanalysis-popups-multisource branch June 24, 2026 00:50
Yeraze added a commit that referenced this pull request Jun 26, 2026
* chore(release): bump version to 4.12.0

Finalize 4.12.0 (from 4.12.0-rc2): package.json, package-lock.json,
desktop/package.json, desktop/src-tauri/tauri.conf.json, helm/Chart.yaml.
Promote CHANGELOG [Unreleased] → [4.12.0].

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

* docs(4.12.0): document Automation Engine, MeshCore regions/scopes & UI additions

Release-audit of every PR since v4.11.5 surfaced documentation gaps. Brings the
docs site current with the 4.12.0 feature set:

- New page docs/features/automation-engine.md (+ sidebar nav) for the node/
  builder-based Automation Engine (#3653 et al). v-pre-wrapped so the {{ }}
  token syntax renders literally without breaking the VitePress build.
- meshcore.md: new Regions/Scopes section (#3667 et al), channel unread
  indicators, heard-repeaters badge, per-message route line, byte counter,
  on-demand telemetry polling, Define Path editor, repeater DM-hidden behavior.
- automation.md: corrected stale auto-favorite eligibility table (#3786/#3774).
- maps.md: traceroute default-keyed channel routing + channel dropdowns (#3723).
- map-analysis.md: rich multi-source node popups (#3692).
- device.md: unmessagable-node DM-hidden behavior (#3760).

Verified: vitepress build passes; tokens render literally.

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

* fix(migration): make migration 104 PostgreSQL channel_database rebuild atomic

Release-audit (PR #3753) flagged the tombstone-exhaustion rebuild path as a
data-availability risk: it DROPs the live channel_database and RENAMEs a fresh
copy into place, but the migration runner wraps each migration's statements in
no transaction, so each DDL auto-commits. A crash/connection drop in the
DROP→RENAME window would leave the database with no channel_database — and the
migration's own idempotency check (`'channel_database'::regclass`) would then
throw "relation does not exist" on the next startup, blocking recovery.

Wrap the rebuild in BEGIN/COMMIT with ROLLBACK on error. PostgreSQL DDL is
transactional, so any failure now rolls back to the original table intact.
(Channel keys/PSKs were already copied before the drop, so this hardens the
narrow availability window rather than fixing data loss. The path only fires at
≥1500 attnum tombstones — 1500+ restarts on the old migration 021 bug — so it's
rare, but the blast radius warranted the fix.)

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