The logbook of issue #54, in the shape the proposal settled on.
A new crate, trx-logbook, holds the contact, the ADIF reader and writer, the
file, and the rules for telling one contact from two. ADIF because it is the
only thing the ecosystem reads: LoTW, eQSL, Club Log, QRZ and every other
logger take it and nothing else, so a log that cannot write .adi cannot be
uploaded, confirmed or moved. The reader is forgiving in the ways real files
are irregular -- lowercase tags, CRLF, missing header, unknown fields, a
declared length that is the only thing ending a value -- and carries what it
does not model through to the export, so a round trip does not strip what
another program wrote.
The file is JSON Lines, appended one line per contact. A log is the one
thing here that cannot be regenerated, and the bookmark store's whole-file
dump would rewrite megabytes to log one contact and lose all of them if the
power went halfway; an append costs the record being written and no more,
which a test tears a line in half to prove. Edits append revisions, deletes
append tombstones, and the file compacts when the superseded outnumber the
live.
The panel is its own tab and stands in every layout. An entry opens with six
fields and no more -- frequency, mode, rig name, time, and the callsign and
locator of whatever decode it was started from. A report stays empty: an FT8
SNR is not what was sent. Times come from the server, because the browser
may be a phone in another timezone, and the panel says so when the two
disagree by more than a second. Worked-before answers as a callsign is
typed.
A decode is not a contact, so the Log button on an FT8 or APRS row opens an
entry and logs nothing by itself.
The ham layout is the fifth operator layout, opening on the logbook with the
radio controls around it, offered only where the rig can transmit.
Two bugs found on the way, both in code written here: a frequency of a whole
number of megahertz ending in a zero rendered as a tenth of itself, in Rust
and in TypeScript alike, because trimming trailing zeros from "20.000000"
walks back through the point. The API also sits under /api/logbook rather
than /logbook, so it cannot shadow its own page the way /bookmarks does.
Closes#54
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
Opening the Map or Statistics page showed only what had been decoded since
the moment it was opened, and a reload — landing straight on the tab, so its
module loads at startup ahead of the history — was the only way to see the
rest. Two things were being thrown away.
The decode log the Statistics page counts lives in the map module, which is
lazy. Recording into a module that is not loaded yet is a no-op, and unlike
the map markers nothing replayed the log when it finally arrived, so every
decode heard before the first visit was simply never counted. Hold those
records in the client and hand them over when the module attaches.
The map's own replay covered APRS, AIS and VDES, whose plugins implement
syncMap, but not the grid squares: the FTx family and WSPR plotted locators
as decodes arrived and had no replay at all, so everything they heard before
the map loaded was lost, and the unique-grid counter with it. Both plot
through a helper now, which their syncMap replays oldest first. A replayed
WSPR spot carries the frequency it was heard on rather than one worked out
against wherever the dial has moved to since.
Pinned in decode-flow, whose history fixture gains FT8 and WSPR spots: after
a first visit the statistics count every stored decode and every grid square,
which before this change were 0 and 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
A client connected to several rigs decodes all of them at once, and the
decode stream carries every rig's traffic to every browser. The decoder
panels listed all of it: a station a background rig copied on another band
appeared in the APRS list next to the selected rig's, the vessel counts and
the "latest seen" lines counted both, the status lines said "Receiving"
because some other rig was, and the CW pane interleaved two rigs into one
stream of text that read as neither.
The page is about the rig the operator selected — the one whose spectrum is
on screen and whose audio is playing — so each panel now shows what that rig
heard: rows, counts, latest-seen, status, the live picture a WEFAX or SSTV
frame is painting, and the CW pane.
Nothing is dropped on the way in. The map is the whole station's view, has
its own rig filter, and would empty out if the plugins stopped feeding it, so
the histories still hold every rig and the map still plots them. That also
means a switch loses nothing: the runtime gained a rerender hook, which the
rig switch calls, and switching back brings the other rig's traffic up again.
The CW pane is the exception — a running stream of text cannot be unpicked
after the fact — so it starts empty on the rig switched to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
The decode SSE stream and the history behind it are not rig-scoped: every
rig's decodes reach the browser, each carrying the rig that heard it. The
panels on the decoder tabs want that — they aggregate the whole station —
but the mini views over the waterfall caption the spectrum underneath, and
they were reading the same unfiltered histories. A background rig copying
APRS on another band put its frames over the active rig's waterfall. The
mode gate did not help: it reads the mode of the rig on screen, so those
frames appeared whenever that rig happened to be in PKT.
Filter each overlay on the rig it belongs to, through one shared predicate
that compares a decode's rig_id with the per-tab active rig already driving
the spectrum and the audio. A decode that names no rig, and a session that
has not learnt its rig list yet, still show everything.
The FTx normalizer was dropping rig_id on the floor, so it now keeps it.
CW needed more than a filter: its lines accumulate character by character,
so two rigs copying at once braided their text into one unreadable line.
Lines in progress are now kept per rig.
The bar repaints in render() move into refreshDecodeBars(), which the rig
switch calls as well — otherwise the outgoing rig's frames stayed on screen
until the next state update — and which finally includes the CW bar.
Closes#49
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
The bookmark fix addressed one instance of a defect the TypeScript
migration left across the feature entries. app.js stopped being a
classic script, so its top-level declarations are no longer shared
globals, but the converted entries kept reading them as window
properties that nothing publishes.
Restore the broken behavior:
- ais, aprs, hf-aprs read serverLat, serverLon and haversineKm as
undefined, so every positioned packet rendered an empty distance.
- ais, aprs, hf-aprs, cw, sat, vdes, wefax, wspr called an undefined
postPath, so clear-history and decoder toggles threw.
- scheduler read authRole as undefined, so the lazy-load path never
self-initialized and the Settings tab opened an inert scheduler.
- background-decode read authEnabled as undefined, so control gating
fell back to role-only.
- vchan read fifteen application values and services as undefined:
mode and bandwidth sync, the out-of-band hint, RX audio restart, and
the frequency field all silently no-opped on a virtual channel.
- vchan wrapped window.refreshFreqDisplay, capturing an undefined
original exactly as it did for setRigFrequency, so leaving a channel
never restored the application's own frequency display.
- _audioChannelOverride was a const that nothing could assign, so RX
audio always subscribed to the primary channel.
- ftx-family read fmtTime, a helper legacy ft8.js owned locally, so
decode bar timestamps rendered empty.
Declare the contract once in plugins/host.ts and import it from the
feature entries, rather than restoring globals that
docs/frontend-architecture.md excludes. trx.state gains jogUnit,
rxActive and audioChannelOverride, and makes lastModeName writable;
trx.core gains the tuning, RDS, WFM, jog and RX audio services the
entries need. vchan interception moves to an interceptFreqDisplay
service method that refreshFreqDisplay calls, matching the frequency,
mode and bandwidth interception it already registers.
Reading registry-built elements through a strict lookup is the same
defect as in bookmarks: renderTimelineNeedle guards its result, but
schedulerEl throws, so the now-initializing scheduler crashed on the
timeline needle group that its own SVG creates.
Feature tests move onto a shared host fixture, and entries that now
import a common module are bundled through bundleEntry like the other
shared-module entries. Covers scheduler self-initialization and the
distance path that the bare window reads broke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>