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