Commit Graph
781 Commits
Author SHA1 Message Date
rouggy afd0714301 chore(changelog): open 0.26.19 with the multi-radio switch 2026-08-27 00:47:28 +02:00
rouggy 2594697e00 fix(cat): the chip says what YOU called the radio
It showed what the radio calls itself over CAT — 'Kenwood (911)' for a K3,
a bare 'CAT' for a Flex that reports nothing useful. That is the answer to
a question nobody asks once the rig has a name in the list, and it made
the two radios look alike in the one place they have to be told apart.

The name the operator typed comes first now, on the chip and in the menu,
and falls back to the radio's own identity when they typed none. The model
identity is still in the tooltip, where it answers 'what is actually
connected' without taking the place of 'which of my rigs is this'.
2026-08-27 00:42:26 +02:00
rouggy cf5efea3e4 fix(cat): the chip opens the radio menu with one radio too
It only became a menu from two radios, on the reasoning that a single
radio has nothing to switch to. That hides the feature from precisely the
operator who has not made a second radio yet — the click lands on a
settings dialog they did not ask for, which is what happened on the first
try. With one radio the menu now shows that radio and 'Add a radio…'.

Right-click still goes straight to the CAT settings.
2026-08-27 00:32:00 +02:00
rouggy 4689505fb9 feat(cat): several radios, switched from the status bar
The answer to 'I have two rigs' was 'make two profiles', which is a heavy
instrument for a light question: a profile carries the whole station —
logbook, callsign, awards, cluster, macros — so switching one to change
which radio is connected takes all of that with it, including a change of
logbook when the profiles point at different databases.

Radios are a list now, the way amplifiers already are. The CAT chip in the
status bar opens it, one click connects the chosen rig, and nothing else
about the station moves.

The storage keeps the ACTIVE radio in the same settings keys everything
already reads (cat.backend, cat.icom_port, …), and switching writes the
chosen entry into them and reloads the link. So the consoles, the CAT
sharing, the band-follow and every panel see exactly what they saw before
and needed no change at all — the list is a second store beside the
configuration, not a replacement for it.

Details that matter more than they look:
  - An operator who has run one rig for a year opens the list and finds it
    there, because a missing list reads as the CURRENT settings rather
    than as nothing. It is written on the first save, not on the first
    read.
  - Editing the CAT panel updates the active entry, or an edit made before
    switching away would be lost on the way back.
  - A new radio starts as a COPY of the current one: a second rig is
    usually the same shape with one port changed, and an empty form is a
    form to fill in twice.
  - Switching saves the panel first, so edits to the radio being left
    behind are kept.
  - The chip only becomes a menu with more than one radio; with one it
    opens the CAT settings exactly as it always did.
2026-08-27 00:22:19 +02:00
rouggy 2fd9845b37 chore: release v0.26.18 2026-08-26 22:17:07 +02:00
rouggy e851bd97e0 chore(changelog): fold the console's fixes into its own entry
It has not shipped yet, so an operator reading about the SunSDR console
should meet what it does — not a list of repairs to a thing they have
never run.
2026-08-26 22:10:01 +02:00
rouggy b487ca1145 merge: the SunSDR console, working this time
Everything here came from one operator with the radio in front of him, and
most of it was mine to fix. The drive commands need the transceiver index
('drive:0,15;', not 'drive:15;') or the radio ignores them without a word.
The console now holds a click the radio does not echo, so a working button
stops looking dead — and lets go the moment the radio contradicts it, so a
REFUSED setting still tells the truth. Filters per mode, APF only in CW,
levels on wide rows with typed values, the RIT control shared with the
Icom console, the transmit meters, and a TUNE that can be switched off
again.

Two findings worth keeping: this radio re-asserts sql_enable eighty
milliseconds after being told to turn the squelch off, and it never
answers TX_POWER or TX_SWR — both are its own doing, and both are visible
in the log rather than argued about.
2026-08-26 22:07:24 +02:00
rouggy 4a28b51ade fix(awards): stale statistics, and the Challenge said out loud
Two things from one screenshot, and the second is arithmetic rather than a
bug.

Editing an award refreshed the grid and not the statistics matrix, so
changing which confirmations count left the table showing the old rule's
numbers with nothing to mark them stale. That is how a setting comes to
look as though it does nothing.

And OpsLog's 3229 against LoTW's 3004: the Challenge counts confirmed
band-slots on ten bands, and 60 m is not one of them. 3229 less the 224 in
the 60 m column is 3005. So the number was right and the QUESTION was
different — the panel now answers the Challenge one too, under the table
where the comparison is made.

The two totals also say what they are on hover. 'Total' and 'Général' side
by side tell nobody which counts entities and which counts band-slots, and
3309 read as entities is an impossible number — which is exactly how it
was read.
2026-08-26 21:45:11 +02:00
rouggy 980c54a616 fix(tci): TUNE can be switched off again, and the meters watch it
Two faults with one cause: this radio does not echo 'tune:0,true'.

