You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(sync): correctness overhaul of QRZ + LoTW sync flows (#185)
* fix(sync): correctness overhaul of QRZ + LoTW sync flows
Both sync paths had bugs causing silent data loss in production:
QRZ download
- OPTION param: send a single value with `;` separators, not two
separate fields (FormData duplicates lose `TYPE:ADIF`)
- MODSINCE (not MODIFIEDSINCE) — the date filter was being silently
ignored, every "incremental" download pulled the full logbook
- add STATUS:CONFIRMED to limit payload, extract APP_QRZLOG_STATUS
- new matchQRZConfirmation: call+band+mode+station_callsign exact
+ ±15min tolerance (was call+date+time only with ±5min)
LoTW download
- URL params: qso_qslsince / qso_qslbefore (no underscores) — date
filter was a no-op
- add qso_qsldetail/qso_mydetail so LoTW returns enriched location
fields (state, county, CQZ, ITUZ, DXCC, grid)
- match: require call+band+mode+station_callsign exact, ±15min,
proper UTC parsing, satellite-mode rule (sat_name must agree)
- on confirmation: enrich state/cnty/cqz/ituz/dxcc/country/grid;
persist stations.lotw_last_qsl_rcvd_date for incremental fetches
LoTW upload (.tq8 signing)
- replace the OpenSSL fallback stub (which logged a warning then
returned UNSIGNED ADIF — every Vercel cron upload was rejected by
LoTW) with a pure-Node node-forge implementation that produces
wavelog-compatible .tq8 files (gzip + per-QSO RSA-SHA1 signature
in <SIGN_LOTW_V2.0>, with the canonical sign-string echoed in
<SIGNDATA>); runtime self-verify before each upload
- multipart `upfile` POST + check for `<!-- .UPL. accepted -->`
- filter unsupported prop_modes (INTERNET, RPT) — flagged 'I'
- normalize callsigns (W1AW_P → W1AW/P)
Cross-service sync
- when LoTW confirms a QRZ-uploaded QSO, set qrz_qsl_sent='M' so
the next QRZ sync re-uploads with OPTION=REPLACE; mirrored for
the QRZ→LoTW direction
Other
- validateLoTWCredentials now actually parses the response body
(the old response.url check always returned true)
- new checkLotwCertCrl helper queries lotw.arrl.org/lotw/crl?serial=
- vercel.json: maxDuration 300s on cron + LoTW routes
Migration: migrations/sync_qrz_lotw_fixes.sql adds p12_password,
cert_serial, crl_status to lotw_credentials; lotw_last_qsl_rcvd_date
to stations; prop_mode/sat_name/band_rx/freq_rx/iota to contacts;
CHECK constraints on the QSL-sent enums.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(migration): add QRZ column setup + migration runner script
Discovered while applying to local + prod that:
1. Some envs initialized contacts without qrz_qsl_sent / qrz_qsl_rcvd
(those were added by the in-app /install route, not the base schema).
Add them with IF NOT EXISTS to make the migration self-sufficient.
2. The PG 18+ syntax `CREATE TRIGGER IF NOT EXISTS` in the existing
postgres-lotw-migration.sql breaks on PG 15/17. Added a one-line
transform in scripts/run-migration.mjs to rewrite it as DROP+CREATE
so the legacy file works without editing it.
scripts/run-migration.mjs is a small Node runner that takes
DATABASE_URL via env and a SQL file via argv; it wraps everything in
a transaction and rolls back on failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(lotw): capture P12 password during certificate upload
The signer needs the P12 password to decrypt the private key. Until
now the certificate-upload route stored only the .p12 file blob, so
buildSignedTq8 had to assume an empty password — fine for unprotected
TQSL exports, broken for password-protected ones.
API: /api/lotw/certificate now accepts an optional p12_password form
field. Before insert, it parses the P12 with node-forge using that
password (via readCertMetadata). Bad password / corrupt file fails
fast with a clear error rather than landing an unusable cert in the DB.
We also persist cert_serial and cert_expires_at extracted from the
parse, populating columns the migration just added.
UI: both upload forms got a password input with show/hide toggle.
Helper text explains the password is required to sign uploads, stored
encrypted, and never returned to the browser. Empty is accepted for
TQSL exports without a password.
Side fix: the LoTW dashboard form was already broken — it didn't send
cert_name (required by the API). Added a Certificate Name field there
too, and restructured the 3-col grid into a 2x2 layout so all four
fields fit cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments