The Main tab was a fixed 50/50 grid, but a map and a cluster list want
very different widths and which one deserves the room changes with what
the operator is doing.
The share is clamped to 15..85: a pane can be made small but never
dragged out of existence, because recovering from that would mean
grabbing a divider no longer on screen. Double-click restores even.
The drag uses pointer capture on the divider — without it Leaflet
swallows the moves as soon as the cursor crosses the map. Persisted
through writeUiPref, so it travels with data/ like the other UI prefs.
The cluster already answers this question for a spot; the entry panel did
not answer it for the station actually being worked. A county hunter
needs to know before the QSO ends — by the time the county shows up in
the awards table the station is long gone.
qso.CountyWorked asks about one county instead of loading the whole
worked set, narrowed to the state so it stays a few dozen rows on a
remote MySQL. It compares through award.USCountyKey on both sides rather
than in SQL, because logged spellings vary ("Los Angeles" against
"LOS ANGELES, CA") and that key function is what resolves it everywhere
else.
An empty or non-US county is not "new" but unknown, and shows nothing: a
badge on a guess is worse than no badge.
Radix gives the content element focus when a tab is selected, so the
focus-visible ring fired on an ordinary click and outlined the whole
lower pane — visible as an orange rule under the tab strip. A tab panel
is a container; the controls inside it carry their own focus styling.
At two grid columns the labels wrapped onto a second line, which set the
row height for everything beside them. The unit is obvious from the
neighbouring DXCC # field and the numbers themselves.
Six equal columns gave a US county name the same width as a two-digit CQ
zone: the county truncated while the zones, the prefix and the DXCC
number sat mostly empty. The row is twelve columns now, spans set per
field.
The bearing and the two distances are read-only and never exceed
"12345 km", so they move to a flex row at a fixed width and the address —
the one field here that was never wide enough — takes the remainder.
The award editor's Test tab matched DK2FJ on its OR rule "qth / exact"
and reported the two Aachen DOKs as ambiguous. The F3 panel, on the same
contact, showed nothing — it builds its own QSO for ComputeQSOAwardRefs
and that object carried address, state, county, zones and continent only.
Any award keyed on the town could not fire there, so live detection was
silently blind to a whole class of awards and the operator only saw them
after logging.
It now passes the entry-strip text as well: QTH, name, country, comment,
note and grid.
The refusal to guess was only visible in the award editor's test tab and
in the awards table — both of them places an operator goes long after the
contact is over. It has to appear while the station is still in front of
you, so ComputeQSOAwardRefs now reports the candidates it declined to
assign and the F3 panel lists them as buttons: click one and it is
written to award_refs, replacing any sibling picked for that award.
They are never auto-added — adding one would be exactly the guess the
setting exists to refuse.
Also: the dark-theme fix for the native calendar glyph named only
input[type=date], so date-time fields kept an invisible icon. All the
temporal input types now share the rule.
Two DARC DOKs are both called "Gießen" — two clubs share the town. Exact
matching finds both and both are right as far as the matcher can tell, so
it kept both and the operator got a QSO credited to a DOK that may well
have been the other one.
Picking one is not an option: it writes a reference into the log with no
evidence behind it. So a Def can now be marked one reference per QSO, and
an ambiguous match on such an award assigns none, names the candidates in
the trace and leaves the contact under missing references — where the
operator's choice now sticks (06e3437).
Off by default, and deliberately NOT a revival of the old `Multi` flag: a
field holding several references (an n-fer POTA activation, a VUCC grid
line) is several references, correctly, and stays that way.
Reported on the DARC DOK award, with DL2FDM: a German address matches two or
three DOKs, the operator deletes the wrong ones and assigns the right one, and
the next recompute puts the wrong ones straight back.
The manual reference was APPENDED to what the matcher found. Storage already
kept exactly one manual entry per award — setOverrideRef drops the previous one
— so it was an override everywhere except at the one place it mattered. The
operator was arguing with the matcher and could not win.
It now replaces. The trace records what was superseded, so a corrected QSO can
still be explained rather than being merely different. A QSO with no correction
keeps every automatic reference, which the test also pins: an override must not
quietly disable matching for everyone else.
This does not address the over-matching itself — an address containing "Berlin
Mitte, 10115 Berlin" genuinely matches two DOK descriptions. That is the next
question, and a separate one.
I took the callsign field's extra width from the RST pair, on the assumption
that five characters was the most they would ever hold. A signal report of
"59+30" is exactly five characters plus its padding, and it was clipped.
The RST fields are back to their original width. The callsign keeps its extra
room — the row has space for both.
Reported on a Xiegu G90. With the blue programming cable — three wires, no
modem lines — CAT works perfectly. Behind a DE-19, which carries audio, CAT and
PTT, the radio went into transmit the moment OpsLog connected and stayed there.
Windows raises DTR and RTS when a serial port is opened, and the DE-19 reads
them as PTT; Xiegu's own documentation asks for both low. The CI-V backend has
dropped them since it was written, for the same reason on Icom rigs with USB
SEND mapped to a line. Xiegu, Yaesu and Kenwood did not. They do now.
The second half of the report: WSJT-X through OpsLog's rigctld decoded fine and
never transmitted. Nothing was broken in that chain — set_ptt reaches the
backend, which sends the CI-V PTT command, which a G90 ignores. That is why
Xiegu keys on a hardware line instead. The backend can now do that: Settings →
CAT → Xiegu → how the rig is keyed (CI-V / RTS / DTR).
Untested here — no G90 in reach. The operator who reported it offered to try.
Nine entries had been appended to the 0.22.5 block after 0.22.5 was already out,
so what shipped as 0.22.6 had no block at all — and the release script said so:
"changelog.json has no 'fr' entries for 0.22.6 - skipping". No Discord
announcement, and no What's-new for anyone updating.
The split is not guesswork: the changelog at tag v0.22.5 carried exactly one
entry, so everything after it belongs to 0.22.6.
Reported by an operator whose node sends little: nothing for two minutes, then a
reconnect — losing the login, the filters, and any spot that arrived during the
gap.
The read had a 120 s deadline and ANY error ended the session, a timeout
included. That reads a silence as a verdict on the link. It is not: a cluster on
a dead band legitimately says nothing for minutes.
Only a real error ends a session now. A genuinely dead peer is still caught, and
by the mechanism meant for it — TCP keepalive probes an idle connection and its
failure surfaces as an error on Read, not as a timeout. The dialer sets it
explicitly rather than inheriting a default, since it is now what the design
depends on.
One trap the advice this came from did not mention: ReadString returns the bytes
it HAD read along with the timeout. Discarding them would lose the first half of
any spot line straddling a deadline, so the partial is carried over.
Tested: a listener that goes silent past the deadline and then sends a spot —
the spot arrives on the ORIGINAL connection, and the listener never accepts a
second one. The tick is a var so that test runs in four seconds instead of
sixty; a slow test is a test that gets skipped.
A panadapter shot settles it: answering an S5 station, the replayed recording
goes out several times wider and taller than the station being answered — in WAV
as well as MP3, so this is not the encoder.
One setting served two sources that have nothing in common. A voice-keyer
message is a microphone at speaking distance and legitimately wants 195%; a QSO
recording is a receiver's line output, which is far hotter. The same 195% put it
into the transmitter flat out, ALC pinned, noise and voice alike.
The QSO playback now has its own level, defaulting to 100%. The voice keyer
keeps the setting it had, so nobody's F-key messages change.
The operator split it in one test: WAV is fine, MP3 is not. The MP3 path is the
only one that resamples — 16 kHz up to 32 kHz, because the encoder emits broken
frames at MPEG-2 rates — and it did so by averaging neighbouring samples.
That is an upsampler in name only. Every component at f is mirrored to
16 kHz − f, and the test measures the mirror just 7 dB below the wanted signal:
a second copy of the whole spectrum. On speech it adds brightness; on receiver
noise, which is broadband, it lays a second noise floor over the top of the
band. Hence "a hiss that is not the QRM, louder than the voice" — and hence its
absence from the WAV, which resamples nothing.
Zero-stuffing plus a 63-tap windowed sinc at the old Nyquist puts the image
56.8 dB down instead of 7, at a cost measured in microseconds against the MP3
encode that follows. Two tests pin it: the image suppression, and that the level
is unchanged — a fix that quietly halved every recording would be a second
surprise on top of the one being repaired.
A station with two radios has two sets of audio endpoints, and the recorder will
happily capture the one nobody is listening to — a Flex DAX channel while the
operator works a Yaesu over USB. The file is then full of hiss with no relation
to what they hear, unaffected by the rig's AGC, and nothing anywhere said which
receiver it came from.
The device names are now logged, resolved to their friendly names, once when
capture starts.
Reported precisely: on the air the voice is well clear of the noise; in the
recording the noise is louder than the voice. Same audio, so the fault is in how
it is captured, not in what is captured.
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY was set alongside AUTOCONVERTPCM. The
name reads like a sensible default and is not: it selects the CHEAP sample-rate
converter, for cases where quality does not matter. Taking a rig's 48 kHz stream
down to our 16 kHz, it folds everything above 8 kHz back into the audio band —
and receiver hiss is mostly high frequencies. The voice keeps its level, the
noise arrives twice.
Dropped, so Windows uses its normal converter, which filters before decimating.
This affects every capture and render path in the app, the voice keyer included.
The monitor followed the slider, but two takes made at 10% and 300% came out
identical: starting a recording re-reads the STORED settings and calls SetGains
with them, so the live value was overwritten the moment a take began.
The slider now persists as it applies. Saving Settings still works as before —
this only means the stored value is already right when a take starts, which is
what every path downstream reads.
Reported again after the monitor was wired up: 300% still sounded like 10%. The
monitor fix was right but could not show, because the sliders only changed the
settings panel's own state — the value reached the audio engine when the
operator clicked Save.
A level is set by ear, by dragging. It now applies on every move, to the monitor
and to the recorder mix alike; saving still persists it. Both halves were needed
and neither works alone.
Padlocks. LockBtn was declared INSIDE the App component, so every render gave
React a new component type and the button was unmounted and rebuilt — about four
times a second while the CAT polls. It flickered under the pointer and swallowed
clicks, because the node being clicked no longer existed when the click landed.
The padlock now lives at module level and is passed its state.
From-radio level. It reached the QSO recorder and nothing else, so an operator
listening through OpsLog heard no difference between 10% and 150% — the control
was not weak, it was disconnected. It now scales the monitor too, applied on the
captured chunk so the network-fed path keeps its own levels, and a running
monitor picks up a change immediately: making someone restart the monitor to
hear the slider is how a working control gets reported as broken.
Callsign field widened to w-56, the room coming from the RST pair which never
needs more than five characters. It is the one field always typed into, it
carries the REC badges, and a long portable call has to stay readable.
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.
Reported precisely: press play again and it stops the current message, but you
then have to wait the whole length of the message before it will play — otherwise
the radio is keyed and the PTT released at once.
Play only SIGNALLED the previous playback to stop and started a new one straight
away. The old goroutine still held the WASAPI render client for a moment, so the
new client could not start; it returned immediately and the PTT tail was all that
was left. The log now shows both halves of that: a play line, then a release
120 ms later.
Stopping now waits for the goroutine to have returned — the point of stopping
before starting again — bounded at 1.5 s so a wedged device cannot freeze the
operator's click, and saying so in the log if it ever comes to that.
From an operator's log, playing a recording on the air repeatedly:
18:52:40.148 play → 18:52:48.420 PTT released 8.2 s, the whole take
18:52:57.759 play → 18:52:57.879 PTT released 0.12 s, nothing came out
Those 120 ms are the PTT release tail alone: the render device refused to start
and playPCM returned an error that was thrown away. The call reported success
every time, which is what "it plays once and then never again" looks like from
the outside — and why the search went to the recorder, which was innocent.
The error is now logged with the device name.
An operator emptied "Recording mic" and got "no audio output to the radio is
configured", and read it as a restriction: that a recording of the other station
alone cannot be played back. No such rule exists — the mic is an INPUT, one more
source to record, while this message is about the OUTPUT that feeds the
transmitter.
The message now names the setting exactly as the panel labels it, and the log
line carries the other two device values so the next report says which field was
actually empty.
Reported: the recording plays once and never again, "as if it threw the sound
away after playing it". PeekQSO is what the playback reads and it is meant to
copy — TakeQSO is the one that clears — so this pins that, and that the copy is
independent of the recorder's own buffer.
It passes, which is the useful part: the take survives, so the one-shot failure
is downstream of it and the search moves to the file and the player.
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.
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.
The operator's own fetch settles it. One record, both fields:
<app_qrzlog_status:1>N <qrzcom_qso_download_status:1>Y
QRZ says the QSO is NOT confirmed and sets the download field anyway — it marks
what was handed back, not what was confirmed. OpsLog read it as a confirmation,
so eighteen QSOs came back "UPDATED" and green, none of them confirmed on QRZ.
The site's own logbook page shows them without a star.
Only app_qrzlog_status = C counts now. The test that asserted otherwise is
corrected and carries the real record.
And the download now takes back what it wrongly gave: when QRZ explicitly says
not-confirmed and our log says confirmed, the flag is cleared and reported on
its own line. These QSOs count towards award slots, so leaving them green until
someone noticed was not an option — and nothing else in the app would ever have
undone them. Only on an explicit no: a record with no app_qrzlog_status is QRZ
saying nothing, and silence is not a retraction.
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.
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.
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.
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.
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".
Both faults reported on the air, both reproduced in synthetic signals first —
the existing tests all passed because each starts a fresh decoder and sends
textbook spacing, which is neither of the cases that hurt.
Speed change. A 32 wpm station followed by a 14 wpm reply decoded as
"TTT TTTT TTTTT TT…" for most of the over. Not the element classifier: the
CHARACTER-gap threshold is 2.2 dits, so at the stale fast estimate it stood at
81 ms while the newcomer's element gaps were 86 ms. Every single element was
flushed as its own character, and a lone element with no contrast reads as a
dah. A silence long enough to end an over now drops the estimate back to the
seed, which is exactly the state a freshly started decoder is in — and that case
was always fine. The threshold scales with the current estimate (12 dits, floor
600 ms) because a fixed one cannot serve both 10 and 40 wpm.
Callsign split. The word boundary sits at 4.6 dits, the geometric mean of a
3-dit letter gap and a 7-dit word gap. It assumes textbook spacing; a fist
leaving 5 dits between letters had every letter turned into a word, so a
callsign arrived as "O Y 1 C T". The boundary now also follows the letter gaps
this operator actually sends, whichever is larger. A wide sender's words may run
together — a far smaller price than a callsign in pieces.
The operator asked the right question: why wait 1.5 s at all, when the mode is
known from CAT?
Because the wait is not the problem — what happened during it was. The drift
guard, meant for two sound cards running on their own clocks, saw a starved
source as a one-second skew and trimmed the radio audio to match. So the opening
of every CW recording was thrown away although it had been captured.
The guard now applies only while BOTH sources are actually delivering. Waiting
then costs nothing: everything buffered is written whole the moment the silent
source is written off, which a test now pins with three seconds of audio.
Using the CAT mode to skip the wait would have been a second mechanism for a
case this one already covers — and it would not cover the others: a device
switched off, unplugged, or held by another program.
The log read:
recorder: the second audio source is silent — recording the radio alone
recorder: no audio at all from "DAX Mic" after 3 s — nothing is streaming
Both true, and together they read as a fault while the recording was going
perfectly well — it was saved seconds later. On CW the mic channel legitimately
delivers nothing; the mixer already said what it was doing about it.
The watchdog now mentions the mic only when the radio is silent too, which is
the case where the recording really is empty.
The operator said it from the start: in CW there is no sidetone, only reception.
My previous fix did not cover it — it wrote off a source that STOPPED, but a
source that never starts has no last-delivery time, so it stayed forever "not
yet dry" and held the mixer at min(A,B)=0. Three empty recordings while the
radio audio streamed perfectly.
A source with no delivery is now measured from when capture started. Tested both
ways: nothing is written off in the first moments, and a source that has never
spoken is written off once the grace period has passed.
Third empty recording, same station, and the sequence was as plain as it gets:
type the call, click record, log the QSO. The device opened and never streamed.
Endpoint ids are what gets stored, and a DAX channel that is reconfigured,
disabled or recreated comes back with a DIFFERENT id. The stale one opens
without complaint on some drivers and simply never delivers — indistinguishable
from a quiet band until the file turns out to be empty.
The recorder now checks the id against the current endpoint list when it starts,
which takes milliseconds, and says outright that the configured input is gone —
on screen, not only in the log. If it IS still there, the answer is equally
useful: the device exists and the fault is upstream, in whatever should be
feeding it.
The watchdog fired exactly as intended and said:
no audio at all from "{0.0.1.00000000}.{6a27abfd-f277-496b-b6f1-f92ec284c636}"
which tells the operator nothing they can act on. "DAX RX 1 (FlexRadio DAX)"
points straight at the DAX panel.
Endpoint ids are what gets CONFIGURED and stored, so they are what the recorder
holds; the friendly name is resolved only when something has gone wrong. When
the endpoint cannot be found at all the id is still shown — a device that has
disappeared explains an empty recording too.
"No recording was running at log time" says the take had already ended, but not
who ended it. The only path that does is the callsign being cleared — which also
happens when a clicked spot replaces it — and that path was silent.
Now it names itself, so the log shows the moment the recording was lost instead
of only its absence at the end.
Second empty recording from the same station, and this time NEITHER source had
delivered a sample — so the fix for a silent second source could not apply, and
the log still said only "recording was empty" at the end of the QSO.
Two blind spots, both now closed:
- captureStream's error was discarded. A device that cannot be opened — renamed,
unplugged, held by another program — looked exactly like one that is merely
quiet.
- A WASAPI device can open cleanly and then produce nothing at all. A DAX
channel with no stream behind it does precisely that, and it is
indistinguishable from silence until the file turns out to be empty.
A watchdog now reports both, three seconds in, and the first goes to the OPERATOR
as a toast rather than only to the log: they are mid-QSO, and by the time they
find out at save time the audio is gone for good.
From an operator's log: a Flex with From Radio on DAX RX and Recording mic on
DAX Mic, a full CW QSO recorded, and at log time "snapshot failed: recording
was empty".
The mixer took min(len(A), len(B)) samples. In CW the mic path does not run, so
DAX Mic delivered nothing, min stayed 0, and the drift guard then threw the
radio audio away a second at a time. Two devices configured meant the recording
depended on the quieter one being alive.
A source that has delivered nothing for 1.5 s is now treated as absent and the
other is recorded alone, with one line in the log saying so — half a recording
is worth having, silence is not. Both are mixed again the moment the missing one
comes back.
Neither is written off before it has EVER spoken: at startup both timestamps are
zero, and declaring one dead then would clip the opening of every recording.
That case is tested too.
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.
"I click play and nothing happens" is what an operator sees when a function
returns an error into a promise the interface swallows. Every refusal in the
playback path is now logged as well as returned, and each names its own cause —
audio not initialised, the take still running, the take empty, the device
rejecting the file.
One of those was not even checked: playing back needs an OUTPUT device INTO the
radio, which is a different setting from the recorder's input and is very likely
unset on a station that has only ever recorded. It now refuses with that
sentence instead of failing somewhere inside the player.