Commit Graph
440 Commits
Author SHA1 Message Date
rouggy 39bd1ff414 feat: the play button stops the transmission while it is going out
A second press restarted the message from the beginning. What the operator wants
there is to cut it short — enough heard, or pressed by mistake with the
transmitter keyed — and then to be able to send it again at once.

It now shows a stop square while playing and releases the PTT through the same
path the natural end uses, so nothing is left keyed. Together with the device
handover fixed a moment ago, sending again immediately afterwards works.
2026-07-31 20:23:03 +02:00
rouggy 734931cf54 chore: release v0.22.5 2026-07-31 17:18:18 +02:00
rouggy cad4ec4b72 feat: filter cluster spots on new POTA and new county
The grid already badges them; only the filter was missing.

They are not entity statuses — a new park or county is another dimension of the
same spot — so the filter key is now a superset of the status, and matching is
an OR across all of them, as it already was for a previously-worked callsign.
The chips carry the colours their badges use in the grid: a filter that does not
look like what it selects has to be learned twice.
2026-07-31 17:04:01 +02:00
rouggy 5623b860c2 chore: release v0.22.4 2026-07-31 16:56:41 +02:00
rouggy 3e22c4d1a3 feat: filter award references by mode class
All / CW / Phone / Digital, stacked on top of the worked/confirmed filter. The
question an operator actually has — which DXCC entities have I worked on CW but
not confirmed on CW — needs both at once; answering only one of them is what
sends people to a spreadsheet.

References carried bands but not modes, so the award computation now aggregates
mode CLASSES per reference, worked and confirmed separately. Classes, not ADIF
modes: nobody chasing CW cares whether the digital side was FT8 or RTTY, and a
list of twenty mode names would not answer the question. An unrecognised mode
counts as data rather than as nothing, so a new digital mode does not vanish
from the filter the day it appears.

Two rules the filter needs to be honest:

  - Not-confirmed means not confirmed ON THIS MODE. An entity worked on CW and
    confirmed on SSB is still a CW entity to chase, and that is exactly what the
    filter is for.
  - "Not worked" plus a mode is a contradiction — an unworked reference has no
    mode — so the mode filter stands aside there instead of emptying the list.
2026-07-31 16:09:11 +02:00
rouggy e4217da010 fix: right-click menu cut off at the bottom of the window
Closes #9.

It was clamped against a GUESSED height — 230 px with the upload submenu, 110
without. The menu is far taller than that with a selection, so right-clicking on
a row near the bottom hid half the entries, delete among them.

It is measured now: placed above the cursor when it does not fit below, pinned
to the top with its own scrollbar when it fits neither way. Its height depends
on which actions the caller passes, so no constant could have stayed right.
2026-07-31 12:25:37 +02:00
rouggy c5ac945de0 fix: digital-mode grouping ignored by the spots already listed
The backend honours the setting: it normalises the log side and the spot's own
mode through the same grouper, so an FT4 spot on a band worked in FT8 reads as
worked. That was not the fault.

Spot statuses are cached per call+band+mode and the cache never expired. Ticking
the box in Settings therefore changed nothing on screen — every spot already
listed kept the answer to the old question, and only new arrivals were judged by
the new rule. The cache is now dropped when settings are saved.

Also: writeUiPref swallowed a failed database write. The interface kept working
from localStorage while the BACKEND — which reads some of these keys, this one
among them — still saw the old value. The two disagreeing in silence is exactly
the shape of this bug, so the failure is now logged.
2026-07-31 12:20:07 +02:00
rouggy eb40991482 fix: the power headings sat one column left of their boxes
The rule between the band-driven columns and the mode-driven ones was an extra
empty cell added to the body rows only. Seven cells per row against six in the
header slid every heading across: Phone stood over the TX antenna, CW over
Phone.

The rule is now drawn on the first power cell itself, in the header and the body
alike, so the two cannot drift apart again.
2026-07-31 12:00:21 +02:00
rouggy 781cf636ea fix: one table per band instead of two
Antennas and power were two tables listing the same bands, one under the other,
so configuring a band meant matching rows by eye between them.

They are now one row per band. That the two settings are stored separately and
applied on different triggers — antennas on band change, power on band or mode
change — is a backend detail, and no reason to split what is a single decision
per band for the person configuring it. A rule in the header keeps the two
groups readable.
2026-07-31 11:52:03 +02:00
rouggy 62b6fe0a3a feat: FlexRadio TX power per band and mode class
Settings → FlexRadio, beside the per-band antennas and applied the same way:
when the band or the mode changes. 1.5 kW that is fine on SSB has no business
going into an FT8 signal on the same band.

