fix(neighbor-info): keep links when neighbor lastHeard is NULL but NI report is fresh#3026
Conversation
…port is fresh PR #2615 (zombie-node fix) intentionally leaves `lastHeard=NULL` on placeholder neighbor rows we've only learned about second-hand through a NeighborInfo packet. That was correct in isolation, but the read-side filter in both /api/neighbor-info (server.ts) and /api/sources/:id/neighbor-info (sourceRoutes.ts) rejects any row whose neighbor.lastHeard is NULL or older than the cutoff, so every indirect-neighbor link gets dropped on read. This change keeps the reporter freshness check (we must have directly heard from the reporting node), but falls back to the NeighborInfo record's own `timestamp` / firmware-supplied `lastRxTime` when the neighbor's `lastHeard` is missing or stale. `timestamp` is stored in ms (set via `Date.now()` in meshtasticManager), `lastRxTime` is firmware seconds — handled accordingly. Tests: - src/server/routes/sourceRoutes.neighbor-info.test.ts — fixture timestamp corrected to ms (matches real DB shape); existing staleness/null tests retargeted at the reporter side; new cases cover neighbor.lastHeard NULL + fresh NI report (kept), NULL + stale NI report (dropped), and NULL + fresh lastRxTime (kept). - src/server/server.neighbor-info-position.test.ts — inline shadow filter synced with the real handler so the test mirror does not drift. Refs #3025 Authored by Merlin 🪄 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: NeighborInfo NULL lastHeard Filter FixTodo List:
✅ Code Quality AssessmentOverall Implementation Quality: ExcellentThe implementation is well-crafted with clear logic, comprehensive documentation, and thorough test coverage. The fix correctly addresses the regression while maintaining the original security intent. 🔍 Detailed ReviewFiltering Logic Changes (server.ts:4152-4164 & sourceRoutes.ts:705-717)✅ Strengths:
🔧 Technical Implementation: // Step 1: Require fresh reporter (security check)
if (!ni.node?.lastHeard || ni.node.lastHeard < cutoffTime) return false;
// Step 2: Keep if neighbor is also fresh
if (ni.neighbor?.lastHeard && ni.neighbor.lastHeard >= cutoffTime) return true;
// Step 3: Fallback to NeighborInfo record freshness
const reportSec = Math.floor((ni.timestamp ?? 0) / 1000);
const rxSec = ni.lastRxTime ?? 0;
return Math.max(reportSec, rxSec) >= cutoffTime;This logic is sound and efficient - early returns prevent unnecessary computation. Test Coverage (sourceRoutes.neighbor-info.test.ts:202-280)✅ Comprehensive Test Suite:
Test Quality Analysis:
Mirror Test Sync (server.neighbor-info-position.test.ts:157-161)✅ Excellent Maintenance Practice: 🚨 Issues FoundMinor Issues:
Performance Considerations:
🔒 Security Analysis✅ Security Posture Maintained:
📊 Performance Assessment✅ Performance Impact: Minimal
🧪 Test Coverage Evaluation✅ Excellent Coverage:
📝 Code Style & Maintainability✅ High Quality:
🔄 Follow-up Recommendations
✅ Final AssessmentThis is a high-quality fix that addresses a real regression while maintaining security properties. The implementation is clean, well-tested, and follows best practices. The author's honesty about potential remaining issues (#3025) and proper use of "Refs" vs "Fixes" shows excellent engineering judgment. Recommendation: ✅ Approve |
System Test ResultsMeshMonitor System Test ResultsTest Run: 2026-05-15 07:55:26 EDT Test Summary
✅ Overall Result: PASSEDAll deployment configurations are working correctly! Test DetailsConfiguration Import:
Quick Start Test:
Security Test:
V1 API Test:
Reverse Proxy Test:
Reverse Proxy + OIDC Test:
Virtual Node CLI Test:
Backup & Restore Test:
Database Migration Test:
DB Backing Consistency Test:
|
Summary
Addresses one cause of #3025 — the NULL-
lastHeardfilter regression introduced in v4.0.0 by #2615 (zombie-node fix). NeighborInfo packets are still being parsed and saved correctly (🔗 Saved neighbor:log lines confirm this), but the read-side filter on both/api/neighbor-infoand/api/sources/:id/neighbor-infowas rejecting every indirect-neighbor link because PR #2615 intentionally leaveslastHeard=NULLon placeholder rows for nodes we've only heard about via NeighborInfo.This PR keeps the reporter-side freshness requirement but falls back to the NeighborInfo record's own freshness signals (
timestamp/lastRxTime) when the neighbor'slastHeardis missing or stale.Root cause
The filter (introduced when the zombie guard landed) reads:
Combined with the zombie guard's behavior in
meshtasticManager.ts(placeholder neighbors written withlastHeard=NULL), this drops every NeighborInfo row whose neighbor side is a node we haven't directly heard from — which is most of them.The fix
Both filters now:
ni.node) to have a freshlastHeard— we must have directly heard from the node making the claim.lastHeard, keep the row (as before).Math.max(timestamp/1000, lastRxTime)againstcutoffTime.timestampis stored in ms (Date.now()inmeshtasticManager.ts:6710),lastRxTimeis firmware seconds.Files changed
src/server/server.ts:4152—GET /api/neighbor-infofilter.src/server/routes/sourceRoutes.ts:705—GET /api/sources/:id/neighbor-infofilter (the one the Messages tab actually uses).src/server/routes/sourceRoutes.neighbor-info.test.ts— fixturetimestampcorrected from seconds to ms (matches the real write path); existing staleness/NULL-lastHeard tests retargeted at the reporter side; new cases covering NULL-neighbor + fresh NI report (kept), NULL-neighbor + stale NI report (dropped), and NULL-neighbor + freshlastRxTime(kept).src/server/server.neighbor-info-position.test.ts— inline shadow filter synced with the real handler so the test mirror doesn't drift.Honesty caveat
The reporter (
@SyselMitY) followed up on #3025 indicating their case also involves neighbors that do have a freshlastHeard, so this PR likely doesn't fully close out #3025. Use ofRefs #3025(notFixes) is intentional — further DB inspection has been requested from the reporter, and there may be a second cause (sourceId scoping ingetNodesByNums, possibly elsewhere) still to nail down.Out of scope (deliberately)
GET /api/neighbor-info/:nodeNum— no node-age filter there, left alone.meshtasticManager.ts— the NULLing oflastHeardon placeholder rows is intentional and stays.Follow-ups
databaseService.nodes.getNodesByNums(used bysourceRoutes.ts:681) does not appear to scope tosourceId. If a node number is shared across sources, the per-source neighbor-info endpoint could pick uplastHeardfrom the wrong source's row. Worth a separate look as part of [BUG] Meshtastic NeigborInfo: Response received but not shown in UI #3025 investigation.Test plan
npm run typecheck— cleannpm run build— greennpx vitest run— full suite 4916/4916 pass (21/21 on targeted neighbor-info files)@SyselMitY) on a live build that NeighborInfo links now show in the UI for their meshAuthored by Merlin 🪄