Skip to content

fix(export): route search-results ADIF export through the shared generator - #230

Merged
patrickrb merged 1 commit into
mainfrom
optio/task-5e06a36f-85d6-401d-b11b-f4cc4f4a4d94
Jul 23, 2026
Merged

fix(export): route search-results ADIF export through the shared generator#230
patrickrb merged 1 commit into
mainfrom
optio/task-5e06a36f-85d6-401d-b11b-f4cc4f4a4d94

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

The search page's Export button (/api/contacts/search?export=true) hand-rolled its own ADIF serializer rather than reusing the shared generateAdif() in src/lib/adif.ts. That duplicated copy had drifted and carried three defects the main per-station export had already fixed:

  1. UTF-8 byte-length bug. Field lengths were declared with JS String.length (UTF-16 code units) instead of the UTF-8 byte count ADIF requires — the exact interop bug fixed for the main export in fix(adif): declare export field lengths as UTF-8 byte count #228. A name like José, an ø-callsign, or a CJK QTH exported with the wrong declared length and got truncated/mis-aligned by strict readers (LoTW/TQSL, Cloudlog, N1MM).
  2. 500 on null fields. It called .toString() / .length on frequency, mode and band unconditionally. A single contact imported without one of those (all nullable columns) crashed the entire export with a 500.
  3. Dropped data. It emitted only ~11 fields, silently discarding DXCC, QSL status (qsl_rcvd/lotw_qsl_rcvd/…), country, zones, and station info that the main export includes — so a search export was far less useful for awards/QSL tooling.

Solution

Route the export branch through the existing, well-tested generateAdif(). SELECT * from contacts already returns snake_case columns matching AdifExportContact, so the rows feed straight in. Station ("my_*") fields are absent when a search spans multiple stations; adifField() skips nullish values, so they're simply omitted rather than mis-declared. Content-Type is aligned to application/octet-stream to match the main export. This removes ~45 lines of duplicated serialization and keeps both export paths in lockstep going forward.

The frontend consumer (src/app/search/page.tsx) just downloads the returned blob, so the change is transparent to it.

Testing

  • Added three generateAdif regression tests in tests/adif-generate.spec.ts:
    • tolerates null frequency/mode/band without throwing (the old crash),
    • includes DXCC / QSL / country fields for the search export (the dropped data),
    • (existing tests already cover the UTF-8 byte-length correctness).
  • npm run typecheck — clean
  • npm run lint — clean
  • npm run build — succeeds
  • npx playwright test tests/adif-generate.spec.ts — 10/10 pass

Follow-up

  • The search route's query params (gridLocator, startDate, endDate, qslStatus) are still camelCase, a known drift noted in CLAUDE.md; left untouched here to keep this change focused.

🤖 Generated with Claude Code

…rator

The /api/contacts/search?export=true path hand-rolled its own ADIF
serializer instead of reusing generateAdif(). That copy carried three
defects the main export had already shed:

- Declared field lengths with JS String.length (UTF-16 code units)
  rather than the UTF-8 byte count ADIF requires — the interop bug fixed
  for the main export in #228. Accented names, ø-callsigns and CJK QTHs
  came out with wrong lengths and got truncated/mis-aligned by strict
  readers (LoTW/TQSL, Cloudlog, N1MM).
- Called .toString()/.length on frequency/mode/band unconditionally, so
  a single contact imported without one of those fields 500'd the whole
  export.
- Emitted only ~11 fields, silently dropping DXCC, QSL status, country,
  zones and station info.

Delegating to generateAdif() fixes all three and keeps the two export
paths in lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodelog Ready Ready Preview, Comment Jul 23, 2026 8:07pm

Request Review

@patrickrb

Copy link
Copy Markdown
Owner Author

Review (unable to submit formal approval — GitHub rejects self-review since this PR and I share the same authenticated account):

Verified this correctly routes the search export through the shared generateAdif():

  • The three defects described (UTF-8 byte-length, 500 on null frequency/mode/band, dropped DXCC/QSL/country fields) are real — confirmed against the old hand-rolled serializer removed here and against adifField()'s UTF-8 byte-length + nullish-skip behavior in src/lib/adif.ts.
  • SELECT * FROM contacts columns line up with AdifExportContact (checked against drizzle/schema.ts); station ("my_*") fields are simply absent since there's no station join here, and adifField() correctly omits them rather than crashing or mis-emitting.
  • src/app/search/page.tsx's export handler only reads the blob + Content-Disposition filename, so the Content-Type: application/octet-stream change (matching the sibling /api/adif/export route) is transparent to the frontend.
  • New tests in tests/adif-generate.spec.ts directly cover the two previously-crashing/dropped-data cases and follow the existing test file's conventions.
  • No any, comments explain non-obvious WHY per repo convention, change is small and focused with a documented follow-up (camelCase query params) left out of scope.

Verdict: Approve. No correctness, security, or convention issues found.

@patrickrb
patrickrb merged commit d4c4e47 into main Jul 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant