diff --git a/src/app/api/lotw/upload/route.ts b/src/app/api/lotw/upload/route.ts index fe67a36..97b54c4 100644 --- a/src/app/api/lotw/upload/route.ts +++ b/src/app/api/lotw/upload/route.ts @@ -247,10 +247,8 @@ export async function POST(request: NextRequest) { band: c.band || '', band_rx: c.band_rx, mode: c.mode || '', - // contacts.frequency is stored in MHz already (DECIMAL(10,6)), but the - // LotwQso interface expects MHz frequencies multiplied... re-check: - // Looking at QRZ contactToQRZFormat (qrz.ts:225): freq = frequency / 1_000_000 - // → frequency is stored in Hz. So convert: c.frequency is in Hz here. + // contacts.frequency is numeric(10,6) and stores MHz directly. pg returns + // numeric as a string by default, so coerce to Number for the LotwQso type. freq: c.frequency ? Number(c.frequency) : undefined, freq_rx: c.freq_rx ? Number(c.freq_rx) : undefined, prop_mode: c.prop_mode, diff --git a/src/lib/lotw.ts b/src/lib/lotw.ts index 825aeaf..a6b08bf 100644 --- a/src/lib/lotw.ts +++ b/src/lib/lotw.ts @@ -345,10 +345,16 @@ function parseP12(buf: Buffer, password: string): ParsedP12 { // Format a frequency in MHz the way TQSL does — trim trailing zeros, // no exponent, no thousands separator. Avoids JS scientific notation. -function freqToMhz(freqHz: number): string { - // Hz → MHz; use toFixed(6) then trim trailing zeros and dangling dot. - const mhz = freqHz / 1_000_000; - return mhz.toFixed(6).replace(/\.?0+$/, ''); +// +// IMPORTANT: input is MHz, not Hz. contacts.frequency is numeric(10,6), +// whose precision physically can't hold Hz for HF (14.205 MHz in Hz is +// 14205000 — 8 integer digits, the column allows 4). The form, ADIF +// import, and QRZ download all write MHz. An earlier version of this +// function divided by 1_000_000 thinking the value was Hz; that turned +// 14.205 MHz into 0.000014 MHz in the .tq8 and LoTW silently dropped +// every QSO whose signature otherwise verified. +function formatFreqMhz(freqMhz: number): string { + return freqMhz.toFixed(6).replace(/\.?0+$/, ''); } // Format a Date as YYYY-MM-DD in UTC. @@ -436,9 +442,9 @@ function buildCanonicalSignString(station: LotwStationProfile, qso: LotwQso): st // CALL if (qso.call) s += qso.call; // FREQ (MHz) - if (qso.freq != null && qso.freq > 0) s += freqToMhz(qso.freq); + if (qso.freq != null && qso.freq > 0) s += formatFreqMhz(qso.freq); // FREQ_RX (MHz) - if (qso.freq_rx != null && qso.freq_rx > 0) s += freqToMhz(qso.freq_rx); + if (qso.freq_rx != null && qso.freq_rx > 0) s += formatFreqMhz(qso.freq_rx); // MODE if (qso.mode) s += qso.mode; // PROP_MODE @@ -551,10 +557,10 @@ function renderContactRecord( out += lenPrefix('BAND', qso.band.toUpperCase()) + '\n'; out += lenPrefix('MODE', qso.mode.toUpperCase()) + '\n'; if (qso.freq != null && qso.freq > 0) { - out += lenPrefix('FREQ', freqToMhz(qso.freq)) + '\n'; + out += lenPrefix('FREQ', formatFreqMhz(qso.freq)) + '\n'; } if (qso.freq_rx != null && qso.freq_rx > 0) { - out += lenPrefix('FREQ_RX', freqToMhz(qso.freq_rx)) + '\n'; + out += lenPrefix('FREQ_RX', formatFreqMhz(qso.freq_rx)) + '\n'; } if (qso.prop_mode) out += lenPrefix('PROP_MODE', qso.prop_mode.toUpperCase()) + '\n'; if (qso.sat_name) out += lenPrefix('SAT_NAME', qso.sat_name.toUpperCase()) + '\n'; diff --git a/src/lib/qrz.ts b/src/lib/qrz.ts index 7b8e498..7e6d322 100644 --- a/src/lib/qrz.ts +++ b/src/lib/qrz.ts @@ -224,7 +224,10 @@ export function contactToQRZFormat(contact: { time_on, band: contact.band, mode: contact.mode, - freq: contact.frequency ? (contact.frequency / 1000000).toString() : undefined, // Convert Hz to MHz + // contacts.frequency is numeric(10,6) and stores MHz directly (not Hz). An + // earlier version divided by 1_000_000 thinking the value was Hz; that emitted + // a frequency like 0.000014 MHz to QRZ for a 14.205 MHz QSO. + freq: contact.frequency ? Number(contact.frequency).toString() : undefined, rst_sent: contact.rst_sent, rst_rcvd: contact.rst_received, gridsquare: contact.grid_locator,