Three classes rather than one row per mode, because what decides the power is
duty cycle and what the amplifier will take of it, not the mode's name — FT8 and
RTTY punish an amplifier the same way, SSB and AM do not. The classification is
pinned by a test: a digital mode sorted as phone would deliver exactly the
1.5 kW this exists to prevent.

An empty box means "leave the power alone", never zero watts, and an unknown
mode changes nothing — setting a transmit power from a name we do not recognise
is not a good guess.

Unlike the antennas, a locked band does not skip it: the lock means "do not let
the rig drag my log entry around", not "let the amplifier keep whatever the last
mode left".
2026-07-31 11:30:46 +02:00
rouggy a1ceea978b fix: no play-on-air button on CW
In CW the transmitter builds its tone from the keyer and ignores the DAX TX
audio path, so the button keyed the rig and sent silence. Confirmed on a Flex:
our side reported the playback as done while nothing went out.

Only the button goes. The row stays, or a CW operator who stopped their
recording would be left with no way to resume it — and the recording itself is
still worth keeping.
2026-07-30 23:34:23 +02:00
rouggy e152ef0ee0 feat: record CW QSOs again
CW was excluded because SmartSDR does not route the operator's own sidetone
through DAX, so the recording sounds one-sided. The audio path is open all the
same and the other station IS captured — which is the half worth keeping.

The frontend had ONE list serving two purposes: which modes may be recorded, and
which modes the voice keyer may transmit on. Adding CW to it would have let the
DVK key the rig with speech on a CW frequency. They are now two lists: PHONE_MODES
for the keyer, RECORDABLE_MODES (phone + CW) for the recorder.

Digital modes stay out of both: their audio is a modem tone nobody will replay.
2026-07-30 23:13:21 +02:00
rouggy 93b4964b3b chore: release v0.22.3 2026-07-30 22:33:01 +02:00
rouggy 7a7fa62af7 feat: choose how dates are displayed (Standard / French / US)
Settings → General, beside the language. An operator reading 2026-07-30 as the
7th of the 30th month is reading their own log wrongly, and that is a display
problem.

Storage stays ISO/ADIF, deliberately and permanently: it is what ADIF
specifies, it sorts correctly as text, and a stored format that followed a UI
preference would make the log unreadable the day the preference changed. Exports
and uploads are untouched.

Two details that decide whether it actually works:

  - The columns are rebuilt when the format changes. The formatters are captured
    inside AG-Grid's column definitions, so nothing else would notice and the
    table would keep the old format until something forced a rebuild.
  - main.tsx re-reads the choice after the portable prefs are pulled from the
    database. The module reads localStorage at import time, which is BEFORE that
    sync — so a copied data/ folder would have shown ISO until the next launch.

UTC is not part of the choice and never will be: a logbook is kept in UTC, and
showing local time would make the display disagree with the QSO's own record
twice a year.
2026-07-30 22:22:33 +02:00
rouggy 6ee4c430c7 feat: colour the QSL and upload status letters
Y green, N red, R blue — colour only, no pill or badge. These columns sit among
callsigns and dates, and a row of coloured boxes would shout louder than the
callsign it belongs to.

Twelve columns: paper QSL, LoTW, eQSL, Club Log, HRDLog, QRZ.com both ways, and
OpsLog's own card and recording flags. All four views the operator named —
recent QSOs, worked before, NET Control, QSL manager — turn out to share this
one grid, so they are all covered by construction rather than by four edits that
could drift.

Semantic tokens rather than fixed colours, so the four themes follow and the
contrast stays right on the light ones. ADIF's I (ignore) is deliberately left
in the default ink: it is neither good news nor bad, and colouring it would put
it in one camp or the other.
2026-07-30 22:12:23 +02:00
rouggy 0df6370c59 docs: the author's wording for the remote-delete hint
Says what the switch does, then names the one limit that changes the decision.
Drops my "no undo" line: deleting a QSO is already understood to be permanent,
and repeating it reads as a warning about the checkbox rather than the QSO.
2026-07-30 22:04:15 +02:00
rouggy fa94657619 docs: cut the remote-delete hint to what must be known before ticking it
Four sentences of API behaviour under a checkbox. Two facts actually change the
operator's decision: it cannot be undone, and QRZ.com cannot reach QSOs uploaded
before OpsLog started keeping their record number.

