IOTA is an official ADIF field, and almost everything for it was already here:
the qso table has carried my_iota since the first migration, the insert and scan
handle it, and both ADIF import and export write it. The one missing link was
the station profile, so the reference had to be typed on every contact of an
island activation, or added afterwards in a bulk edit.
Migration 0025 adds the column; it joins my_sota_ref and my_pota_ref in the
profile, in the Station Information panel, and in the same stamp-if-empty block
that fills the other My* fields on a logged QSO.
Uppercased and trimmed on save. ADIF spells it EU-005, and an operator typing
"eu-005" would otherwise put a reference no award matcher recognises on every
QSO of an activation - the kind of mistake you only find months later.
The panel had grown two shapes for the same kind of choice. Some filters were
checkboxes, some were chips, and New counties only was a checkbox duplicating a
chip - which I added, and which was the worst of it: a control that exists twice
is not more discoverable, it is one control the operator has to recognise twice.
The rule now:
SWITCH a behaviour that is on or off, and narrows nothing by a property of
the station: hide worked, group duplicates, the two display options,
LoTW users only.
CHIPS pick any number from a set; none picked means all. Status, mode,
spotter continent. Selected is solid, unselected is the same chip
faded, so the palette keeps teaching the colour code while switched
off.
Nothing appears in both shapes. The duplicate county checkbox is gone.
Spotter continent became a chip row: seven two-letter codes fit on two lines,
they read as a set the way Status and Mode do, and several can be picked at
once - which "EU or NA" needs and a dropdown cannot express.
Both Lock buttons now sit in the heading of the section they lock, instead of
floating between sections, and every multi-select section clears the same way
through the same heading slot. One section helper and one chip helper, so the
next filter cannot drift.
The panel was also entirely hardcoded English - Search call, Hide worked, Bands,
Status - against the project's own bilingual rule. All of it goes through t()
now, both locales.
The L badge is a single letter, not a word: the call column is 120 px and holds
a callsign. It keeps the muted blue of a confirmation and never a status colour,
because whether a station uploads to LoTW says nothing about whether the spot is
worth chasing - those are different questions and must not share a palette.
The spotter's continent is not the DX's. It asks whether anyone near you is
hearing the band at all, which is why it earns its own control rather than
reusing the existing Continent column. The spotter callsign now travels with the
status query so the backend can resolve it against the one DXCC prefix table,
instead of a second continent rule appearing in the frontend.
Both AND with the status chips rather than joining their OR: "a new band, and
from Europe" is the question being asked. An unresolved spot is never dropped by
them - the status arrives a moment after the row, and filtering meanwhile made
the list flicker.
New counties only is the SAME state as the NEW COUNTY chip, reached a second
way, not a second filter. County chasing is a mode you switch into, and hunting
for one chip among eight is not how you switch into it.
Colour moves from the text to the CELL. A filled Band cell means new band, a
filled Pfx cell means new prefix, a filled County cell means new county. This is
not the pills coming back: a pill is a box inside the cell with its own height
and padding, so it pushed the text off the row baseline. A background has no
geometry - the text does not move a pixel - and it reads from across the room,
which a tinted glyph does not. Worked-call stays text-only: it is not a novelty,
and filling it would wash most of the rows.
The colours still come from the semantic tokens and lib/spotMarkers, so a fact
keeps one colour across the grid, the band map and the filter chips - and the
per-marker colour setting will drive all of it from one table.
US County column. The backend already resolved the county from the offline ULS
store to decide NewCounty and then threw it away; it now returns it, which costs
nothing.
The Locator column is renamed Spotter locator. It always held the SPOTTER's grid
- cluster.go says so - so a column labelled Locator next to a DX callsign was
reading as the DX's grid and was mostly empty besides. The DX grid is not in the
feed at any price worth paying under an RBN firehose.
Both display options move out of Preferences into the cluster filter panel,
beside Hide worked. They are changed while working a run, not set up once, and
a preferences dialog reopened every ten minutes is a filter in the wrong place.
The slot-highlight option reused the new-slot status, which already meant
something else and something narrower: the ENTITY was worked on this band and
on this mode, but never on the two together. What the option surfaces is a fact
about the CALLSIGN - never worked on this band and mode - and the entity may be
long since confirmed there.
Overloading it cost exactly what it always costs. Forty consecutive rows read
NEW SLOT, the genuine new-slot rows drowned in them, and the Status column
stopped carrying information at all.
new-call is its own status now, with its own badge, its own tooltip spelling out
that it is the callsign and not the entity, and its own filter chip. It keeps
the caution colour for the moment: it sits at the same "worth a look, not a new
one" level as new-slot, and the colour rework will separate them.
The status filter had to move onto the displayed status too. It read the raw
backend entry, where new-call by construction never appears, so the chip would
have selected nothing at all.
The "colour the stations not worked on this band and mode" option did nothing.
The backend computed WorkedSlot correctly and the generated bindings declared
it, but the status map in App.tsx is assembled field by field from the reply,
and worked_slot was not on that list. The extra key was simply dropped, so the
frontend saw undefined, the === false test never matched, and every spot stayed
muted. Silent by construction: nothing warns that a copied-out struct is missing
a field. All three assembly sites now carry it, and the state type names it so
the compiler catches the next one.
The band map also memoised the transformed status map on spotStatusRaw alone,
so toggling an option changed nothing until a poll happened to hand over a fresh
object. The options are read at render time and are part of the dependencies.
Observation, not prediction, and it needs no new data source: the cluster event
worker already enriches every spot with the great-circle distance and bearing
from the operator's grid, which is exactly what a single-hop Es detection rests
on.
The signature is four or more DISTINCT stations at 500-2400 km inside a 90
degree bearing sector within twelve minutes. Each constraint earns its place:
distinct callsigns because one station spotted by six skimmers is six spots and
one station; the lower bound because a 6 m contact under 500 km is ordinary
tropo and says nothing about the ionosphere; the upper bound because past one
hop the bearing test stops meaning anything; and the sector because a real Es
cloud illuminates a direction, which is what separates an opening from a merely
busy evening.
Fed AFTER the Historical guard in the worker. A SH/DX reply replays a hundred
past spots in a second - precisely the shape of a burst - and would announce an
opening that ended hours ago.
Season LABELS, it never gates. Both hemispheres get a summer peak and a lesser
winter one, and an opening outside those is announced with "unusual for the
season" attached: the rare one is the one an operator must not hear about last.
One announcement per band per opening (45 minute quiet period). An opening runs
for hours and produces hundreds of spots; one alert is information, forty is
noise.
The post-QSO status refresh was gated on the DX-cluster list being on screen; the band map draws the same statuses and was left out. Working a station in the ordinary layout - Recent QSOs left, docked band map right - left its pill NEW until the cluster tab happened to be opened. Reported on an E51 that stayed orange with the QSO already in the log.
The input was bound straight to the number and committed on every keystroke, so
it could not be EMPTIED: clearing it yields "", Number("") is 0, 0 fails the
"> 0" guard, the state never moved, and value={qsoLimit} snapped the old figure
straight back. Going from 200000 down to 100 was a fight against the field — and
read as the setting refusing to persist, which it was not: writeUiPref stores it
and syncPortablePrefs makes the database authoritative at boot. It also wrote
the preference once per keystroke (1, then 10, then 100).
Raw text in local state, committed on blur or Enter, per the controlled-input
note in CLAUDE.md.
Every line of cluster traffic went straight to state: a spread copy of a 2000-element array, a slice copy, and a React render, per line. An RBN feed sends hundreds a second, so that was tens of MB/s of garbage in the renderer and hundreds of re-renders - and it ran whether the console was open or not, so the cost was paid for a panel nobody was looking at. Reported on an old PC where the UI had stopped responding. Lines are staged in a ref and flushed on a 200 ms timer, the same shape the spot handler already uses. The staging buffer is bounded too, so a burst longer than the console can show is not carried in full just to be sliced away on commit.
Both band maps were pinned to a hardcoded width — 300px docked beside the
tables, 260px per card in the Band map tab. On a busy band the map could not be
given more room, and on a quiet one the log could not take it back.
The docked map becomes a resizable grid column with the grip in the gap between
the panes, so the handle costs no space; the tab cards share one width with the
grip on their right edge. Side-by-side columns of different widths read as a
mistake rather than a choice, which is why the tab has one width and not one
per card. Double-click either grip to return to the default.
The drag measures from the pointer's START position rather than the container,
so the same helper serves both edges — the docked map sits on the left or the
right depending on the operator's setting, and the grip is on its inner edge
either way. Pointer capture, like the main splitter: without it the map or the
grid under the cursor swallows the moves.
Both widths are persisted through writeUiPref and registered as portable, so
they travel with the data folder like the main splitter and the rest of the
layout.
One station reaches the log SHOUTED by QRZ, lower-cased by a hurried
operator and in whatever case an imported ADIF carried, so the same name
appears three ways across a log. Name and QTH are now title-cased word by
word; Comment and Note only get a capital first letter, because the rest
routinely holds callsigns and modes ("TNX QSO F5ABC, FT8 59") that
lower-casing would destroy.
Normalised on blur, never per keystroke — rewriting the value mid-word
fights the typist (the controlled-input trap in CLAUDE.md). Applied in
both entry layouts and in the QSO editor, since leaving the editor alone
would just reintroduce the mess on the first correction.
The Recent QSOs box only ever searches callsigns, so it upper-cases as
you type and carries an inline clear button.
A user on a slow PC with RBN saw OpsLog at 94% CPU and 8.5 GB RAM (Logger32: 3%
/ 34 MB on the same feeds). Two runaway costs under the spot firehose:
- ClusterSpotStatuses re-scanned the ENTIRE logbook (5-6 full-table maps) on
every 50 ms spot batch — ~20×/second — its "one scan regardless of batch" doc
was untrue. On a big log that's millions of row-scans/second → the pegged CPU.
Now cached in clusterStatusCache (an immutable snapshot), rebuilt only when the
logbook changes (noteWorked on a single log, invalidateAwardStats on bulk), so
it's one scan per logged QSO instead of per batch.
- The frontend spotStatus map had no cap: one entry per call|band|mode ever seen,
and RBN produces thousands of unique calls/hour → unbounded growth to GBs, plus
a full {...prev} copy 20×/second. Now pruned back to the live (SPOTS_CAP=1000)
spots once it drifts past 2×, with a cheap same-reference bail-out otherwise.
The pop-up listing the QSOs behind a Stats band/mode square gets a cleaner header
(band chip, hint), banded/hover rows and roomier spacing. Callsigns are now
clickable and open the QSO editor (App.openEdit threaded through DetailsPanel →
BandSlotGrid → SlotQSOModal as onEditQso), matching the double-click-to-edit of
the log grids.
setSpotStatus({}) blanked EVERY spot's status, so logging one contact wiped all
the other NEW/NEW-BAND badges and they popped back a moment later. Instead,
re-fetch the shown spots and OVERWRITE each status in place (merge) — the just-
worked spot updates while the rest stay put, no flicker. Same visibility gate
and 2 s debounce; backend cost is one status batch (one logbook scan) as before.
A just-worked call/entity/POTA/prefix/slot kept its NEW badge because per-spot
status is cached and never expires. Drop the cache on qso:logged so the grid
re-evaluates. Kept cheap for old machines / big logbooks: only fires while the
cluster is on screen (a log while hidden marks it dirty and refreshes once on
next open), and debounced 2 s so a fast run coalesces instead of re-scanning the
logbook per QSO.
The WORKED filter also matched the entity status 'worked' (entity/band/mode
already worked, shown as a plain "—"), so selecting WORKED still listed spots of
calls never worked. Match the 'worked' key only via worked_call (the blue
WKD-CALL flag); a worked call still shows under NEW BAND / NEW SLOT.
The filter logic already matched worked_call spots against a 'worked' key; add
the chip to the status row so worked spots can be shown or isolated, not only
dropped via the "Hide worked" checkbox.
Five operator-requested items:
- Alt+W clears the QSO entry. Handled before the `typing` guard and above
the keyer's key routing, so there is always one key that clears whatever
else is running — Esc is not that key when the CW keyer reserves it.
- The Grid box no longer pops outside the entry panel on a narrow window.
Row 2 needed 300+130+76+gaps = 538 px inside a panel whose min-width is
520, so Grid was pushed out and clipped at every narrow width, not just
extreme ones. QTH's min-width drops to 80 and the row wraps rather than
overflowing if it ever still can't fit.
- Selecting a QSO in the log now drives the Stats (F1) matrix. Uses its own
WorkedBefore call into separate state, NOT runWorkedBefore: that one owns
the entry form's wbRef and can trigger a field backfill, which browsing
the log must never do. The entry form wins whenever it holds a call.
- Clicking a coloured band/mode square lists the contacts behind it. Returns
the exact callsign AND the rest of the entity, because that pair is what
the cell's colour encodes; the call's own QSOs are bolded. The DXCC arm
matches the stored dxcc column only — reconstructing it from the callsign
here would disagree with the matrix above, which is built from that column.
- The Awards DXCC list shows each entity's primary prefix in its own sortable
column. Derived live from cty.dat into Ref.Group rather than stored on the
reference row, so an existing installation needs no re-seed.
Two ways a bogus contact could be logged:
- Enter in the TX/RX frequency field bubbled to the entry container's
onKeyDown, which calls save() on Enter-in-an-input. Typing a frequency and
pressing Enter tuned the rig AND logged the QSO. The freq fields now
stopPropagation so Enter only tunes.
- WSJT-X/JTDX broadcasts its DX-call field over UDP; when it holds a bare
number ("1"), applyUdpCall dropped it into the callsign, and the next Enter
logged a QSO with callsign "1". Reject any DX call with no letter.
The keyer panel synthesised a "connected" status for the icom/flex/yaesu
rig engines but not kenwood, so the Kenwood/Elecraft engine fell back to the
(disconnected) WinKeyer hardware status and always showed "not connected"
even though its CAT link was up.
The frontend engine loader had no "kenwood" case, so a saved engine of
"kenwood" fell through to the WinKeyer default — the Kenwood/Elecraft
CW-over-CAT engine never ran even though the setting showed it selected.
Also clarify the Kenwood data-mode comment: there is no single mode digit
right for both a K3 (DATA=MD6) and a TS-590SG (MD6=FSK), so data stays on
USB as the safe common base; a rig-specific DATA mode is a follow-up.
- PGXL: remote AUTH password; direct-link meter fallback (VITA-first) with
250 ms poll + peak-hold; fan mode uses bare "fanmode=" (was ignored).
- TCI: spot colour sent as decimal ARGB (ExpertSDR dropped the hex string);
spot click handled via CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT to fill the entry.
- FlexRadio: clicking an OpsLog spot on the panadapter now applies the mode too.
- Filter presets: the trash icon deletes again (Radix pointer-down intercept).
- PTT hotkey: keys over CAT when a backend is active (was hijacked by a stale
Audio-tab RTS/DTR serial setting); AltGr keys allowed; presses logged.
- CW macro <LOGQSO>: logs after the preceding CW is sent, not on the first letter.
Settings -> Rotator is now a list like the amplifiers: add/remove rotors,
mix PstRotator / Rotator Genius / ARCO, and a Rotator Genius can drive both
its ports (one entry = two rotors). The compass gains a rotor selector, and
a per-rotor motorized-antenna flag so the boom/pattern paths show only for
the rotor carrying the Ultrabeam/SteppIR.
Also in this batch: a keyboard PTT hotkey (hold-to-talk or toggle, reusing
the audio PTT method with a CAT fallback), and TCI spot push to the
panadapter with click-to-fill of the callsign.
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 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.
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.
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.
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.
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.
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".
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.
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.
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.
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 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.
A red dot in the slot the counter will occupy. Click it and the counter starts,
the dot disappears, and logging the QSO writes the file exactly as an automatic
recording would — the save path already keyed off an active take, not off the
setting.
With automatic recording disabled the capture engine is not idle, it is not
running at all, so a manual take starts it. Two consequences handled here:
- It is released again when the take ends — logged, cancelled, or dropped for
being a digital mode. An operator who records one contact does not expect
their microphone held open for the rest of the session. The release is a
deferred call at the top of the save path so every exit goes through it,
rather than one that has to be remembered at each return.
- There is no pre-roll. An automatic recording opens with the seconds BEFORE
the callsign was typed; a manual one can only begin now. The button's
tooltip says so, because otherwise the difference between two files is a
mystery to the person holding them.
The button only appears where it can work: devices configured, automatic
recording off, and a mode whose audio is worth keeping. That state is asked of
the backend rather than inferred from a preference in the interface, so the two
cannot drift apart, and it is re-read when settings are saved.
From a Kenwood trace: IF answered mode digit 1 on 7.160 MHz, the backend read
LSB correctly, and the entry form showed no mode at all while the frequency
tracked perfectly.
Nothing was wrong in the backend. Radios report the SIDEBAND, and the station's
mode list holds SSB — so the value pushed into the selector was not in its own
list of options and nothing could match. Yaesu and Icom report the same way, so
this was never Kenwood-specific; it only surfaced there.
Normalising at the point where a rig-reported mode enters the form fixes every
backend at once, rather than editing three mode tables and the next backend
someone writes.
It is also the ADIF-correct direction, which matters more than the selector:
LSB and USB are SUBMODEs in ADIF, not modes. MODE=LSB is not a valid value and
is what would have gone to LoTW and eQSL.
An operator who has deliberately put LSB or USB in their own mode list keeps
them — the list is their statement of what they log, not ours.
Two faults in the progress bar shipped an hour ago.
It was a modal. A hundred QSOs is a minute; a contest log is two thousand and
ten minutes, and for all of it the operator could do nothing else — while their
radio is on. It is now a card in the corner: same bar, same count, same
callsign, none of the imprisonment.
And the menu said "Update from QRZ.com" while the code has always walked EVERY
configured provider, falling through to HamQTH when QRZ answers not-found. The
label is now "Update from the callsign databases", which is what happens.
Noted while reading that code, NOT fixed here because it trades against the
user's API quota and is their call: this path goes through the lookup cache, so
a second run on the same callsigns re-reads the cache rather than reaching the
provider. The cache comment already argues the opposite for deliberate clicks —
"a lookup the operator asked for by clicking is a deliberate act and must reach
the provider" — and a right-click on a selection is exactly that.
Selecting the 102 contacts of a freshly imported contest log and updating them
from QRZ.com is 102 network round trips. Nothing moved on screen while it ran,
so the only honest reading was that the application had frozen — and the natural
response to that is to kill it, halfway through.
The backend now emits a progress event per QSO and the frontend shows the same
overlay the ADIF import uses: a bar, the count, and the callsign being queried.
The callsign is reported BEFORE the lookup rather than after. With a slow
provider that is the difference between "waiting on F4BPO" and a name that lags
one QSO behind whatever is actually taking the time — which would be the field
someone stares at while deciding whether it is stuck.
The overlay is cleared by the closing event and again in a finally, so neither
a completed run nor a failed one can leave it on screen.
An operator worked the RSGB IOTA contest in another logger and got back records
carrying <STATE:5>EU005: N1MM-class software stores the received exchange in the
column its contest module uses, not the one ADIF reserves. Imported verbatim,
every QSO gains a US-state field reading "EU005" and the IOTA award sees
nothing — the reference is in the log, just not where anything looks for it.
The import dialog gains optional source → destination rows, prefilled with
STATE → IOTA since that is the case that prompted it. Applied to the RAW record
before conversion, so it works for promoted columns and Extras alike and the
destination is parsed exactly as if the file had carried it there.
Two rules, both from the same principle that the file outranks our guess:
- the source is CLEARED, so a reference does not linger in STATE where it
would show as the contacted station's US state in the grid and every export;
- a destination the file already filled is kept, judged against the record as
it ARRIVED — otherwise the result would depend on Go's map iteration order,
which is deliberately random.
The swap case in the test is what forced that second rule to be stated: "never
overwrite" and "exchange two fields" cannot both hold silently, so a destination
that is itself a mapping source is treated as an explicit swap and nothing else
is overwritten.
Reported: a 6 s auto-call gap gives clearly less than 6 s between the end of one
CQ and the start of the next.
The wait after sending watched only the keyer's busy signal. That signal travels
from the keyer to the window as an event, and the code gave it one second to
appear before giving up — if it had not arrived and cleared by then, the wait
ended immediately and the gap ran from the START of the call. A 6 s gap after a
4 s CQ becomes about 2 s of silence, which is exactly what was heard.
The message's own duration is now a FLOOR: the wait runs at least the estimated
sending time, and the busy signal only extends it (slow link, long buffer),
capped as before. The estimate can only be approximate, but it cannot be skipped
by a late event, and erring long costs a slightly wider gap rather than a call
that steps on the previous one.
"Showing 10000 of 23683 matches · 28648 total" was read as a filter matching more
than the log holds. The numbers were right — 23 683 matches out of 28 648 QSOs,
capped at 10 000 displayed — but the middle one belonged to "matches" while
sitting where "of N" normally means the total.
Each number is now named where it appears: shown · match the filter · in the log.
Thousands separators too, since 23683 and 28648 are hard to compare at a glance.
No counting change: both counts run over the same table, so matches can never
exceed the total.