So the panel never knew a tune was running. The button stayed on TUNE and
every further press sent another START — there was no way to stop it from
here at all. The state is recorded when the command is sent now; whatever
the radio says afterwards still wins, it simply never says anything.

And the transmit meters were asked for only while t.tx, which a tune
carrier does not set: the radio reports tuning as its own state, not as a
transmission. So power and SWR sat at zero for the whole tune — the exact
carrier an operator holds a tune for in order to watch an SWR on. They now
follow PTT or TUNE, and the S-meter reads '—' under our own carrier
either way.
2026-08-26 21:25:06 +02:00
rouggy 1201b44908 feat(tci): the transmit meters, and no temperature invented
TX_POWER and TX_SWR are read-only commands the radio answers when asked,
so they are asked for on each poll of a KEYED radio and not at all
otherwise: a receiving station pays nothing for meters nobody is watching.
Both appear next to the S-meter only while transmitting, because showing
them the rest of the time would show the last thing that happened as if it
were now.

An SWR of 0 draws as '—' rather than as 1.0. A perfect match on an antenna
nobody has measured is the one reading an operator should never be handed.

There is no temperature. The protocol's command list has TX_POWER and
TX_SWR and nothing thermal at all — so rather than leave the question
hanging, it is written down where the next person will look for it. A
temperature invented from something else, on a transmitter, is exactly the
kind of number somebody would trust.
2026-08-26 21:14:58 +02:00
rouggy 01d0b8f22b fix(tci): the squelch is the radio's refusal, not our bug — and the panel now shows it
The log settles it, and it was not mute. Mute works: '→ mute:0,true;' and
the radio answers 'mute:0,true'. What happens is that the squelch REFUSES
to go off — we send 'sql_enable:0,false;', the radio echoes false, and
eighty milliseconds later announces 'sql_enable:0,true' again. Six times
in the log, always the same shape. The SQL lamp coming back on right after
a MUTE click is that revert landing, not the mute doing it.

So the optimistic hold is fixed rather than the phantom. It was a fixed
1.2 s, which is wrong in both directions: too short for a setting the
radio never echoes at all (AGC), and too long for one it refuses, where it
showed the operator a lie for a second. The requested value now stands
while the radio says nothing about that setting, and the instant the radio
reports any change for it, its word replaces ours. A refusal is therefore
visible immediately, and an unechoed setting still sticks.

Also from the same session: filters per MODE — the narrow end in CW, the
voice widths in SSB, a middle set for the digital modes — because 250 Hz
in SSB and 2.8 kHz in CW are buttons nobody presses. And APF is shown only
in CW: it rings a single tone out of the noise, which is a CW tool and
nothing else.
2026-08-26 21:04:00 +02:00
rouggy dad71e9e4f fix(tci): a filter button does what it says, and mute is put under a log
250 now means 0-250. It meant 575-825: the width was right and it was
centred on the CW note, on the reasoning that a CW filter should contain
the note. That reasoning may be right for a radio and it is still wrong
here, because it is not what the button says — and a button that does not
do what it says is worse than one that does something simple. The two
edges are editable underneath for anything else, which is what TCI takes
anyway.

And MUTE still lights the squelch on a real radio. Nothing in this code
can do that — the button sends mute and only mute, and the two are
separate state — so the radio's own announcements are logged as they
arrive. What it says after the command will settle whether this is our
reading or its doing; no more reasoning from here will.
2026-08-26 20:59:31 +02:00
rouggy fbe01bf19d fix(tci): wide level rows, honest filter labels, and mute read both ways
Three things reported from the radio, all of them mine.

THE LEVELS. Two half-width sliders side by side left each of them a couple
of centimetres long, which is not enough to set 15% with. One level per
row now, full width, with the value typed in or stepped with ± — and in
the radio's own units, so the volume is dB and the squelch a dBm
threshold, matching what ExpertSDR3's own window shows.

THE FILTERS. The button said 250 and the radio was set to 300-550: 250 Hz
wide, but sitting where no CW note is. The edges are computed from the
width now, and a narrow filter is CENTRED ON THE CW NOTE — a 250 Hz filter
from 100 to 350 would put the note outside its own passband. A button also
lights on the WIDTH the radio reports rather than on an exact pair of
edges, so moving one edge on the radio no longer darkens every button.

MUTE. Read from one shape only, while this radio reports the other
('mute:0,false'), so the button showed the opposite of the truth. Both are
accepted now.

None of this should have reached main before somebody had a radio in front
of it.
2026-08-26 20:54:28 +02:00
rouggy fc79be7c05 fix(tci): the drive commands need the TRX index, and the console holds its own clicks
Two faults, reported from a real SunSDR.