The rest — how Club Log matches, that the local delete happens regardless, that
the log records each answer — is true, and belongs in the log and the code where
it already is. Documentation in a checkbox hint is documentation nobody reads.
2026-07-30 22:03:02 +02:00
rouggy f5b74856c6 fix: the remote-delete switch sits above the service tabs
It applies to QRZ.com and Club Log both, but it was rendered after the tab
strip — so it read as a QRZ setting when the QRZ tab was open, appeared to
repeat itself on the Club Log tab, and was invisible to anyone opening a third
one.

Above the tabs it belongs to the panel, which is what it actually is.
2026-07-30 22:01:45 +02:00
rouggy d42fdab85c feat: withdraw a deleted QSO from QRZ.com and Club Log
Opt-in, off by default: neither service can undo it, and a local delete is not
always a statement about the world — a duplicate cleared out of a contest log
was never meant to change the DX's log.

The two APIs are not alike, and the difference decides what is possible:

  - Club Log deletes by MATCH (callsign, exact timestamp, band number), so it
    works for every QSO in the log, past or future. The band table is its own
    list and is pinned by a test: a band mapped to the wrong number does not
    fail, it points the delete at a different QSO.
  - QRZ.com deletes ONLY by logid. So markExtUploaded now stores the LOGID it
    already received on every upload — it was being logged and thrown away. A
    QSO uploaded before this cannot be withdrawn from here at all, and the log
    says so by name rather than leaving the operator to assume it worked.

Both run BEFORE the local delete, because both need the QSO's own fields to
find their copy and there is nothing left to ask with once the row is gone.
Neither can block it: the operator asked for the QSO to go, and a refusing
website is not a reason to keep it.
2026-07-30 21:57:40 +02:00
rouggy 83a8708d60 fix: remember the award sort, and the grey line, across restarts
The sort choice is now written through writeUiPref and both keys are registered
as portable, so they travel with a copied data/ folder like every other UI
preference.

Registering them is the part that matters: writeUiPref stores to the DB, but
only keys on the portable list are read BACK at startup. A key written and never
restored looks exactly like one that was never saved — which is what would have
happened here.

The grey-line toggle had the same gap. It called writeUiPref from the day it was
written but was never added to the list, so it came back off on the next launch.
Found while adding the award keys beside it.
2026-07-30 21:49:25 +02:00
rouggy c0671bfe2e feat: sort award references by reference or description
Reference stays the default: that is how award lists are published and how a
paper list is read. Description answers the other question an operator actually
has — "have I worked anything in Alicante" — which reference order scatters
across eight thousand rows.

Sorted where the filtered list is built, so the grid and list views inherit it
rather than each growing its own copy that could disagree.

localeCompare rather than a byte comparison, because these lists are Spanish,
French and Italian place names: "Ávila" belongs beside "Avilés", not after
"Zaragoza". References compare numerically for the same reason in reverse —
08019 must not sort between 0801 and 081.
2026-07-30 21:47:05 +02:00
rouggy ec0cb95de9 style: the play button is green
It was the same red as the recording indicator beside it, so at that size the
two read as one control. Green is also what the theme already uses for a
go-ahead action (the amplifier ON buttons), and it is a semantic token, so it
follows all four themes rather than pinning a colour.
2026-07-30 21:43:41 +02:00
rouggy c6088bb2de fix: the stop button overlapped the recording counter
Two absolutely-positioned boxes at guessed offsets: right-14 and right-2. They
collided as soon as the counter passed a minute and the text grew — a layout
that only looked right for the first 60 seconds, and did not even manage that at
this font size.

One flex row with a gap, so spacing is a consequence of the content rather than
an arithmetic guess about it. The callsign field also gains right padding while
the badges are shown, so a long call runs under them instead of being typed over.
2026-07-30 21:39:50 +02:00
rouggy 069728ac68 feat: stop a QSO recording and play it back on the air
The situation, in the operator's words: working VP2MAA, recording, and he asks
to hear his own signal. So: stop, play, and the recording is still saved with
the QSO.

Stopping FREEZES the take rather than ending it — nothing more is captured,
nothing is discarded, and logging writes the file exactly as before. Playback
reuses the voice keyer's path: same output device, same PTT keying and release
with its generation guard, so a replay cannot be cut short by a stale unkey.

