Skip to content

Commit 7553af3

Browse files
patrickrbclaude
andcommitted
fix(sync): emit MHz frequency to LoTW + QRZ instead of dividing by 1M
contacts.frequency is numeric(10,6) and stores MHz directly — the column precision can't physically hold Hz for HF (14.205 MHz in Hz is 14205000, which needs 8 integer digits; the column allows 4). The form, ADIF import, and QRZ download all write MHz. Both upload paths assumed Hz and divided by 1_000_000: LoTW (src/lib/lotw.ts): freqToMhz(14.205) = 0.000014 MHz QRZ (src/lib/qrz.ts): (14.205 / 1000000).toString() = "0.0000142..." For LoTW that produced a .tq8 whose canonical sign-string used the wrong frequency. The signature still verified (same wrong value on both sides), so LoTW accepted the upload and the app marked contacts lotw_qsl_sent='Y'. LoTW then silently dropped each QSO because 0.000014 MHz isn't a valid amateur frequency — uploads "succeeded" but nothing appeared on the LoTW site. The author of #185 chased this confusion the wrong way in the upload-route comment, inferring Hz from the QRZ uploader's matching bug. Fix: - Rename freqToMhz → formatFreqMhz, drop the /1_000_000 (input is MHz). - Same for contactToQRZFormat in qrz.ts. - Replace the misleading "stored in Hz" comment in the upload route. After this lands, contacts uploaded with the bad frequency are still marked lotw_qsl_sent='Y'/'qrz_sync_status'='synced' in the DB and won't re-upload. PR description has a one-liner to reset them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 08b31f4 commit 7553af3

3 files changed

Lines changed: 20 additions & 13 deletions

File tree

src/app/api/lotw/upload/route.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,8 @@ export async function POST(request: NextRequest) {
247247
band: c.band || '',
248248
band_rx: c.band_rx,
249249
mode: c.mode || '',
250-
// contacts.frequency is stored in MHz already (DECIMAL(10,6)), but the
251-
// LotwQso interface expects MHz frequencies multiplied... re-check:
252-
// Looking at QRZ contactToQRZFormat (qrz.ts:225): freq = frequency / 1_000_000
253-
// → frequency is stored in Hz. So convert: c.frequency is in Hz here.
250+
// contacts.frequency is numeric(10,6) and stores MHz directly. pg returns
251+
// numeric as a string by default, so coerce to Number for the LotwQso type.
254252
freq: c.frequency ? Number(c.frequency) : undefined,
255253
freq_rx: c.freq_rx ? Number(c.freq_rx) : undefined,
256254
prop_mode: c.prop_mode,

src/lib/lotw.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,16 @@ function parseP12(buf: Buffer, password: string): ParsedP12 {
345345

346346
// Format a frequency in MHz the way TQSL does — trim trailing zeros,
347347
// no exponent, no thousands separator. Avoids JS scientific notation.
348-
function freqToMhz(freqHz: number): string {
349-
// Hz → MHz; use toFixed(6) then trim trailing zeros and dangling dot.
350-
const mhz = freqHz / 1_000_000;
351-
return mhz.toFixed(6).replace(/\.?0+$/, '');
348+
//
349+
// IMPORTANT: input is MHz, not Hz. contacts.frequency is numeric(10,6),
350+
// whose precision physically can't hold Hz for HF (14.205 MHz in Hz is
351+
// 14205000 — 8 integer digits, the column allows 4). The form, ADIF
352+
// import, and QRZ download all write MHz. An earlier version of this
353+
// function divided by 1_000_000 thinking the value was Hz; that turned
354+
// 14.205 MHz into 0.000014 MHz in the .tq8 and LoTW silently dropped
355+
// every QSO whose signature otherwise verified.
356+
function formatFreqMhz(freqMhz: number): string {
357+
return freqMhz.toFixed(6).replace(/\.?0+$/, '');
352358
}
353359

354360
// Format a Date as YYYY-MM-DD in UTC.
@@ -436,9 +442,9 @@ function buildCanonicalSignString(station: LotwStationProfile, qso: LotwQso): st
436442
// CALL
437443
if (qso.call) s += qso.call;
438444
// FREQ (MHz)
439-
if (qso.freq != null && qso.freq > 0) s += freqToMhz(qso.freq);
445+
if (qso.freq != null && qso.freq > 0) s += formatFreqMhz(qso.freq);
440446
// FREQ_RX (MHz)
441-
if (qso.freq_rx != null && qso.freq_rx > 0) s += freqToMhz(qso.freq_rx);
447+
if (qso.freq_rx != null && qso.freq_rx > 0) s += formatFreqMhz(qso.freq_rx);
442448
// MODE
443449
if (qso.mode) s += qso.mode;
444450
// PROP_MODE
@@ -551,10 +557,10 @@ function renderContactRecord(
551557
out += lenPrefix('BAND', qso.band.toUpperCase()) + '\n';
552558
out += lenPrefix('MODE', qso.mode.toUpperCase()) + '\n';
553559
if (qso.freq != null && qso.freq > 0) {
554-
out += lenPrefix('FREQ', freqToMhz(qso.freq)) + '\n';
560+
out += lenPrefix('FREQ', formatFreqMhz(qso.freq)) + '\n';
555561
}
556562
if (qso.freq_rx != null && qso.freq_rx > 0) {
557-
out += lenPrefix('FREQ_RX', freqToMhz(qso.freq_rx)) + '\n';
563+
out += lenPrefix('FREQ_RX', formatFreqMhz(qso.freq_rx)) + '\n';
558564
}
559565
if (qso.prop_mode) out += lenPrefix('PROP_MODE', qso.prop_mode.toUpperCase()) + '\n';
560566
if (qso.sat_name) out += lenPrefix('SAT_NAME', qso.sat_name.toUpperCase()) + '\n';

src/lib/qrz.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ export function contactToQRZFormat(contact: {
224224
time_on,
225225
band: contact.band,
226226
mode: contact.mode,
227-
freq: contact.frequency ? (contact.frequency / 1000000).toString() : undefined, // Convert Hz to MHz
227+
// contacts.frequency is numeric(10,6) and stores MHz directly (not Hz). An
228+
// earlier version divided by 1_000_000 thinking the value was Hz; that emitted
229+
// a frequency like 0.000014 MHz to QRZ for a 14.205 MHz QSO.
230+
freq: contact.frequency ? Number(contact.frequency).toString() : undefined,
228231
rst_sent: contact.rst_sent,
229232
rst_rcvd: contact.rst_received,
230233
gridsquare: contact.grid_locator,

0 commit comments

Comments
 (0)