DRIVE AND TUNE DRIVE DID NOTHING, and neither did MUTE. Those commands
carry the transceiver index — 'drive:0,15;', not 'drive:15;' — and sent
without it the radio ignores them silently: no error, no answer, the power
unchanged. The rule was in the radio's own reports all along, which is
where it should have been read from: it announces 'drive:0,85' and
'mute:0,false' at connect, while 'mic_level:100' and 'volume:-12' come
with no index at all. Sending the shape the radio speaks in is the whole
rule, and it is now written down next to the two exceptions.

AGC LOOKED STUCK ON SLOW. The panel showed only what the radio reported
back, on the principle that the radio is the truth — but ExpertSDR3 does
not echo every setting it accepts, so a working button sat unlit. Changes
are shown at once and held for a moment now; whatever the radio announces
afterwards still wins, so a clamped or refused setting stays honest
without every working one looking broken.

Also from the same report, and fair: the consoles did not resemble each
other. The Icom panel's RIT control is now a shared component both use —
chip, signed offset, ± keys, wheel, and TYPING a value straight in, which
is the thing a row of ±10/±100 buttons cannot do. Ctrl+←/→ shifts the RIT
here as it does there. And LONG is gone from the AGC row: the protocol
takes it, but it is a hang time nobody reaches for between overs.
2026-08-26 19:15:48 +02:00
rouggy afde919e5b fix(audio): the radio is offered only where it can actually be
Adding it to the device lists put it in all four dropdowns, and an
operator picked it as their RECORDING MIC — which would have recorded the
station they were listening to instead of their own voice, on every voice
message and every QSO recording. It is not a microphone, and it is not a
pair of speakers either: the audio going the other way is transmit audio.
So it is listed for 'From radio' and 'To radio', and taken out of the mic
and the listening dropdowns.

And the RX monitor is disabled when the radio IS the source, with a line
saying why: the TCI stream is pushed into the QSO recorder, and nothing
plays it to the speakers. The button was there, enabled, doing nothing
audible — which is worse than a button that says it cannot.
2026-08-26 19:04:02 +02:00
rouggy 98c49f6bbc feat(tci): a control console for the SunSDR
TCI already carries the frequency, the mode, the meters and now the audio.
It also carries everything else about the radio — and OpsLog was logging
most of it once as '(unhandled once)' and throwing it away. The console is
mostly a place to put what was already arriving.

That makes it the cheapest panel here, and it is worth saying why. A K3
console costs a command and a reply for every value it shows, which is why
it reads its settings in a rotation and its meters only while on screen.
TCI PUSHES: the radio announces its drive, its filters, its noise blanker
and the rest on connect, and again whenever any of them changes —
including when the operator changes them in ExpertSDR3's own window, which
this panel therefore follows without asking anything.

What it drives: drive and tune drive, mic gain, TUNE, volume, mute,
squelch and its threshold, NB, NR, ANF, APF, AGC speed, the passband, RIT
and XIT with their offsets, and the VFO lock. The S-meter is a real dBm
reading, so its S units are arithmetic rather than the calibration guess a
K3's meter needs.

Setters never update the cached state. The radio answers with the new
value, and taking its word is what keeps the panel honest when a setting
is refused, clamped, or changed at the radio a second later — the one
exception being a slider mid-drag, held for 900 ms so it is not dragged
back by its own echo.

Capped width and centred, like the other consoles. Also offered as a
docked pane — and the Elecraft console is offered there too now: App has
always had that pane, Settings simply never listed it.
2026-08-26 18:31:44 +02:00
rouggy 8b9c835ca1 fix(antenna): the COM port picks from the list again
Reported on an Ultrabeam over USB: the port could not be chosen from the
dropdown, and the panel would not scroll.

That field was a Combobox — a list you can also type into — chosen so the
port of an adapter currently UNPLUGGED could still be configured. It is
now the same Select every other serial device in OpsLog uses, the one that
demonstrably works. The case it was built for is kept differently: a
configured port that is no longer detected is added to the list, so it
stays selected and visible instead of vanishing when the adapter is out.

Also hardened the Test button's guard, which called .trim() straight on
the port and the host. A settings object without them — an older build, a
save that landed short — throws there DURING RENDER, and a panel that
throws while rendering is exactly a panel with a dead dropdown that will
not scroll. Whether or not that is what this operator hit, it is a way to
produce both symptoms at once and it costs nothing to remove.
2026-08-26 18:21:41 +02:00
rouggy 37b436f543 fix(maps): Carto now stamps its key-free tiles, so Light and Topo move to Esri
Reported with a screenshot: the map came up with API KEY REQUIRED written
twice across the middle of it, on the Light and Voyager views only. Those
are exactly the two Carto basemaps.

