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 }; +}