From 920c46b18b2b9014066ddea807dc11d4d4c55455 Mon Sep 17 00:00:00 2001 From: Optio Agent Date: Fri, 24 Jul 2026 15:09:02 +0000 Subject: [PATCH] fix(logging): fix RST signal-strength bars for dB-report digital modes The new-contact form's `rstToBars` visualization hard-coded a digital-mode list (FT8/FT4/PSK31/RTTY/MFSK/OLIVIA/CONTESTIA) that had drifted behind the canonical `DB_REPORT_MODES` in src/lib/modes.ts. Picking JS8, FST4, JT65, JT9, Q65, MSK144 or PSK63 pre-fills a "-10" dB report (via defaultRstForMode), but the meter didn't recognize those modes as dB-report, so it parsed "-10" as a voice RST and rendered a misleading 1-bar signal instead of the correct dB-scaled bars. Expose the classification as a canonical `isDbReportMode(mode)` from src/lib/modes.ts (now the single source `defaultRstForMode` also uses) and route the meter through it, so the report the form fills and the meter that draws it can no longer disagree. Guarded by unit tests in modes.spec.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/new-contact/page.tsx | 10 ++++++--- src/lib/modes.ts | 11 ++++++++- tests/modes.spec.ts | 43 +++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/app/new-contact/page.tsx b/src/app/new-contact/page.tsx index f3dcb75..9ce64d8 100644 --- a/src/app/new-contact/page.tsx +++ b/src/app/new-contact/page.tsx @@ -37,7 +37,7 @@ import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import { frequencyToBand, AMATEUR_BANDS } from '@/lib/bands'; -import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes'; +import { AMATEUR_MODES, defaultRstForMode, isDbReportMode } from '@/lib/modes'; import { gridToLatLon, gridLocatorError, @@ -134,8 +134,12 @@ function rstToBars(rst: string | undefined, mode: string): number { // Digital values like -10 / +12 — map to 5 bars across -25..+10 dB const numeric = Number.parseInt(rst.replace(/[^-\d]/g, ''), 10); if (Number.isNaN(numeric)) return 0; - const isDigital = ['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA'].includes(mode); - if (isDigital) { + // dB-report modes (the WSJT-X / weak-signal family, plus the keyboard PSK/RTTY + // group) carry a "-10"-style SNR, not an RST — scale them on the dB axis. + // Sourced from the canonical classifier so this stays in step with the report + // the form pre-fills; a hard-coded list here previously omitted JS8/FST4/JT65/ + // JT9/Q65/MSK144/PSK63, whose "-10" default then rendered a wrong 1-bar meter. + if (isDbReportMode(mode)) { const clamped = Math.min(10, Math.max(-25, numeric)); return Math.round(((clamped + 25) / 35) * 5); } diff --git a/src/lib/modes.ts b/src/lib/modes.ts index afcd9d5..d7d3845 100644 --- a/src/lib/modes.ts +++ b/src/lib/modes.ts @@ -53,12 +53,21 @@ const DB_REPORT_MODES = new Set([ 'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA', ]); +// Whether `mode`'s signal report is conventionally a dB SNR ("-10") rather than +// an RST. Exposed as the single source of truth so UI that interprets a report +// value — the pre-filled default, a signal-strength meter — classifies a mode +// the same way everywhere instead of each call site carrying its own drifting +// copy of the digital-mode list. +export function isDbReportMode(mode: string): boolean { + return DB_REPORT_MODES.has(mode.trim().toUpperCase()); +} + // The signal report a logging form should pre-fill when an operator picks `mode`. // CW takes RST with a tone digit (599); dB-report digital modes take -10; every // other mode (phone, digital voice, image, packet) takes a 59 RS(T). export function defaultRstForMode(mode: string): string { const m = mode.trim().toUpperCase(); if (m === 'CW') return '599'; - if (DB_REPORT_MODES.has(m)) return '-10'; + if (isDbReportMode(m)) return '-10'; return '59'; } diff --git a/tests/modes.spec.ts b/tests/modes.spec.ts index 75692c4..f15ea28 100644 --- a/tests/modes.spec.ts +++ b/tests/modes.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes'; +import { AMATEUR_MODES, defaultRstForMode, isDbReportMode } from '@/lib/modes'; // The mode list is the canonical set of operating modes Nextlog can store, and // the single source of truth the contact-search mode dropdown draws from. These @@ -93,6 +93,15 @@ test.describe('defaultRstForMode', () => { } }); + test('agrees with isDbReportMode for every canonical mode', () => { + // defaultRstForMode and isDbReportMode share one classification: a mode + // defaults to a -10 dB report exactly when it is a dB-report mode. + for (const mode of AMATEUR_MODES) { + if (mode === 'CW') continue; // CW is RST (599), not a dB report + expect(defaultRstForMode(mode) === '-10').toBe(isDbReportMode(mode)); + } + }); + test('is case-insensitive', () => { expect(defaultRstForMode('cw')).toBe('599'); expect(defaultRstForMode('ft8')).toBe('-10'); @@ -105,3 +114,35 @@ test.describe('defaultRstForMode', () => { } }); }); + +test.describe('isDbReportMode', () => { + test('classifies the WSJT-X / weak-signal digital family as dB-report modes', () => { + // These pre-fill a "-10" report in the logging forms, so any consumer that + // renders a signal-strength meter must scale them on the dB axis, not read + // the leading digit as an RST readability. The signal-strength bars on the + // new-contact form previously hard-coded a shorter list that omitted JS8, + // FST4, JT65, JT9, Q65, MSK144 and PSK63, so those modes rendered a + // misleading 1-bar meter. + for (const mode of [ + 'FT8', 'FT4', 'JS8', 'FST4', 'JT65', 'JT9', 'Q65', 'MSK144', + 'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA', + ]) { + expect(isDbReportMode(mode)).toBe(true); + } + }); + + test('classifies phone, CW, digital-voice and image modes as non-dB modes', () => { + for (const mode of [ + 'SSB', 'CW', 'AM', 'FM', 'DMR', 'DSTAR', 'C4FM', 'YSF', 'M17', 'FREEDV', + 'SSTV', 'PACKET', 'ATV', + ]) { + expect(isDbReportMode(mode)).toBe(false); + } + }); + + test('is case-insensitive and tolerates surrounding whitespace', () => { + expect(isDbReportMode('ft8')).toBe(true); + expect(isDbReportMode(' JS8 ')).toBe(true); + expect(isDbReportMode('ssb')).toBe(false); + }); +});