Playing is refused while still recording. The transmitted audio would feed back
into the same take on any rig monitoring its own transmission, and stopping is
one click the operator has already made — it is their statement that the take is
finished, not a hoop.

The elapsed clock freezes with the recording and resumes from where it stopped,
because it now shows the LENGTH OF THE FILE rather than the time since the QSO
began. Only a fresh take returns it to zero.
2026-07-30 21:36:42 +02:00
rouggy f221c0ee1e feat: record a contact by hand when automatic recording is off
A red dot in the slot the counter will occupy. Click it and the counter starts,
the dot disappears, and logging the QSO writes the file exactly as an automatic
recording would — the save path already keyed off an active take, not off the
setting.

With automatic recording disabled the capture engine is not idle, it is not
running at all, so a manual take starts it. Two consequences handled here:

  - It is released again when the take ends — logged, cancelled, or dropped for
    being a digital mode. An operator who records one contact does not expect
    their microphone held open for the rest of the session. The release is a
    deferred call at the top of the save path so every exit goes through it,
    rather than one that has to be remembered at each return.
  - There is no pre-roll. An automatic recording opens with the seconds BEFORE
    the callsign was typed; a manual one can only begin now. The button's
    tooltip says so, because otherwise the difference between two files is a
    mystery to the person holding them.

The button only appears where it can work: devices configured, automatic
recording off, and a mode whose audio is worth keeping. That state is asked of
the backend rather than inferred from a preference in the interface, so the two
cannot drift apart, and it is re-read when settings are saved.
2026-07-30 21:24:51 +02:00
rouggy 013492adb0 fix: name CAT backends by how the radio is reached
The list read "Yaesu (native CAT)", "Kenwood (native)", "FlexRadio / SmartSDR
(native)". Native says how OpsLog is built, which is our business; the operator
opening that menu is answering a different question — how is my radio plugged
in.

So: OmniRig, FlexRadio (API), Yaesu (USB), Kenwood (USB, network), Xiegu (USB),
Icom (CI-V USB), Icom (CI-V network), TCI. Shorter, and each entry now tells
someone with a cable in their hand which line to pick.
2026-07-30 21:20:08 +02:00
rouggy 3a87b58ba3 feat: Kenwood CAT over a network serial bridge
The same ASCII stream over a socket instead of a wire: ser2net, an
Ethernet-serial adapter, a Raspberry Pi at the radio. One TCP transport
presented as a serial port, so the backend keeps a single code path.

Explicitly NOT the radio's own RJ45. A TS-890 or TS-990 speaks Kenwood's
KNS/ARCP there — session, authentication, a different protocol — and that needs
one of those radios in hand to write honestly. The setting's help text says so,
because an operator who plugs in their TS-890's Ethernet port and types its
address deserves to learn that from the interface rather than from silence.

Two details that decide whether this is usable or maddening:

  - A read deadline expiring is reported as "no data yet", not as an error. It
    is exactly what a serial read timeout means to the caller; as an error it
    would make ask() abandon a rig that is merely thinking.
  - The log line names what was connected to. "connected on  @ 0 baud" after a
    network connect sends someone hunting a serial fault that does not exist.

The host wins over the COM port when both are filled: it is the more deliberate
setting, and silently preferring the wire leaves someone staring at an address
they typed and a radio that never answers.
2026-07-30 21:13:19 +02:00
rouggy e6b8a17772 fix: a rig reporting LSB/USB left the mode selector empty
From a Kenwood trace: IF answered mode digit 1 on 7.160 MHz, the backend read
LSB correctly, and the entry form showed no mode at all while the frequency
tracked perfectly.

Nothing was wrong in the backend. Radios report the SIDEBAND, and the station's
mode list holds SSB — so the value pushed into the selector was not in its own
list of options and nothing could match. Yaesu and Icom report the same way, so
this was never Kenwood-specific; it only surfaced there.

Normalising at the point where a rig-reported mode enters the form fixes every
backend at once, rather than editing three mode tables and the next backend
someone writes.

It is also the ADIF-correct direction, which matters more than the selector:
LSB and USB are SUBMODEs in ADIF, not modes. MODE=LSB is not a valid value and
is what would have gone to LoTW and eQSL.

