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.
CW has moved in and out of this list twice — excluded for the missing sidetone,
restored because the other station is captured regardless. A test says which,
so the next reading of that comment does not undo the decision.
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.
SmartSDR moves its active flag onto the TX slice as soon as the transmit
frequency is touched, and OpsLog followed it. But the operator is listening to
the DX on the other slice: the S-meter, audio level, filter and DSP they are
adjusting all belong there, so following the transmitter handed them the
controls of a receiver they were not using.
In split the receive slice now wins. Simplex is unchanged, and two same-band
slices in different classes (SSB alongside FT8) are still not a split.
A slice clicked IN OPSLOG is remembered and overrides both the split rule and
the radio's focus — that is the operator speaking. Activating a slice on the
radio or in SmartSDR deliberately does not move OpsLog. The pin is dropped when
that slice stops being in use, so a closed receiver cannot strand the display.
The panel now highlights the slice OpsLog is working with rather than the radio's
focused one; reporting the radio's flag would light up one slice while every
control acted on another.
The 0.22.3 block had grown into explanations: why a fault happened, how it was
found, what it does not cover. That belongs in the commits, where it already is.
An operator opens this list to learn what changed for them. Sixteen entries now
say it in a line each.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Two silent returns in the save path: no take was running at log time, or the
mode carries no audio worth keeping. Both simply produced no file.
An operator reporting "the sound is not in the folder" is describing an absence,
which is the same absence in either case — but they are different problems with
different fixes, and neither leaves anything behind to inspect. Now each says
which one happened, and for which callsign.
The success line already existed ("qso-rec: saved <path>"), so between the three
the log now accounts for every logged QSO.
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.
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.