Skip to content

fix(neighbors): align live map "Show Neighbor Info" with the Map Analysis Neighbors view#3560

Merged
Yeraze merged 1 commit into
mainfrom
fix/neighbors-map-mismatch
Jun 19, 2026
Merged

fix(neighbors): align live map "Show Neighbor Info" with the Map Analysis Neighbors view#3560
Yeraze merged 1 commit into
mainfrom
fix/neighbors-map-mismatch

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Problem

A user reported (via Discord) that the Neighbors drawn on the Map Analysis page don't match the Show Neighbor Info links on the main source map — even with Show Route Segments and Show Traceroute disabled. The live map showed many more links than the analysis view.

Root cause

Both views read the same neighbor_info table (each NeighborInfo packet deletes-then-reinserts the reporter's full list with a fresh timestamp, so the table holds each reporter's latest list). They diverged purely in the freshness filter:

View Endpoint Freshness gate
Map Analysis "Neighbors" /api/analysis/neighbors record timestamp within lookback (report recency)
Live map "Show Neighbor Info" /api/sources/:id/neighbor-info (+ legacy /api/neighbor-info) endpoint nodes' lastHeard within maxNodeAgeHours

So the live map kept showing a node's stale last-known neighbor list for as long as the node stayed otherwise active (still sending position/telemetry), surfacing far more links than the analysis view and misrepresenting how recently each RF link was observed.

Fix

Per maintainer decision (report-time is the canonical "neighbor" definition), switch the live-map endpoints to report-time freshness — keep an edge only when its NeighborInfo record timestamp is within maxNodeAgeHours — matching the analysis view.

Applied to both /api/sources/:id/neighbor-info (used by the live map) and the legacy /api/neighbor-info for consistency.

Tests