An operator who has deliberately put LSB or USB in their own mode list keeps
them — the list is their statement of what they log, not ours.
2026-07-30 20:50:05 +02:00
rouggy f82631690a fix: grey line cut off at the edge of the world copy
The map repeats the world sideways, and zoomed out several copies are on screen
at once. The rings span −180…+180 once, so the shading ended in a hard vertical
edge wherever the next copy began: it read as a rectangle laid over the planet
rather than as night.

Each ring is now drawn again shifted a full turn either way. Three copies cover
every zoom this map reaches and cost nothing — three more polygons on a canvas
layer.

Zooming the map to fit one world would have hidden it rather than fixed it, and
would have taken away the free pan and zoom the operator has.
2026-07-30 19:35:26 +02:00
rouggy 3b58e39eec feat: grey line on the world map; fix: trace checkboxes lied about their state
Grey line — the day/night terminator and its twilight band, off by default,
redrawn every minute so a map left open all day is not silently wrong by
evening. Its own Leaflet pane below the overlays, non-interactive, so it never
hides the path or beam nor swallows a click.

The solar maths is pure and was checked against known positions before being
wired to anything: both solstices, the March equinox, the subsolar longitude at
12 UTC, and day/night at Paris, Tokyo, New York, Sydney and Reykjavik across
seasons and hemispheres. That last set is what caught the real bug: the equation
has TWO solutions per meridian and the naive branch put New York in daylight at
02:00 in December — the terminator at +63° where it belongs at −63°. A shaded
map that looks entirely plausible and is exactly mirrored. It now anchors on the
classical terminator and takes the branch nearest it, which is also what makes
the twilight band follow the terminator instead of jumping hemisphere.

Separately, from a Xiegu user: the protocol-trace checkbox does not stay ticked.
It is React state initialised to false on every mount, so a trace still running
came back unticked — the operator ticks it to "switch it on", which switches it
OFF, and the log they send contains no trace at all. Worse than a cosmetic bug:
it silently defeats the one tool asked for to diagnose their radio. Both boxes
(CAT and WinKeyer) now read their real state from the backend.
2026-07-30 18:06:12 +02:00
rouggy 45e3f3edb8 fix: bulk update no longer holds the application hostage, and says what it does
Two faults in the progress bar shipped an hour ago.

It was a modal. A hundred QSOs is a minute; a contest log is two thousand and
ten minutes, and for all of it the operator could do nothing else — while their
radio is on. It is now a card in the corner: same bar, same count, same
callsign, none of the imprisonment.

And the menu said "Update from QRZ.com" while the code has always walked EVERY
configured provider, falling through to HamQTH when QRZ answers not-found. The
label is now "Update from the callsign databases", which is what happens.

Noted while reading that code, NOT fixed here because it trades against the
user's API quota and is their call: this path goes through the lookup cache, so
a second run on the same callsigns re-reads the cache rather than reaching the
provider. The cache comment already argues the opposite for deliberate clicks —
"a lookup the operator asked for by clicking is a deliberate act and must reach
the provider" — and a right-click on a selection is exactly that.
2026-07-30 18:00:19 +02:00
rouggy c931d8a762 feat: progress bar for bulk QSO updates from QRZ.com
Selecting the 102 contacts of a freshly imported contest log and updating them
from QRZ.com is 102 network round trips. Nothing moved on screen while it ran,
so the only honest reading was that the application had frozen — and the natural
response to that is to kill it, halfway through.

The backend now emits a progress event per QSO and the frontend shows the same
overlay the ADIF import uses: a bar, the count, and the callsign being queried.

The callsign is reported BEFORE the lookup rather than after. With a slow
provider that is the difference between "waiting on F4BPO" and a name that lags
one QSO behind whatever is actually taking the time — which would be the field
someone stares at while deciding whether it is stuck.

The overlay is cleared by the closing event and again in a finally, so neither
a completed run nor a failed one can leave it on screen.
2026-07-30 17:54:07 +02:00
rouggy eb271e8f20 fix: Kenwood split read from FR/FT when the status frame omits it
Reported from a Flex in Kenwood CAT mode: frequency read perfectly, split never
appeared. Not every rig speaking this dialect fills IF's split bit.

Rather than guess which column that firmware populates, ask the question that
DEFINES split — is the transmit VFO a different VFO from the receive one — which
is exactly what FR and FT answer, and the same rule the Yaesu backend settled on
after several wrong turns. FR is also trusted over IF for which VFO is in use:
they are asked in the same breath, and a rig vague about the split bit may be
just as vague about the VFO field.