Nothing here could have caught it. The tiles still answer 200 with a real
image — the watermark is IN the image — so the map looks like it is
working and simply says otherwise in large grey letters.

Both views move to Esri, which is already behind Street and Satellite. That
is the point: a second key-free provider is a second chance to be cut off,
and this is now the third time a tile source has changed its terms under
us. Light is Esri's light canvas with its own label layer, and Voyager
becomes the topographic map, labelled Topo — the stored key stays
'voyager' so nobody who chose it is quietly moved somewhere else.

The canvas runs out of tiles at zoom 16 where the others go to 19, so
maxNativeZoom is set per basemap: Leaflet upscales the last real tile
instead of asking for a level that does not exist, which is the difference
between a slightly soft map and a blank grey square.
2026-08-26 18:16:45 +02:00
rouggy eff412a9a9 chore(changelog): the Elecraft amplifiers, in 0.26.18 2026-08-26 18:11:09 +02:00
rouggy 3013a2ba08 feat(kpa): the band-follow is asked for, not assumed
Telling somebody's amplifier where to be is a write to their station, and
it now has a switch — the SAME switch an Acom uses. 'Keep the amplifier on
the radio's band' is one idea to an operator; that it needs a second
serial port and a rig emulator on one amplifier and a single ^BN on
another is our business, not a reason for two checkboxes.

On by default for a KPA, because an amplifier left on the wrong band is
the worse outcome of the two, and the operator who has wired it straight
to the radio turns it off once. The hint says exactly that case, since it
is the only one where a second voice telling the amplifier where to be is
unwelcome.
2026-08-26 18:07:35 +02:00
rouggy de95a437bb fix(kpa): its own card, and no band-follow it does not need
Two things the wiring got wrong, both visible on screen.

A KPA fell through to the PowerGenius branch of the amplifier card and was
drawn as one — titled 'POWERGENIUSXL · ELECRAFT KPA1500', with a
PowerGenius's meters and none of its own. It has a card now: OPERATE, ON
and OFF, band, power, SWR, temperature, supply, and the fault across the
end. The OPERATE button carries a tooltip saying it also clears the fault,
because that is the button an operator already has under the cursor when
they need it.

And the band-follow option was offered on it. That option exists because
an Acom POLLS a transceiver and has no command to be given a band, so
following it needs a second serial port and a rig emulator answering those
polls. The KPA has ^BN — OpsLog simply tells it, on the link it is already
using, and only when the band CHANGES. The option is gone from the KPA and
the telling is automatic.

Also: Acom rather than ACOM everywhere it is read, which is how the
company writes it. Identifiers, package names and log lines are left
alone — renaming those is churn with no reader.
2026-08-26 18:00:46 +02:00
rouggy 85872f8379 feat(kpa): the Elecraft amplifiers in the Settings, the card and the widget
Wired into the multi-amplifier support that already carries the SPE, the
ACOM and the PowerGenius: a fourth brand rather than a fourth mechanism.
The linking, the fan-out and the poll all work on it unchanged.

Three things the KPA does differently, and each is handled where it shows
rather than explained in a hint:

  - A KPA500 has no network side at all. Choosing it puts the entry on
    serial, and a configuration still asking for TCP is refused with a
    line saying so instead of retrying an address that cannot answer.
  - The amplifier answers ^ON while its main supplies are OFF — a sleeping
    microcontroller stays awake for exactly that — so power-on stays
    available over the network, unlike the SPE and ACOM which need their
    serial control lines.
  - Going to OPERATE clears the current fault, everything except
    temperature. The widget says so under the fault, because the button
    that fixes it is the one already on screen.

Defaults that leave a working configuration when the model is changed:
38400 baud, TCP port 1500, and a full-scale power mark of 500 or 1500 W
by model — a KPA500 read against a 1500 W scale looks idle at full
output.
2026-08-26 17:50:30 +02:00
rouggy 37e9e288ba fix(awards): band columns follow the contacts, not the permission
The columns took the award's declared band list when it had one, so every
band the award PERMITS got a column whether or not anything had ever been
worked on it. DDFM permits 6m and 70cm; a station that has never worked a
French department on either was shown two empty columns, which reads as a
gap in the log rather than as a band nobody tried.

They now follow the bands this operator actually has contacts on for the
award. The declared list is the fallback for an award with nothing worked
yet — there, saying what the award is played on is the only honest answer
available — and the classic HF set is the fallback for an award that
declares nothing either.

Together with taking the order from the full band list, this is what puts
23cm on DDFM: the column appears because there are contacts on it.
2026-08-26 11:57:02 +02:00
rouggy 2b8ebd73cb fix(awards): the band matrix stops pretending 70cm is the top band
The columns were the intersection of the award's bands with a fixed list
that ended at 70cm, so a band the award genuinely covers could not appear
whatever it said. DDFM is worked well into the microwaves and had no 23cm
column, and nothing anywhere would have produced one.

