fix(position): surface per-fix SNR/hops in position history for direct hears (#3590)#3593
Conversation
…t hears (#3590) The map position-history tooltip never showed SNR for nodes heard directly (zero hops), even though the packet monitor showed SNR was present. Two issues: 1. The `/api/nodes/:nodeId/position-history` handler grouped lat/lon telemetry rows but dropped the per-fix `rxSnr`/`hopStart`/`hopLimit` columns (added in migration 089 for #3492). The frontend tooltip was already wired to render "Heard directly (0 hops)" + SNR, but the data never reached it. The pivot logic is extracted into `pivotPositionHistory()` and now carries SNR/hops through, treating the firmware -128 "no SNR" sentinel as absent. 2. The POSITION_APP handler recorded SNR with a `snr && snr !== 0` truthiness guard, which silently dropped a legitimate 0.0 dB SNR — common for a node heard directly. Switched to the `!= null && !== -128` sentinel check used by the central packet path, so direct hears (incl. 0 dB) update both the per-fix position metadata and the node's last-known SNR. Adds unit tests for the pivot covering the 0 dB case, the -128 sentinel, and field omission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review — fix(position): surface per-fix SNR/hops in position history (#3590)
This is a clean, well-scoped bug fix. The root-cause analysis in the PR description is accurate and complete. A few notes: StrengthsExtraction of The sentinel check fix ( Test coverage (10 cases in Issues Found1. Inconsistent fix:
|
The central processMeshPacket snr_local telemetry path still used a truthiness guard (`rxSnr && rxSnr !== 0`) which silently dropped a legitimate 0 dB SNR from a directly-heard node, contradicting the issue's "any directly-heard packet of any type should update the node's last-known SNR". Switch to the firmware-sentinel check (`!= null && !== -128`) already used in the position handler. Addresses claude-review finding on PR #3593. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review — fix(position): surface per-fix SNR/hops in position history (#3590)
This is a well-scoped, two-commit fix that correctly addresses both root causes described in the PR. The prior automated review raised several concerns — here is an updated assessment after reading the actual code on the branch. What was previously flagged vs. what's actually thereIssue 1 (previous review): The second commit ( if (meshPacket.rxSnr != null && meshPacket.rxSnr !== -128) {with an explanatory comment referencing #3590. The old Issue 2 (previous review): Optional fields not omitted when absent — still present, but truly cosmetic. Issue 3 (previous review): Remaining minor observations1. const snrValue = (rxSnr !== undefined && rxSnr !== null && rxSnr !== 0)
? rxSnr.toFixed(1)
: 'N/A';This is in the auto-responder message templating path, not in the telemetry write path, so it is unrelated to the bug being fixed. However it will display 2. const rawPosRxSnr = meshPacket.rxSnr ?? (meshPacket as any).rx_snr ?? undefined;
const posHopStart = meshPacket.hopStart ?? (meshPacket as any).hop_start ?? undefined;These snake_case fallbacks suggest some callers pass un-camelCased protobuf objects. The 3. Positive notes
SummaryThe core fix is correct and complete. The previous automated review's primary concern (the |
Resolve migration-number collision: renumber the dead-drop migration 094_create_dead_drop -> 095_create_dead_drop (keeping main's 094_add_meshcore_node_favorite). Update migrations.ts registry, migrations.test.ts (count 95, last=create_dead_drop), and the migration's internal Postgres/MySQL export names. meshtasticManager.ts auto-merged cleanly (main's Yeraze#3593/Yeraze#3598/Yeraze#3600 changes + the mailbox dispatch coexist). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
Summary
Closes #3590.
When a node was heard directly (zero hops), SNR was present in the packet monitor but never appeared in the map position-history tooltip. There were two root causes:
1. Read path stripped the per-fix SNR/hops
The
/api/nodes/:nodeId/position-historyhandler (used by the map history trail) grouped the per-metric telemetry rows into per-fix position objects but never read therxSnr/hopStart/hopLimitcolumns that the position handler stamps onto the lat/lon rows (added in migration 089 for #3492). The frontend tooltip (mapHelpers.tsx) was already wired to renderHeard directly (0 hops)+ SNR — the data simply never reached it.The grouping logic is extracted into a pure
pivotPositionHistory()helper (src/server/utils/positionHistoryPivot.ts) that now carries SNR/hops through, treating the firmware-128"no SNR" sentinel as absent.2. Write path dropped a legitimate 0 dB SNR
processPositionMessageProtobufrecorded SNR with ameshPacket.rxSnr && meshPacket.rxSnr !== 0truthiness guard, which silently dropped an SNR of exactly0.0 dB— common for a node heard directly. This is replaced with the!= null && !== -128sentinel check already used by the central packet path (processMeshPacket), so directly-heard packets (including 0 dB) correctly update both the per-fix position metadata and the node's last-known SNR. The per-fixposRxSnris likewise normalized so-128is never written.Tests
src/server/utils/positionHistoryPivot.test.ts(10 cases) covering: zero-hop SNR capture, the 0 dB preservation (the core bug), the-128sentinel stripping, null/undefined SNR, field omission when absent, incomplete-fix filtering, oldest-first sort, and metadata coming from either the lat or lon row.mainare unchanged).🤖 Generated with Claude Code