Cost is bounded: a rig that rejects FR/FT answers "?;" once and is never asked
again, so this is two short commands per poll only where it works. Both paths
are tested against the emulator — split found through FR/FT with IF silent, and
IF-reported split still working on a rig that refuses FR/FT without re-asking.

Also extends the CAT wire trace to the Kenwood backend, ASCII quoted so an empty
reply is visible as such: "" and ";" look identical unquoted, and telling them
apart is the whole question when a rig half-supports a command. If this fix is
not the whole story on real hardware, the trace is what will say so.
2026-07-30 16:50:06 +02:00
rouggy 712a124081 chore: release v0.22.2 2026-07-30 15:14:32 +02:00
rouggy 08a932c9c1 fix: drop the per-column filters from the recent-QSO grid
They searched only the rows on screen. With the grid holding the most recent
page, filtering a column for RSGB-IOTA across a 28 000-QSO log returned nothing
— which reads as "the log does not contain it", not "it is not on this page".
That is how an import that had worked perfectly came to be reported as broken:
the tool answered a narrower question than the one being asked, and said so
only by being empty.

The advanced filter queries the whole logbook and is the honest instrument. The
headers also get back the width the filter menu icon was taking, which is the
smaller reason but a welcome one on a grid this dense.

No saved filter can be stranded by this: the grid persists widths, sort and
visibility, never a filter model.
2026-07-30 15:13:25 +02:00
rouggy 4d5c5c3eb3 feat: move ADIF fields on import, for contest exports that misplace the exchange
An operator worked the RSGB IOTA contest in another logger and got back records
carrying <STATE:5>EU005: N1MM-class software stores the received exchange in the
column its contest module uses, not the one ADIF reserves. Imported verbatim,
every QSO gains a US-state field reading "EU005" and the IOTA award sees
nothing — the reference is in the log, just not where anything looks for it.

The import dialog gains optional source → destination rows, prefilled with
STATE → IOTA since that is the case that prompted it. Applied to the RAW record
before conversion, so it works for promoted columns and Extras alike and the
destination is parsed exactly as if the file had carried it there.

Two rules, both from the same principle that the file outranks our guess:
  - the source is CLEARED, so a reference does not linger in STATE where it
    would show as the contacted station's US state in the grid and every export;
  - a destination the file already filled is kept, judged against the record as
    it ARRIVED — otherwise the result would depend on Go's map iteration order,
    which is deliberately random.

The swap case in the test is what forced that second rule to be stated: "never
overwrite" and "exchange two fields" cannot both hold silently, so a destination
that is itself a mapping source is treated as an explicit swap and nothing else
is overwritten.
2026-07-30 14:57:05 +02:00
rouggy d322ea9a07 feat: report UI crashes instead of showing a blank window
Two white-screen reports are open — one after logging a 10 GHz QSO, one on
starting a DVK auto-call — and neither could be investigated, for the same
reason: a render error emptied the window and left NOTHING. No line in
opslog.log, no dialog, nothing the operator could send but the words "white
screen". A fault the user cannot report is a fault that cannot be fixed.

So before hunting either trigger, the reporting path:

  - An error boundary catches a render throw, writes the error and component
    stack to the app log through the new LogUIError binding, and shows it on
    screen — selectable, with Copy and Reload.
  - Global handlers catch what a boundary cannot: throws from timers, event
    handlers and rejected promises. An auto-call loop lives entirely in
    callbacks, which is exactly where the second report came from, and those
    leave no trace at all today.

It deliberately does not try to resume: React cannot promise sane state after a
render throw, and a half-working logger would be worse than a clear stop.

This does not fix either crash. It makes the next occurrence arrive with its
cause attached, which is the step that has been missing.
2026-07-30 14:36:19 +02:00
rouggy 75c02ea2a0 feat: CI-V byte trace, for a fault the command table cannot explain
Reported on an IC-9100: the MOX button does not transmit, it sets split. The
source sends the PTT command (0x1C 0x00) and nowhere sends the split one (0x0F),
so the discrepancy is between what OpsLog sends and what the rig acts on — and
that link has already been shown, in the same operator's log, to lose frame sync.

Rather than guess from the command table, this adds what settled the WinKeyer
bug in one line: the actual bytes. Settings → CAT → Log the CI-V protocol writes
every frame in both directions as hex. RX is traced BEFORE framing, since the
bytes as they arrived are what reveals a lost boundary — a decoded view would
hide exactly the fault being hunted.