The intersection is now taken against every band in frequency order, the
same list internal/award already sorts its own band counts by. The classic
HF-plus-2m-and-70cm set stays as the fallback for an award with no band
list and no contacts yet — the full list would be twenty-nine columns of
mostly nothing.
2026-08-26 11:51:15 +02:00
rouggy f9f8380ece chore: release v0.26.17 2026-08-26 11:44:40 +02:00
rouggy 2e3464104d fix(awards): the RDA arbitration writes CNTY, and settled rows leave the list
Two faults, and the first was a design mistake of mine.

The choice was written only as the award override, to keep the imported
CNTY as a record of what HAMLOG said. But CNTY is what the comparison
READS, so the contact went on disagreeing for ever: settle a hundred
rows, run the comparison again, get the same hundred back. The reasoning
was about preserving evidence; the effect was a button with no visible
consequence anywhere, which is indistinguishable from one that does
nothing. This is the operator's own log, and correcting a field in it is
the point of the exercise. The district now lands in CNTY as well as in
the award reference.

And the settled rows stayed on screen, which made the same button look
broken a second time. They are dropped as they are applied — locally,
rather than by re-running the comparison, which reads the whole log and
would be seconds of silence on a remote database to be told what is
already known. The agree/disagree counters follow.
2026-08-26 11:08:15 +02:00
rouggy fd939797dd feat(awards): settle each RDA disagreement on its own row
The comparison could show two hundred contacts where the log and the
database name different districts, and leave the operator with nothing to
do about any of them but open each QSO and edit it by hand.

Now each row carries the two values as buttons: click the district you
keep, then apply. There are shortcuts for keeping one source everywhere,
and applying is a separate act — clicking through two hundred rows with
each one written as it is clicked would make a slip permanent before the
reading was finished.

The choice is written as the contact's AWARD REFERENCE, not over the
imported CNTY, and that distinction is the point. CNTY is what the
downloaded log said; overwriting it would destroy the evidence the
comparison runs on, and the next comparison would agree with itself and
prove nothing. The override is the operator's decision, it is what the
award engine counts, and it beats both sources. So the log keeps saying
what it said, the database keeps saying what it says, and the contact
counts for the district that was chosen.

The buttons show the districts themselves rather than a tick box: someone
arbitrating between RO-19 and KO-05 should be picking a value they can
read, not remembering which source a checked box stood for.
2026-08-26 10:22:23 +02:00
rouggy a621a3ee62 chore: release v0.26.16 2026-08-26 00:25:37 +02:00
rouggy 5388f76733 chore: release v0.26.15 2026-08-26 00:14:16 +02:00
rouggy ceb88d2e29 refactor(tci): remove the test bench now that choosing the device is the setup
The TCI section of the audio settings was an investigation: open the
stream, read what arrives, record ten seconds to listen to, key a tone.
It answered every question it was built for — the frame layout, the
sample width, that the radio asks rather than follows a clock, that the
transmit audio source decides — and confirmed 80 W on real hardware.

None of that belongs in front of an operator now. The radio is simply one
of the devices in the two dropdowns, and choosing it IS the configuration:
one control, in the place where the question is already being asked.
Keeping the tick box beside it would have been two switches for one
decision, with the second one where nobody looks.

Gone with it: the stream/record/probe bindings, the audio.tci_rx setting,
and twenty-four translation keys. The plumbing they proved out stays and
now carries the voice keyer.
2026-08-26 00:13:04 +02:00
rouggy ec3e60e47b chore(changelog): open 0.26.15 with the TCI audio work
The RDA and Elecraft SWR entries move with it: both were written after
v0.26.14 was tagged, so they ship in the version that carries them rather
than in the one already on operators' machines.
2026-08-26 00:09:32 +02:00
rouggy 5c6df90526 merge: TCI audio — receive, transmit, and the voice keyer over the CAT link
A SunSDR carries its audio on the same WebSocket as its commands, so
OpsLog can take it directly: no virtual cable, no second sound card, no
Windows mixer between the recording and the air. The radio appears as a
device in both audio lists and can be chosen for either direction.

Everything here was settled on real hardware over one evening, and none of
it was guessable from the documentation:

  - The receive stream answers format=3 for four-byte floats, so the
    sample width is derived from the frame rather than trusted from the
    field.
  - The radio asks for transmit audio only when the transmission is the
    CLIENT'S, and only when its transmit audio source is TCI rather than
    the microphone.
  - The chrono is a REQUEST, not a clock: no payload, carrying the size it
    wants, 47 times a second. Audio goes out in answer to it and never on
    a timer of our own — a timer was the first attempt and all 234 frames
    of it were ignored.