Notes / possible follow-up

  • The window size is still per-view (maxNodeAgeHours for the live map vs the analysis page's LOOKBACK selector); set both to the same value to compare like-for-like. Semantics (the filtered field) now match.
  • The analysis endpoint applies only source-level permission scoping, while the live map also applies per-channel viewOnMap filtering. For non-admins this can still differ — out of scope here, flagging for a possible future pass.

🤖 Generated with Claude Code

…Neighbors view

The live source map and the Map Analysis page disagreed on which neighbor
links to draw. Both read the same neighbor_info table, but applied different
freshness filters:

- Map Analysis "Neighbors" (/api/analysis/neighbors) windows by the
  NeighborInfo record's own `timestamp` (report recency).
- The live map (/api/sources/:id/neighbor-info and the legacy
  /api/neighbor-info) gated on the endpoint *nodes'* `lastHeard`, so a node's
  stale last-known neighbor list kept showing for as long as the node stayed
  otherwise active (still sending position/telemetry). That surfaced far more
  links than the analysis view and misrepresented how recently each RF link
  was actually observed.

Switch the live-map endpoints to report-time freshness (record `timestamp`
within maxNodeAge), matching the analysis definition. Because each NeighborInfo
packet deletes-then-reinserts the reporter's full list with a fresh timestamp,
a fresh record inherently means we heard the reporter within the window.
Keying off the record timestamp (instead of node `lastHeard`) also naturally
preserves indirect-neighbor links whose neighbor row has a null lastHeard
(#3025/#2615), since lastHeard is no longer consulted. Firmware-supplied
`lastRxTime` is also dropped from the predicate: it is subject to device clock
skew and the analysis view does not use it, so it could otherwise reintroduce
divergence.

Tests updated to assert report-time semantics (stale report dropped even when
endpoint nodes are still active; fresh report kept even when a node's lastHeard
is stale/null).

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: fix(neighbors): align live map "Show Neighbor Info" with Map Analysis Neighbors view

  • Gathering context and reading changed files
  • Reviewing code quality and correctness
  • Checking test coverage
  • Posting final review feedback

Summary

The fix is well-motivated and correctly implemented. Both endpoints now filter neighbor edges using the NeighborInfo record's timestamp (packet-receipt time in ms) instead of the endpoint nodes' lastHeard, which was the source of the divergence between the live map and the Map Analysis view.


Code Quality

sourceRoutes.tsGET /:id/neighbor-info (lines 1195–1211)

The new filter is clean and correct. One minor ordering note: the .filter(ni => reportSec >= cutoffTime) runs after the .map(ni => {...}) that adds the node/neighbor objects, and then .map(({ node, neighbor, ...rest }) => rest) strips them again. A small potential optimization would be to apply the timestamp filter before the enrichment map (since you already have access to ni.timestamp on the raw record), but this doesn't affect correctness and the current approach avoids restructuring the logical pipeline — acceptable here.

neighborInfoRoutes.ts — legacy GET / (lines 42–52)

The same logic is applied consistently. Note that this file calls databaseService.getLatestNeighborInfoPerNodeScoped (line 12) — a non-Async-suffixed method — which per CLAUDE.md conventions suggests it may be synchronous or a deprecated shim. If this is the legacy route and will be removed or replaced by the source-scoped equivalent, it may not be worth polishing further.

timestamp unit assumption

Both endpoints assume timestamp is stored in milliseconds (they do Math.floor((ni.timestamp ?? 0) / 1000) to get seconds). The PR description confirms this. The test file's makeNeighborRecord also uses Date.now() (ms). This is consistent throughout and is also how meshtasticManager writes the value. No issue, but worth keeping documented for future contributors.

Null-safety on timestamp

(ni.timestamp ?? 0) safely handles nulls by treating them as epoch 0, which will always fail the freshness check — correct behavior (null timestamp = no known report time = don't show the edge).


Potential Issue: filter order causes unnecessary enrichment

In sourceRoutes.ts (lines 1160–1212), the channel-permission filter runs first (good), but the timestamp filter runs after the per-record enrichment .map(ni => {...}) that does the node lookups, position resolution, and transport classification. Since allNodeNums and nodeMap are already batch-fetched before the pipeline (line 1142), the node lookup itself is O(1) from the Map — this is fine in practice. No real performance concern.


Test Coverage

The updated tests in sourceRoutes.neighbor-info.test.ts are thorough and lock in the exact semantics of the fix:

The server.neighbor-info-position.test.ts update (lines 149–156) correctly mirrors the live-route filter inline. This test file is a standalone mock-app test — the filter copy is necessary to keep it honest against the real route behavior.


Minor Observations

  1. neighborInfoRoutes.ts line 12: getLatestNeighborInfoPerNodeScoped is called without await. This is consistent with the pre-existing code, but if this method was ever made async it would silently break. Since this file appears to be the legacy endpoint (not per-source), this is an existing pre-PR issue, not introduced here.

  2. maxNodeAgeHours settings key differs between routes: sourceRoutes.ts reads 'maxNodeAgeHours' (line 1130), while neighborInfoRoutes.ts reads 'maxNodeAge' (line 14). This discrepancy existed before this PR — both default to 24h when the setting is absent so behavior matches, but it's worth a follow-up to normalize the key name.

  3. No change to the GET /api/analysis/neighbors analysis endpoint — correct; the PR says both views now use the same filtering field. No need to touch the analysis endpoint.


Verdict

Approve. The root cause is correctly identified and the fix is minimal, consistent, well-commented, and well-tested. No bugs or security issues introduced. The pre-existing key-name discrepancy (maxNodeAgeHours vs maxNodeAge) is out of scope for this fix but worth tracking.

@Yeraze

Yeraze commented Jun 19, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review. All three observations are non-blocking and I'm leaving the code as-is:

  1. Filter order (timestamp after enrichment): Agreed it's a no-op for perf — nodeMap is batch-fetched once and lookups are O(1), so reordering would only churn the pipeline. Left as-is.
  2. getLatestNeighborInfoPerNodeScoped non-await / legacy route: Pre-existing, and that route (/api/neighbor-info) isn't called by the current frontend. Out of scope here.
  3. Settings-key discrepancy (maxNodeAgeHours vs legacy maxNodeAge): Real but pre-existing and only affects the unused legacy route (both default to 24h). Flagging as a follow-up rather than widening this PR's scope.

Ready to merge from my side once you're happy.

@Yeraze Yeraze merged commit aee118d into main Jun 19, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/neighbors-map-mismatch branch June 19, 2026 17:11
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