[feat](trx-logbook): work a contest, and record what came back #60

Merged
sjg merged 6 commits from feat/logbook-contest-and-qsl into main 2026-08-07 21:50:02 +02:00
Showing only changes of commit 76e33a91bb - Show all commits
+147
View File
@@ -322,3 +322,150 @@ trx-configurator
| `trx-app` | Config types and validation | Yes |
| `serialport` | Serial port enumeration | Yes (transitive) |
| `soapysdr` | SDR device enumeration (optional) | Yes (feature-gated) |
---
## Logbook and Ham Radio Layout
Two halves of one feature ([#54](https://git.haxx.space/sjg/trx-rs/issues/54)): a station
logbook that speaks ADIF, and an operator layout that puts a transceiver's controls and that
logbook on one screen.
Nothing in the application records a QSO today. The map's "QSO summary" cards describe
contacts *between other stations*, reconstructed from decoded traffic; bookmarks are
frequencies, not contacts. Neither is the operator's own log.
### Requirements
| ID | Description |
|----|-------------|
| REQ-LOG-001 | The system shall record QSOs the operator makes, each holding at minimum callsign, date, time, band, frequency, mode and both signal reports. |
| REQ-LOG-002 | When logging a QSO, the system shall pre-fill frequency, band, mode and time from the selected rig, and the station's own callsign and locator from configuration. |
| REQ-LOG-003 | The system shall allow a logged QSO to be edited and deleted. |
| REQ-LOG-004 | The system shall survive a crash without losing a QSO that was recorded before it. |
| REQ-LOG-005 | The system shall list, search and filter the log by callsign, band, mode and date range. |
| REQ-ADIF-001 | The system shall export the log as an ADIF 3.1.x `.adi` file. |
| REQ-ADIF-002 | The system shall import ADIF `.adi` files produced by other logging software, preserving fields it does not itself use. |
| REQ-ADIF-003 | When importing, the system shall identify QSOs already held and shall not duplicate them. |
| REQ-LOG-006 | Where a decoded station is on screen, the system shall offer to start a log entry from it, pre-filled, without logging it unattended. |
| REQ-LOG-007 | The system shall show whether a callsign has been worked before, and on which bands. |
| REQ-LAY-001 | The system shall offer a "Ham radio" operator layout presenting the transceiver controls and the logbook together. |
| REQ-LAY-002 | Where the selected rig cannot transmit, the system shall not offer the ham layout. |
### A decode is not a QSO
The decoders are receive-only: FT8, CW, APRS and the rest report what was *heard*. A heard
callsign is the beginning of a log entry, not a contact, and the logbook must never write one
by itself — REQ-LOG-006 says offer and pre-fill, never auto-log. Digital QSOs made in
WSJT-X or similar arrive the way every other logger takes them: through ADIF import.
### Architecture
#### New crate: `trx-logbook`
```
src/trx-client/
trx-logbook/
src/
lib.rs # LogbookHandle: open, add, edit, delete, query, import, export
qso.rs # Qso: the record, its ADIF field mapping, band/mode helpers
adif.rs # ADI reader and writer (pure Rust, no new dependencies)
store.rs # Append-only JSON Lines file, in-memory index, compaction
dedupe.rs # Worked-before and import-collision rules
```
A library crate under `src/trx-client/`, beside `trx-frontend`, consumed by
`trx-frontend-http`. The log belongs to the station rather than to a rig or a frontend, so it
sits where any frontend can reach it.
#### Storage: append-only, not a rewritten blob
Bookmarks use `PickleDb` with `AutoDump`, which rewrites the whole file on every write. That
is right for a few dozen bookmarks and wrong for a log: a station with 40 000 QSOs would
rewrite several megabytes to log one contact, and lose the lot if the power went during the
dump.
The log is instead a JSON Lines file — the shape `trx-decode-log` already uses — at
`~/.config/trx-rs/logbook.jsonl`, appended one record per write and read into an in-memory
index at startup. An edit or a delete appends a new revision of that record's id; the load
keeps the last one, and a compaction pass rewrites the file when superseded records exceed a
threshold. Appending is O(1) and atomic per line, so a crash costs at most the line being
written.
#### ADIF in-repo
ADI is a tagged text format: `<FIELD:length>value`, records ended by `<EOR>`, a header ended
by `<EOH>`, everything outside a tag ignored. It is small enough to implement exactly, which
this project already prefers for its decoders, and doing so keeps the dependency list where it
is. The reader must be lenient in the ways real files are irregular — lowercase tags, CRLF,
missing header, unknown fields, type indicators — and the writer strict. Unknown fields are
carried through import to export unchanged, so a round trip through trx-rs does not quietly
strip what another logger wrote. ADX (the XML form) is out of scope.
#### Integration points
| Source | What it gives the log | How |
|--------|----------------------|-----|
| `RigState` | `FREQ`, `BAND`, `MODE`/`SUBMODE`, and the rig id a QSO was made on | watch channel already in the frontend context |
| Client config `general.callsign` | `STATION_CALLSIGN`, `OPERATOR` | already surfaced as `owner_callsign` in frontend meta |
| Rig latitude/longitude | `MY_GRIDSQUARE` | `latLonToMaidenhead`, already in the frontend |
| Decoder panels and map | a pre-filled entry: callsign, grid, and the report to offer | existing decode history; no new plumbing |
| `bandForHz` | `BAND` from a frequency | exists in `map-core.ts`; move to a shared module |
#### HTTP API
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/logbook` | Query: filters, paging |
| `POST` | `/logbook` | Add a QSO |
| `PUT` | `/logbook/{id}` | Edit |
| `DELETE` | `/logbook/{id}` | Delete |
| `GET` | `/logbook/export.adi` | Export, honouring the current filter |
| `POST` | `/logbook/import` | Import, answering with counts: added, duplicate, rejected |
| `GET` | `/logbook/worked/{call}` | Worked-before: bands and modes |
Writes require the control role, as the rig endpoints do.
### Frontend
A **Logbook tab** joins the tab order: an entry form that opens pre-filled, a table with the
filters of REQ-LOG-005, and import/export. Worked-before shows against the callsign as it is
typed.
The **ham layout** is a fifth entry in the operator layouts (`compact`, `broadcast`, `digital`,
`full`), which already gate on capability, seed the disclosure sections and persist per rig:
```ts
ham: {
label: "Ham radio",
unavailable: "Ham radio needs a rig that can transmit",
advanced: true, audio: true, scheduler: false,
preferredTab: "logbook", capability: "ham",
}
```
with the `ham` capability set from `RigCapabilities.tx`. It keeps frequency, VFO, mode, filter,
PTT, power and the meters; it hides the broadcast furniture; and it puts the log beside the
spectrum rather than a tab away.
### Phases
| Phase | Lands |
|-------|-------|
| 1 | `trx-logbook`: `Qso`, the ADI reader and writer, round-trip tests against files from other loggers |
| 2 | Store, dedupe, and the HTTP API behind the control role |
| 3 | Logbook tab: entry, table, filters, import, export |
| 4 | Ham layout, pre-filled entry from a decode row or the map, worked-before |
| 5 | Optional: QSL and LoTW/eQSL fields, contest exchange fields, per-band worked/confirmed statistics |
### Open questions
- One station log, or one per rig? The proposal assumes one, with the rig id recorded on each
QSO, since a callsign worked on the second rig is still worked.
- Multiple operators at one station: `OPERATOR` per QSO, or per session?
- Clock: the client's or the server's? The server's is proposed — it is the machine at the
radio — with the offset shown if the browser disagrees by more than a second.
- Should the log file be configurable, or fixed beside the bookmarks?
- Import collisions: the proposed key is callsign, band, mode and time to the minute. Contest
operators work the same station twice in a minute on different bands, which that key allows;
a same-band dupe inside a minute is treated as the same QSO.