Confirmed: a clean test recording, and 80 W out of a 1 kHz tone.
2026-08-26 00:05:40 +02:00
rouggy 848ce68ec5 feat(tci): key the radio ourselves and push a tone, to see what it wants
A first real transmission settled one question and raised a better one.
With the receive stream open and six seconds of transmit, the radio sent
282 frames of receive audio and NOTHING else: no chrono, no transmit
audio. So the chrono the documentation describes is not offered to a
client that merely happens to be connected while the operator keys the
microphone, and waiting for it to appear is waiting for nothing.

The reading that fits is that the radio asks for audio when the
transmission is the CLIENT'S and takes the microphone when it is the
operator's — which makes the experiment obvious. Key it from here, push a
1 kHz tone, and watch. Chrono frames appearing gives their size and
cadence by measurement instead of by guesswork; no chrono but a tone on
the meter is just as useful, because then the pacing is optional and the
voice keyer can push frames at the rate the stream already runs at.

A tone rather than silence so the answer shows on the power meter and not
only in the log.

It transmits, so: an explicit button inside a warning box, five seconds,
capped at ten, and every path out unkeys — including the panic that has
not happened yet and a socket that dies mid-tone. A transmitter left keyed
by a defect is the one fault here that would reach somebody else's band.

Writes are now serialised too. send() held the lock only long enough to
read the connection, which was enough while every command came from the
poll loop; a stream of audio frames from a second goroutine is not, and
gorilla panics on a concurrent write rather than failing quietly.
2026-08-25 23:18:11 +02:00
rouggy 4441b16699 fix(awards): the RDA comparison list no longer jumps back to the top
Two faults, one behind the other.

RDAPanel is nested inside SettingsModal so it can read its state, and it
was written as <RDAPanel />. A nested function is a new component TYPE on
every render: React cannot know it is the same panel, so it unmounted the
tree and mounted a fresh one — and a fresh scroll container starts at the
top. It is called now, like the other panels, which produces the same
elements in place and never disturbs the scroll position.

What it was reacting to should not have reached it either. The PSK
Reporter and grid-cache poll ran every three seconds from wherever you
were in Preferences, re-rendering the whole dialog for a count shown only
on the cluster section. It now runs while that section is open, and the
one-time loads it was sharing an effect with have stayed where they were
rather than refetching on every click in the sidebar.

The visible result: a hundred contacts to correct can be read from top to
bottom.
2026-08-25 22:32:56 +02:00
rouggy 5fd3b5b688 chore: release v0.26.14 2026-08-25 22:23:02 +02:00
rouggy f50bbc005c feat(tci): the QSO recorder can take its audio from the radio
Confirmed on a real SunSDR: the stream decodes and the test recording
plays back clean. So it can do the job a virtual audio cable was doing —
this wires it to the QSO recorder, which already accepts a pushed source
(the Icom network audio uses the same door).

The conversion lives here rather than in internal/cat: the radio's job is
to hand over what it sent, not to know that the recorder works in 16 kHz
mono. Three samples are AVERAGED rather than two of them dropped —
decimating by picking every third folds everything above 8 kHz back into
the voice band, and on a receiver that is hiss, which a QSO recording has
plenty of already.

Off by default, and applied the moment it is switched: it replaces a
sound card the operator has already wired up, and an option that needs a
restart to take effect reads as an option that does not work.
2026-08-25 20:14:45 +02:00
rouggy e8dfa0eaaf fix(ui): stop Preferences redrawing with the main window; unclip the dropdowns
TWO FAULTS, ONE SYMPTOM — 'it refreshes ten times a second and the
buttons cannot be pressed'.

Preferences is a child of the main view, so every cluster spot, CAT push
and decode re-rendered the entire panel. On a busy evening that is
several times a second, and the panel is large enough that the rebuild
outlasts the gap between them: buttons missed their clicks because the
element under the pointer was replaced between the press and the release.
It is memoised now, and the callbacks App hands it hold their identity —
without that the memo compares unequal every time and buys nothing.

The dropdown menu was an absolutely-positioned child, so it was clipped
by whichever scrolling or overflow-hidden box it sat in: the satellite
list showed one entry of eight. It is portalled to the body now,
positioned from the field's rectangle, re-measured while open, and opens
upward when the field is near the bottom of the screen — which is exactly
where these fields tend to be.
2026-08-25 20:04:07 +02:00
rouggy b53c56d508 fix(sat): give the satellite field its chevron
It has been a combobox since it was written, but without showToggle it
opens only on a keystroke or ArrowDown — both of which have to be known
about first. On screen it is a text box, which is exactly what 'still no
dropdown' meant.

