diff --git a/src/app/new-contact/page.tsx b/src/app/new-contact/page.tsx index 18a5014..26318c5 100644 --- a/src/app/new-contact/page.tsx +++ b/src/app/new-contact/page.tsx @@ -37,6 +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 { gridToLatLon, distanceKm, bearingDeg, compassPoint, kmToMiles } from '@/lib/grid'; void _PageHeader; @@ -61,7 +62,7 @@ interface PreviousContact { notes?: string; } -const MODES = ['SSB', 'CW', 'FT8', 'FT4', 'RTTY', 'PSK31', 'AM', 'FM'] as const; +const MODES = AMATEUR_MODES; const BAND_PILLS = AMATEUR_BANDS; // QRZ XML license-class codes — used to give the chip a human-readable label. @@ -381,20 +382,8 @@ export default function NewContactPage() { }; const handleSelectMode = (value: string) => { - setFormData((prev) => { - const next = { ...prev, mode: value }; - if (value === 'CW') { - next.rst_sent = '599'; - next.rst_received = '599'; - } else if (['SSB', 'FM', 'AM'].includes(value)) { - next.rst_sent = '59'; - next.rst_received = '59'; - } else if (['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA'].includes(value)) { - next.rst_sent = '-10'; - next.rst_received = '-10'; - } - return next; - }); + const rst = defaultRstForMode(value); + setFormData((prev) => ({ ...prev, mode: value, rst_sent: rst, rst_received: rst })); }; const handleSelectBand = (value: string) => { diff --git a/src/components/QuickLogCard.tsx b/src/components/QuickLogCard.tsx index d0898d3..b54f058 100644 --- a/src/components/QuickLogCard.tsx +++ b/src/components/QuickLogCard.tsx @@ -20,6 +20,7 @@ import { } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import { frequencyToBand, AMATEUR_BANDS } from '@/lib/bands'; +import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes'; interface Station { id: number; @@ -39,15 +40,9 @@ interface LookupResult { error?: string; } -const MODES = ['SSB', 'CW', 'FT8', 'FT4', 'RTTY', 'PSK31', 'AM', 'FM'] as const; +const MODES = AMATEUR_MODES; const BANDS = AMATEUR_BANDS; -function defaultRstForMode(mode: string): string { - if (mode === 'CW') return '599'; - if (['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA'].includes(mode)) return '-10'; - return '59'; -} - function formatUtcClock(d: Date): string { const hh = String(d.getUTCHours()).padStart(2, '0'); const mm = String(d.getUTCMinutes()).padStart(2, '0'); diff --git a/src/lib/modes.ts b/src/lib/modes.ts index 849b828..afcd9d5 100644 --- a/src/lib/modes.ts +++ b/src/lib/modes.ts @@ -38,3 +38,27 @@ export const AMATEUR_MODES = [ ] as const; export type AmateurMode = (typeof AMATEUR_MODES)[number]; + +// Modes whose signal report is conventionally a dB SNR (e.g. "-10") rather than +// an RST. Both logging forms carried their own copy of this list; keeping it +// here as the single source of truth stops the two from drifting apart (and from +// falling behind AMATEUR_MODES) as new digital modes become loggable. +// +// The WSJT-X / weak-signal family (FT8, FT4, JS8, FST4, JT65, JT9, Q65, MSK144) +// genuinely reports dB. The keyboard PSK/RTTY/MFSK/Olivia/Contestia group is +// grouped here to preserve the default Nextlog has always applied to those menu +// entries — not because RTTY is a dB mode. +const DB_REPORT_MODES = new Set([ + 'FT8', 'FT4', 'JS8', 'FST4', 'JT65', 'JT9', 'Q65', 'MSK144', + 'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA', +]); + +// 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'; + return '59'; +} diff --git a/tests/modes.spec.ts b/tests/modes.spec.ts index 5c34248..75692c4 100644 --- a/tests/modes.spec.ts +++ b/tests/modes.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { AMATEUR_MODES } from '@/lib/modes'; +import { AMATEUR_MODES, defaultRstForMode } 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 @@ -54,3 +54,54 @@ test.describe('modes module', () => { expect(AMATEUR_MODES.slice(0, 4)).toEqual(['SSB', 'CW', 'FT8', 'FT4']); }); }); + +test.describe('defaultRstForMode', () => { + test('CW gets a 599 signal report', () => { + expect(defaultRstForMode('CW')).toBe('599'); + }); + + test('phone modes get a 59 signal report', () => { + for (const mode of ['SSB', 'AM', 'FM']) { + expect(defaultRstForMode(mode)).toBe('59'); + } + }); + + test('digital dB-report modes default to -10', () => { + // WSJT-X / weak-signal families report a dB SNR, and Nextlog has always + // defaulted its PSK/RTTY/MFSK/Olivia/Contestia menu entries the same way. + const dbModes = [ + 'FT8', 'FT4', 'JS8', 'FST4', 'JT65', 'JT9', 'Q65', 'MSK144', + 'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA', + ]; + for (const mode of dbModes) { + expect(defaultRstForMode(mode)).toBe('-10'); + } + }); + + test('preserves the exact classification the logging forms used before centralizing', () => { + // The two logging forms each carried their own copy of this heuristic; the + // shared helper must not change the default for any mode they both handled. + expect(defaultRstForMode('CW')).toBe('599'); + for (const mode of ['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA']) { + expect(defaultRstForMode(mode)).toBe('-10'); + } + }); + + test('digital-voice and image modes fall back to 59', () => { + for (const mode of ['DMR', 'DSTAR', 'C4FM', 'YSF', 'M17', 'FREEDV', 'SSTV', 'PACKET', 'ATV']) { + expect(defaultRstForMode(mode)).toBe('59'); + } + }); + + test('is case-insensitive', () => { + expect(defaultRstForMode('cw')).toBe('599'); + expect(defaultRstForMode('ft8')).toBe('-10'); + expect(defaultRstForMode('ssb')).toBe('59'); + }); + + test('returns a default for every canonical mode', () => { + for (const mode of AMATEUR_MODES) { + expect(['59', '599', '-10']).toContain(defaultRstForMode(mode)); + } + }); +});