Session-only, like the keyer trace: it is a diagnostic, and a log full of hex
helps nobody who forgot it was on.

I am not claiming a cause yet. The last time I inferred one from a protocol
document rather than from evidence, I inverted a digit and broke the case that
worked.
2026-07-30 14:30:05 +02:00
rouggy b16cb973f2 feat: native Kenwood CAT backend
A sixth CAT backend, talking to a TS-590/890/990/2000 over its serial port with
no OmniRig in between — frequency, mode, VFO, split and PTT. Elecraft K3/K4 and
the "Kenwood" setting on other radios speak the same dialect.

The protocol was not researched from scratch: internal/catemu already ANSWERS
these commands, pretending to be a TS-2000 so an ACOM amplifier follows OpsLog.
The IF frame layout here is read from the same format string catemu emits, so
the two halves of the repository agree by construction — and the test caught my
own fixture being one character short, which is exactly the error that layout
invites.

Design notes worth keeping:

  - IF; is the poll. One frame carries frequency, TX state, mode, VFO and split,
    so simplex operation costs a single round trip; the other VFO is only asked
    for when split is actually on.
  - FA/FB take ELEVEN digits here where Yaesu uses nine. That is the likeliest
    place to copy the Yaesu backend and be wrong by a factor of a hundred, so it
    has its own test.
  - Every lesson the Yaesu backend learned the hard way is built in from the
    start: replies matched to the command that asked, "?;" remembered so a poll
    stops paying a timeout for an unsupported command, the serial handle closed
    before reopening, and a rig that answers nothing reported as absent rather
    than "connected".

Untested on hardware — I have no Kenwood here. The frame parsing is covered by
tests against catemu's own format.
2026-07-30 11:56:41 +02:00
rouggy da886de189 feat: support ERC rotator controllers through the existing GS-232A backend
An ERC (Easy Rotor Control, incl. ERC Mini) emulates Yaesu GS-232A/B, which is
exactly what the backend written for the microHAM ARCO already speaks — azimuth
out (Maaa), azimuth in (C), stop (S), which is the whole of what was asked for.
So this is two small gaps rather than a new backend:

  - Serial speed was hardcoded to 9600. An ARCO's virtual COM ignores it, but an
    ERC runs at whatever its own configuration sets, and a mismatch reads as a
    dead rotator. It is now selectable beside the COM port.
  - The entry was called "microHAM ARCO", so an ERC owner would never have found
    it. It is named after the PROTOCOL now: "GS-232A controller (microHAM ARCO,
    ERC…)".

The help text states the condition plainly in both languages, because it is the
one thing that will otherwise waste an evening: an ERC left on Hy-Gain DCU-1
speaks a different command set and simply will not answer.

Untested on hardware — I have no ERC here, and the GS-232 path itself is proven
on a real ARCO.
2026-07-30 11:07:57 +02:00
rouggy a8990ff29d fix: the status bar named the settings database, not the logbook
Spotted by the author: on SQLite the database chip showed a.dbPath — the
settings/profile database — under a tooltip reading "Local SQLite logbook".

Those became two separate files when the logbook was split out, so an operator
checking where their QSOs live, or which file to copy before a trip, was pointed
at the wrong one. MySQL was already right.

It now resolves the logbook the same way connectLogbook does: the active
profile's own file if it names one, then the default logbook.db, and the settings
db only in the case where it genuinely doubles as the logbook.

Also moves the 4O3A mark beside its menu label instead of the far edge of the
row, as asked.
2026-07-29 23:53:18 +02:00
rouggy e3b810b4ff feat: draw the actual 4O3A mark in the sidebar, not a text badge
The wordmark I put there was not what the author expected: the brand is "4", a
green waveform of stacked lenses, then "3A".

Only the waveform is SVG. The digits stay HTML text so they keep the sidebar's
own typeface at any theme or zoom — an SVG <text> would pick its own font and
drift from the menu around it. Seven ellipses, tallest in the middle, match the
shape of the mark.

Still drawn rather than fetched: an image off the web has no verifiable
provenance, and this stays one small component to swap if the official SVG is
preferred.
2026-07-29 23:37:57 +02:00
rouggy 337392bb6d feat: mark the 4O3A panels in the settings sidebar
Antenna Genius and Tuner Genius now carry a small 4O3A wordmark, so the two
panels that drive that hardware are recognisable without reading the labels.

Drawn inline rather than shipped as an image file, following the language
picker's flags: it scales with the row, follows the theme through currentColor,
and adds no binary asset to the repository.