allowFreeText with it: the list is the station's own, so a bird worked
once and never added must still be loggable.
2026-08-25 19:56:19 +02:00
rouggy 67c85be23c fix(sat,rda): the dropdown reads the live list, and the RDA buttons answer
SATELLITES: the field read the list once, when the details panel first
mounted — which is at startup. A list saved in Preferences afterwards
therefore did nothing until a restart. It comes from App now, which
already reloads the lists when Preferences close.

RDA COMPARE: a failed comparison wrote its error into the message beside
the FILL DISTRICTS button, a row above — the error appeared under a
button nobody had pressed while the one that had been pressed showed
nothing, which reads as 'the button does nothing'. It has its own message
now, and reports the two results that look like silence: no disagreement
at all, and no Russian contacts to compare. Both buttons say what they
are doing while they do it, and the comparison logs before it starts
reading — on a remote MySQL that read is seconds of quiet.
2026-08-25 19:53:30 +02:00
rouggy e41a3a4c1c fix(rda): the comparison list froze the settings pane instead of scrolling
overscroll-contain on the conflict box stops the wheel from chaining to
the pane behind it. With a short list — nothing to scroll inside the box
— the pointer over the table therefore froze the whole panel: the page
would not move and neither would the list.

The box scrolls when it has something to scroll; the page scrolls the
rest of the time, which is what containment was preventing.
2026-08-25 19:45:39 +02:00
rouggy e4276ff66e fix(lists): the satellite list was never sent to be saved
Save builds the lists payload field by field, and the new list was not
among them — so every Save wrote an empty satellites array over whatever
had just been typed.

Normalised in the save as well as on the field's blur: the Save button is
reachable without ever leaving the box, and a list whose survival depends
on where the cursor went first is a list that saves sometimes.
2026-08-25 19:43:16 +02:00
rouggy 1e87081058 feat(lists): a satellite list, offered on the entry form
SAT_NAME is compared character for character by the awards and by LoTW:
AO-91 and AO91 are two different satellites to everything downstream, and
typing it afresh on every pass is how one of them ends up in a log. So
the station keeps its own list (Preferences → Lists → Satellites) and the
field offers it, alphabetically — while still accepting anything typed,
because a bird worked once and never added to the list must not be
impossible to log.

Not seeded with a shipped list of two dozen birds: an empty list means
this station does not work satellites, and filling the dropdown with
names nobody here has heard makes the field harder to use, not easier.

Also fixes the RDA district comparison: its conflict list was capped at
about six visible rows of a list holding up to two hundred, in a panel
that would not scroll to show the rest, and nothing in it could be acted
on. Taller, scrolling, and a callsign opens the contact.
2026-08-25 19:30:29 +02:00
rouggy 12b0a861e0 style(cat): connection as a dropdown, and only where there is a choice
Three wide buttons for something most radios cannot vary was noise, and
they pushed the fields below into whichever grid column came next — the
panel looked shuffled.

Now: a dropdown, shown only when the brand has more than one way in. A
Yaesu is reached over USB and that is the end of it; a control offering
one entry that cannot be changed is a decision that is not one.

Brands: OmniRig first — it is the one that works with any radio, so it is
where someone who cannot find their rig should land — then alphabetical.
Kenwood and Elecraft drop the proprietary-network entry: neither is
implemented and neither is planned, and naming a road that goes nowhere
is only useful when someone might reasonably look for it.

The standalone checkboxes (protocol log, DTR/RTS) take a full row instead
of half of one, which is what was breaking the alignment.
2026-08-25 17:57:53 +02:00
rouggy 712d83a012 feat(cat): pick the radio, then how it is connected
The backend list mixed two different questions. 'Icom (USB)' and 'Icom
(network)' were separate entries; Kenwood and Elecraft hid the same
choice in a field further down; and Flex, TCI and OmniRig sat in the same
list as if they were the same kind of answer. An operator picks a radio
and then says how it is plugged in, so that is what the panel asks, in
that order, and the two answers together choose the backend.

Each brand offers only the connections it has, and a brand with one way
in still shows it, greyed: 'there is no choice here' is an answer and an
empty space is not. The stored backend names are unchanged — 'icom-net'
is still 'icom-net' — so a settings file written by an older build keeps
working, and brand+connection are DERIVED from it rather than held
alongside it, which is what keeps them from drifting apart when something
else writes the backend.
2026-08-25 17:49:07 +02:00
rouggy bd540ef18b feat(cat): three transports for Kenwood/Elecraft, chosen not inferred
USB, RS-232-to-Ethernet, and the radio's own network protocol are three
different things, so they are three entries in one dropdown — and the
transport is now a stored setting rather than something deduced from
which field happened to be filled in. The old rule (a host wins whenever
it is not empty) is invisible from the settings page: someone who typed a
host months ago and later set a COM port had a radio that never answered
and nothing on screen to say why.

