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>
The panel listed the same bookmarks twice: a status card at the top naming
the selected ones and what each was doing, and a checklist below naming all
of them with the checkboxes. Neither list said which row in the other it
meant, so choosing what to decode and reading what was being decoded were two
passes over the same names. The checklist itself sat squeezed against the
right-hand edge, because the row it lives in sets `align-items: flex-end` and
an inline `flex-direction: column` turned that into "push everything right".
It is one list now. A row carries its checkbox, its name, its frequency,
mode and decoder, and its own state, so picking and watching happen in the
same place. What the rig can hear moves up beside the switch, where it
explains why a selected bookmark reads out of span, and the selection adds
itself up under the list. The states lose the ✓/△/✗ they carried next to a
dot that already said as much, and say what they mean: "Out of span",
"Scheduler has it", "Nobody listening", each with the reason on hover. The
empty list now says which of the two reasons it is empty for, and what to do.
Save was live from the moment the panel opened, with nothing to save; it now
offers itself only when something has changed.
Two races behind it, both of which left the panel useless rather than ugly:
* The rig was whatever the panel was handed at load. Loading before the rig
list arrived handed it null, and the next telling only came when the
operator switched rigs — so the panel stayed empty and silent. Every rig
list refresh now passes the rig on, and both panels ignore one they have.
* The settings panels are wired once the session is up, but their modules
import asynchronously and the wiring skipped whichever had not arrived.
A panel that missed it kept dead buttons for the rest of the session: no
filter, no Select All, no Save. Wiring runs again when the modules land.
Closes#52
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>
Pass predictions were a third view inside the Weather Satellite Decoder card,
under Digital modes — a planning tool filed behind a decoder toggle, beside the
FT8 and WEFAX panels it has nothing to do with. Nothing about knowing when a
bird comes over belongs there.
Move them to /satellites, reached from Tools alongside Statistics, Recorder,
Settings and About: occasional destinations that live behind that menu rather
than taking a slot in the operating strip. Adding a sixth strip button wrapped
the phone nav onto two rows and cost the desktop strip its labels at 1280px, so
the tab is hidden from the strip exactly the way its four peers already are —
the nav is byte-for-byte what it was.
The prediction code moves out of sat.ts into its own plugin that loads with the
page, so the decoder card no longer carries it. Countdowns stop when the page
is hidden and each visit reloads, since passes go stale while it is closed.
The server grows a /satellites index route so a deep link or a refresh serves
the SPA shell rather than a 404.
Closes#47
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>
On a 390 px screen the transmit controls were laid out at x=400, off the
side of a tray 354 px wide: present, invisible, and reachable only by a
horizontal scroll with nothing to say it was there. The page scrolled
sideways by a dozen pixels as well.
Three causes, each in a different place.
A container query at the end of the stylesheet re-imposes `flex-wrap:
wrap` on a narrow tray's rows. That is right while a row runs left to
right; below the phone breakpoint the row is a column, and wrapping a
column starts a *second column* — which is what put the transmit
controls beside the tray rather than under it. The rule outranks the
phone one, two classes to its one, and sits later in the file, so it now
excludes itself below that breakpoint rather than being overridden.
The tray is a grid, and a grid column sizes to its content. One row
wider than the screen — the mode picker, six buttons across — dragged
the whole tray out with it. `minmax(0, 1fr)` lets the column be as
narrow as the phone and the rows wrap inside it.
The wavelength and signal-strength readouts are given the width of their
column on narrow screens, but with padding on a content box that is the
column's width plus the padding. Those dozen pixels were the page's
sideways scroll. They are border-box now.
Also: the rig picker was taking 139 px of a 338 px bar, pushing the rest
of the top bar into the overflow menu. It is capped and ellipsised on
phones, with the full name still in the menu it opens.
The map's filter bar, collapsed, keeps both its anchors and shrinks
inside them rather than dropping `left` to be sized by shrink-to-fit,
and no longer asks for a compositing layer it does not need. An
absolutely positioned, backdrop-filtered, composited box sizing itself
from its content is the shape of thing that renders as nothing on an
engine other than the one it was written against — which is what Edge
does with it. Unverified there: this machine has no Edge to test with.
tests/mobile-layout.mjs holds the page to it at 430, 390 and 360 px: no
sideways scroll, nothing laid out past the right edge, the transmit
controls inside the screen, the rig picker within its cap — and the
collapsed filter bar still on screen with a button to bring the filters
back.
The SSTV panel test stamped its picture with a fixed date, which the
panel's own retention window dropped once that date was a day old. It
uses a recent stamp now.
Signed-off-by: Stan Grams <sjg@haxx.space>
A receiver spreads by being linked to, and there was nothing to link to:
the routes carried the tab and nothing else, so "listen to this" could
only ever mean a screenshot and a frequency typed out in a message.
The query string now carries the dial -- rig, frequency, mode and
bandwidth -- in both directions. Opening a link selects the rig, sets
the mode, tunes, then applies the bandwidth: a mode change brings its
own default bandwidth with it, so an explicit bw has to land after it.
Frequencies are read the way someone writes them by hand (7074k,
14.074M) and written back as whole Hz, so what comes out of the address
bar is the same link in canonical form.
After that the address bar keeps up with the dial, which is what makes
it copyable at any moment rather than only at load. It is rewritten
with replaceState -- tuning is not navigation, and a swept dial would
otherwise bury the back button. A link button in the top bar copies
the current link; it folds into the overflow menu when the bar is tight.
Applying a link changes the radio, so an rx session says so instead of
failing control calls one at a time. A tab listening to a virtual
channel leaves the address alone rather than publishing a frequency the
rig is not on, and bw is skipped in both directions on rigs without
filter control, which would only refuse it.
The fixture pinned every state frame to 100 MHz plus jitter to keep
frames distinct, so no test could observe tuning at all. The jitter
moves to the S-meter and the fixture echoes set_freq/set_mode/
set_bandwidth, as it already did for squelch.
Signed-off-by: Stan Grams <sjg@haxx.space>
AIS, VDES and both APRS decoders were listed under the map plugin group
alone, so opening Digital modes and clicking AIS or APRS gave an empty
panel reading "Connected, listening for packets" while the decodes piled
up unprocessed in the plugin runtime. They appeared only if something
had opened the Map tab first, which flushed the queue. There is also a
map-data group naming exactly those four that nothing loads: the loader
is called with tab names and no tab is called map-data.
They load with the tab whose panels they fill now. map-core stays lazy,
since their calls into it are optional and the Map tab can go on paying
for Leaflet by itself.
tests/decode-flow.mjs follows a decode from the wire to the map: an AIS
vessel and an APRS beacon arrive on /decode, and it asserts both panels
fill with the map module confirmed absent, the mini view names the
vessel and offers a pin, following that pin lands on /map centred on the
vessel, and both decoders leave a marker. Nothing exercised any of this
before — the fixture served an empty decode stream, which is how the map
links came to be broken for every decoder at once.
The fixture stamps decodes as it sends them, since the client prunes
anything outside the retention window, and repeats them, since the views
collapse by vessel and need more than one frame to behave.
Signed-off-by: Stan Grams <sjg@haxx.space>
Tuning across a band edge moved the whole page under the cursor: the
band plan strip is in flow, so a range with no allocations collapsed it
from 18px to nothing and dragged every element below it up (measured at
1600x950: overview top 118 to 100, footer 1026 to 1008). It now keeps
its height whenever a band plan could be drawn at all, and only gives it
back when the feature is off, has no data, or there is no spectrum.
The bookmark rail gets the same treatment for consistency, though it
never moved anything — it is absolutely positioned over the top of the
overview. It stays up and blank rather than vanishing.
Which exposes something the rail was already doing wrong: it covers the
top of the plot, and a bare div still takes pointer events, so whenever
bookmarks were in range that band of the overview could not be dragged
or scrolled. Only the chips are targets now.
tests/spectrum-layout.mjs covers this: it streams spectrum frames, tunes
between a band with bookmarks and allocations and one with neither, and
asserts nothing moves and that the rail lets clicks through.
Signed-off-by: Stan Grams <sjg@haxx.space>