It is a plain wordmark, not the manufacturer's logo artwork — I did not pull an
image off the web, since I cannot verify the provenance or licence of what I
would find, and redistributing a company's mark inside the product is the
author's call. The component is one function and takes the official SVG in its
place if that is preferred.

Not applied to the Amplifier panel: it covers both the 4O3A PowerGenius XL and
the SPE Expert range, so a single vendor mark there would be wrong.
2026-07-29 23:28:27 +02:00
rouggy d98bb2e929 chore: release v0.22.1 2026-07-29 23:13:27 +02:00
rouggy 3ab6b95ff8 fix: auto-call CW gap started before the message had finished
Reported: a 6 s auto-call gap gives clearly less than 6 s between the end of one
CQ and the start of the next.

The wait after sending watched only the keyer's busy signal. That signal travels
from the keyer to the window as an event, and the code gave it one second to
appear before giving up — if it had not arrived and cleared by then, the wait
ended immediately and the gap ran from the START of the call. A 6 s gap after a
4 s CQ becomes about 2 s of silence, which is exactly what was heard.

The message's own duration is now a FLOOR: the wait runs at least the estimated
sending time, and the busy signal only extends it (slow link, long buffer),
capped as before. The estimate can only be approximate, but it cannot be skipped
by a late event, and erring long costs a slightly wider gap rather than a call
that steps on the previous one.
2026-07-29 22:28:18 +02:00
rouggy d536c39ef5 docs: label the three numbers in the QSO counter
"Showing 10000 of 23683 matches · 28648 total" was read as a filter matching more
than the log holds. The numbers were right — 23 683 matches out of 28 648 QSOs,
capped at 10 000 displayed — but the middle one belonged to "matches" while
sitting where "of N" normally means the total.

Each number is now named where it appears: shown · match the filter · in the log.
Thousands separators too, since 23683 and 28648 are hard to compare at a glance.

No counting change: both counts run over the same table, so matches can never
exceed the total.
2026-07-29 18:10:39 +02:00
rouggy 482f81fe45 docs: say that the import option also fills the confirmation statuses
Asked for as a missing feature. It is not missing — the "fill my station fields"
option has stamped the default QSL / LoTW / eQSL / Club Log / HRDLog / QRZ.com
statuses on empty fields since v0.14. What was missing is any mention of it: the
option's description talked only about MY_* fields, so the behaviour was
undiscoverable except by comparing a log before and after.

The description now says it, in both languages, and names why it matters — a
WSJT-X log carries almost no QSL fields.

The rule is also pinned by a test now, which meant splitting the fill from the
settings lookup: blanks are filled, existing values are never touched.
Overwriting them would erase confirmations the operator has actually received,
and that is the half a future edit is most likely to get wrong.
2026-07-29 17:56:10 +02:00
rouggy 54caee37c4 chore: release v0.22.0 2026-07-29 14:55:17 +02:00
rouggy 6f5acc2eef docs: name the rig setting that makes the voice keyer inaudible
The weak audio was not a level at all: the radio was still modulating from its
front microphone, so almost nothing of the USB feed reached the air. On an FTDX10
that is MENU → SSB MOD SOURCE = REAR.

Worth putting in the settings hint rather than leaving in a conversation — every
operator wiring a voice keyer for the first time meets it, and no amount of gain
in OpsLog can compensate for a rig listening to the wrong input.
2026-07-29 14:46:50 +02:00
rouggy 67a03c3ddc feat: a level control for the voice keyer, and name every CAT backend in its PTT list
The messages went to the radio exactly as recorded. Nothing in OpsLog could
raise them, so a microphone captured quietly drove the rig quietly and the only
remedies were the radio's own USB input menu or the Windows mixer — which is
where the operator was heading. There is now a level from 10 to 400 %, applied
with clamping (a wrap would turn loud speech into noise on the air), and Play
previews at that same level so the adjustment is made against what will actually
be transmitted.

The PTT method list also named only OmniRig, Flex, Icom and TCI, so choosing a
native Yaesu left "CAT" with no backend beside it — which reads as "there is no
CAT PTT for my radio" and sends the operator to RTS on a COM port that has
nothing to do with the rig. Same list-needing-every-member shape as three
earlier bugs in this feature. The TestPTT log line had the same rot: it said
"CAT via OmniRig" whatever backend was running.
2026-07-29 14:29:47 +02:00