From 1ce55049dc116820f429e11a5e7b945e3753222c Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Sun, 5 Jul 2026 09:50:39 -0400 Subject: [PATCH] fix(map): show direct-heard SNR in node hover tooltip (#3925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The map node-circle hover tooltip only rendered the node name and hop count; it never surfaced SNR, even when the node was heard directly (0 hops) and an SNR value was available. This was inconsistent with: - the Nodes list card view (`hopsAway === 0 && snr != null` -> 📶 X.XdB) - the position-history point tooltip fixed under #3590 Extract a pure `getMapHoverTooltipMeta(effectiveHops, snr)` helper in `nodeHops.ts` that decides whether to show the direct-heard SNR, and use it in the map hover tooltip so a "N hops · 📶 X.XdB" line is rendered when the node is heard directly and SNR is known. Adds `nodeHops.test.ts` covering the show/hide conditions (0 hops with SNR, SNR of 0, null/undefined SNR, multi-hop, unknown hops). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n --- src/components/NodesTab.tsx | 17 ++++++++++++---- src/utils/nodeHops.test.ts | 39 +++++++++++++++++++++++++++++++++++++ src/utils/nodeHops.ts | 29 +++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/utils/nodeHops.test.ts diff --git a/src/components/NodesTab.tsx b/src/components/NodesTab.tsx index 7791ee7ec..120f4a52c 100644 --- a/src/components/NodesTab.tsx +++ b/src/components/NodesTab.tsx @@ -16,7 +16,7 @@ import MapLegend from './MapLegend'; import { formatTime, formatDateTime } from '../utils/datetime'; import { getDistanceToNode, calculateDistance, formatDistance } from '../utils/distance'; import { getTilesetById } from '../config/tilesets'; -import { getEffectiveHops } from '../utils/nodeHops'; +import { getEffectiveHops, getMapHoverTooltipMeta } from '../utils/nodeHops'; import { buildNodeExportRows, nodesToCsv, nodesToHtml, downloadTextFile } from '../utils/nodeExport'; import { useMapContext } from '../contexts/MapContext'; import { useTelemetryNodes, useDeviceConfig, useNodes } from '../hooks/useServerData'; @@ -2284,11 +2284,20 @@ const NodesTabComponent: React.FC = ({ {(() => { const tooltipHops = getEffectiveHops(node, nodeHopsCalculation, traceroutes, currentNodeNum); - return tooltipHops < 999 ? ( + const { hops, showSnr, snr } = getMapHoverTooltipMeta(tooltipHops, node.snr); + if (hops === null && !showSnr) return null; + return (
- {tooltipHops} hop{tooltipHops !== 1 ? 's' : ''} + {hops !== null && ( + {hops} hop{hops !== 1 ? 's' : ''} + )} + {showSnr && ( + + {hops !== null ? ' · ' : ''}📶 {snr!.toFixed(1)}dB + + )}
- ) : null; + ); })()} diff --git a/src/utils/nodeHops.test.ts b/src/utils/nodeHops.test.ts new file mode 100644 index 000000000..b9c612cf6 --- /dev/null +++ b/src/utils/nodeHops.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { getMapHoverTooltipMeta } from './nodeHops'; + +describe('getMapHoverTooltipMeta (issue #3925)', () => { + it('shows SNR when heard directly (0 hops) and SNR is known', () => { + const meta = getMapHoverTooltipMeta(0, -8.5); + expect(meta).toEqual({ hops: 0, showSnr: true, snr: -8.5 }); + }); + + it('shows SNR for an SNR value of 0 (falsy but valid)', () => { + const meta = getMapHoverTooltipMeta(0, 0); + expect(meta).toEqual({ hops: 0, showSnr: true, snr: 0 }); + }); + + it('does not show SNR when heard directly but SNR is null', () => { + const meta = getMapHoverTooltipMeta(0, null); + expect(meta).toEqual({ hops: 0, showSnr: false, snr: null }); + }); + + it('does not show SNR when heard directly but SNR is undefined', () => { + const meta = getMapHoverTooltipMeta(0, undefined); + expect(meta).toEqual({ hops: 0, showSnr: false, snr: null }); + }); + + it('does not show SNR for multi-hop nodes even when SNR is present', () => { + const meta = getMapHoverTooltipMeta(2, -12); + expect(meta).toEqual({ hops: 2, showSnr: false, snr: null }); + }); + + it('reports unknown hops (>= 999) as null and hides SNR', () => { + const meta = getMapHoverTooltipMeta(999, -5); + expect(meta).toEqual({ hops: null, showSnr: false, snr: null }); + }); + + it('preserves a known multi-hop count', () => { + const meta = getMapHoverTooltipMeta(3, undefined); + expect(meta).toEqual({ hops: 3, showSnr: false, snr: null }); + }); +}); diff --git a/src/utils/nodeHops.ts b/src/utils/nodeHops.ts index e72e08eef..a8c449c75 100644 --- a/src/utils/nodeHops.ts +++ b/src/utils/nodeHops.ts @@ -75,3 +75,32 @@ export function getEffectiveHops( return node.hopsAway ?? 999; } } + +/** + * Metadata for the map node-circle hover tooltip's hop/SNR line. + * + * SNR is only meaningful (and only shown) when the node was heard directly + * (0 effective hops) and an SNR value is known. This mirrors the Nodes list + * card view and the position-history point tooltip (issue #3590 / #3925). + */ +export interface MapHoverTooltipMeta { + /** Effective hop count, or null when unknown (>= 999). */ + hops: number | null; + /** Whether to render the direct-heard SNR value. */ + showSnr: boolean; + /** The SNR value to render (dB), or null when it should not be shown. */ + snr: number | null; +} + +/** + * Compute what the map node-circle hover tooltip should display for the + * hop/SNR line, given the effective hop count and the node's SNR. + */ +export function getMapHoverTooltipMeta( + effectiveHops: number, + snr: number | null | undefined +): MapHoverTooltipMeta { + const hops = effectiveHops < 999 ? effectiveHops : null; + const showSnr = effectiveHops === 0 && snr != null; + return { hops, showSnr, snr: showSnr ? snr! : null }; +}