The third entry is named and refused, with the reason. It is a session
with its own framing and login, not the CAT byte stream over a socket, so
it has to be written per radio and against one — and a K4 owner reading
this list should be told that, not left wondering whether the empty
address field was the problem.

The upgrade reads an older install from what it has: a configured host
means the bridge, which is what the previous code used it for. Pinned by
a test, including the case that matters after the fact — choosing USB
with a stale host still stored gives the COM port.
2026-08-25 17:46:14 +02:00
rouggy 2f2fced15c fix(cat): make the Kenwood/Elecraft link an explicit USB-or-network choice
The network option IS implemented — the same ASCII CAT over TCP instead
of a cable, for a ser2net bridge, an Ethernet-serial adapter, or a radio
exposing its raw CAT port. What was wrong is how it was offered: the COM
port and the network address sat side by side with nothing to say that
the address wins whenever it is not empty.

Worse, the example address read 192.168.1.50:4532. That is Hamlib
rigctld's port — a different protocol, and the one OpsLog SERVES under
'Share CAT'. Anyone copying the example was pointing the radio link at
OpsLog's own server.

Now one choice, then the fields that belong to it. And when the far end
answers something that is not the radio's CAT, the error says that rather
than 'check the baud rate', which over TCP is advice about a setting that
cannot be the cause.
2026-08-25 17:27:44 +02:00
rouggy 97ae05d688 fix(awards): the entry-form picker offers only the awards this station follows
F3 listed every award that existed, enabled or not, followed or not — so
a station chasing three of them assigned references from a list of twenty
and had to know which of the twenty mattered.

It now applies the same two rules the Awards tab already uses: an award
switched off in the editor is not a candidate for anything, and when a
selection of followed awards exists, only those are offered. An empty
selection still means all of them, so nobody loses a picker by never
having chosen.
2026-08-25 17:07:30 +02:00
rouggy df1d2767cd feat(antgenius): the selected antenna can fill MY_ANTENNA
A station with a switch knows something the log does not: which antenna
is actually connected. The working conditions hold what was PLANNED for
the band, and stay right until the operator throws the switch — after
which every QSO keeps claiming the other antenna.

Optional, and off by default: a station that names its antennas
differently in the two places would otherwise find its log quietly
rewritten. The name written is the one configured on the device, since
that is the name the operator gave it and the one they will look for.

WHICH PORT is the real problem, and the reason this is not a one-liner.
The switch has two, the radio has two jacks, and the QSO went out through
one of them; naming the wrong port's antenna is worse than naming the
band default, because it looks authoritative. The radio's own TX antenna
selection decides, and the jack-to-port wiring is a setting — it is the
station's cabling and neither device can report it.

When the port cannot be told — a transverter jack, a rig that reports
nothing, both switch ports live — nothing is written and the band default
stands. Silence beats a confident guess in a field nobody re-checks.
2026-08-25 15:08:48 +02:00
rouggy 01a23ccb77 feat(tci): read the radio's declared format, and record a test WAV
The SunSDR announces its own stream at connect —
audio_stream_sample_type:float32 and audio_stream_channels:2 — and both
were being logged as unhandled while the code worked the format out from
frame arithmetic. The declaration is better evidence and arrives before
the first frame; the arithmetic stays as the check on it. The channel
count now drives the mix-down instead of an assumed stereo.

Adds a ten-second test recording, written as a WAV beside the QSO
recordings. Counting frames proves a socket is delivering bytes; it says
nothing about whether those bytes are the receiver's audio, at the right
rate, in the right order. A stream decoded with the width wrong or the
samples misaligned counts exactly as well as a correct one and sounds
like a fan — so the test is a file the operator can play, the same way
the CW decoder was settled on the air rather than on a spectrogram.

The file is written at the rate the RADIO reported, not a constant: a
recording at the wrong rate plays at the wrong speed, which is the one
fault that would be blamed on the decoding.
2026-08-25 08:25:29 +02:00
rouggy b2b93e2839 feat(cluster): hold the list still while it is being read, and stop truncating filters
Two things reported together, both about a list that fights back.

The GRID froze nothing: every spot landed at the top and pushed the rest
down, so a few rows in, the callsign under the pointer had moved by the
time the click arrived. It now stops redrawing as soon as it is scrolled
away from the top and says how many spots are waiting; reaching the top
again, or clicking the notice, releases it. The arrivals are counted by
finding the frozen top row in the live list rather than by comparing
lengths — the list is a ring buffer, so once it is full a length
comparison would report nothing new for the rest of the evening.

The COMMAND BUTTONS were capped at 120 characters. A DXSpider filter
naming the prefixes an operator wants runs well past that, and the field
just stopped accepting keystrokes: the command was saved truncated with
nothing to say why. 500 now, with the full text in the tooltip since the
box cannot show it.
2026-08-24 22:19:54 +02:00