From 3b0865cbda6d3d4c3f249e58b1605e721503050f Mon Sep 17 00:00:00 2001 From: Optio Agent Date: Fri, 24 Jul 2026 09:07:50 +0000 Subject: [PATCH] feat(logging): offer every stored mode in both logging forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quick Log card and the full new-contact form both hard-coded an 8-mode menu (SSB/CW/FT8/FT4/RTTY/PSK31/AM/FM). Search already offers the full canonical mode list (#242) and ADIF import promotes submodes like JS8/FST4/JT65/Q65/PSK63/Olivia — so an operator could import or filter those modes but could not *log* one without editing the QSO afterward. Both forms now draw their menu from the canonical AMATEUR_MODES, exactly as they already do for AMATEUR_BANDS. The per-mode RST default heuristic (duplicated and drifting between the two forms) is centralized as modes.defaultRstForMode, preserving the classification each form used. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/new-contact/page.tsx | 19 +++--------- src/components/QuickLogCard.tsx | 9 ++---- src/lib/modes.ts | 24 +++++++++++++++ tests/modes.spec.ts | 53 ++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 23 deletions(-) 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)); + } + }); +});