The operator's keyer is a K3NG — an Arduino running an emulation of the
WinKeyer protocol — which changes the diagnosis and nearly broke it.
Checked against K3NG's own source before anything else, because yesterday's
handshake now FAILS a connect where it used to press on regardless. It is
safe: k3ng_keyer.ino implements admin echo (0x04) and echoes the byte back,
0x13 is a documented no-op, and OPTION_WINKEY_STRICT_HOST_OPEN — on by
default — ignores every byte except 0x00 before host open, so the resync
nulls are dropped harmlessly and the echo probe still gets through.
The real cause is in K3NG's options file, beside the feature itself:
"disabling Automatic Software Reset is highly recommended", and an option
to "discard errant serial port bytes at startup" for when it is not. On an
Arduino, DTR is wired to reset through a capacitor: opening the port reboots
the board into its bootloader. Ours spoke 400 ms later, to a keyer that was
not running yet.
So the retry now waits 2.5 s, which recovers it without an operator pressing
connect twice, and the engine list gains a K3NG entry that skips the doomed
fast attempt altogether. Everything else is identical to a K1EL — same
settings panel, same protocol.
Fifteen entries for seven subjects — the block had "WinKeyer:" twice, "QSL:"
and "QSL designer:" four times between them, "US counties:" three times.
That is one line per commit, which is how it was written, not one line per
thing an operator notices. Merged, in both languages.
An operator's log showed the whole fault in its first line: "connected on
COM3 — no reply — the keyer did not answer Host Open", followed by seven
configuration commands and two calls sent as Morse. Nothing was listening.
Reporting a link as up and then writing to it regardless is the part worth
fixing; the handshake is why it was down.
K1EL's Application Interface Guide gives the sequence, and we did one step
of it. Now all of it:
- DTR on, RTS OFF. K1EL's own init sets DTR_CONTROL_ENABLE with
RTS_CONTROL_DISABLE, and on a serial WinKeyer those lines ARE the power
supply — DTR feeds the 3.3 V regulator, RTS provides the negative rail.
go.bug.st/serial defaults both to true, so we drove RTS high on every
connect without a line of code saying so.
- 400 ms after the lines come up, for a WK1 still booting off DTR.
- Three 0x13 nulls to resync the command parser. A keyer left part-way
through a command by whoever spoke to it last would absorb Host Open as
a parameter — the everyday cause of a silent WinKeyer, and one the
operator can do nothing about from the outside.
- An echo test (0x00 0x04 0x55) before trusting the port at all. This is
the step that answers "is there a keyer here", and connecting now fails
on it, with the byte that came back when something else replied.
The whole handshake is retried once, since the first attempt's nulls are
what clear a confused parser. Tested against a fake port that reproduces
each failure: absent, mid-command, and echoing but versionless.
copyDirContents wrote into a destination it never created, so the first
os.Create returned ENOENT. The caller was written as "ignore os.IsNotExist"
— meant for a design with no pictures at all — and that guard matched the
failed write exactly: nothing was copied, no error surfaced, and validation
two lines later reported "copied design is incomplete: hero photo file
img_… not found" with no way to tell why.
Three fixes, one cause:
- copyDirContents creates its destination;
- the caller stats the source folder instead of pattern-matching an error,
so a genuine copy failure is reported and rolls back both the row and
the folder;
- DuplicateProfile had the same defect in its own form — an INSERT … SELECT
that cloned the template rows and left their photos behind, giving the
new profile designs with no thumbnail and nothing to print. It now copies
row and folder together, per template, since each needs its id first.
A test reproduces the original failure: it fails with the exact ENOENT that
was being swallowed.
QSL_VIA is the manager. QSL_SENT_VIA and QSL_RCVD_VIA are the ADIF "QSL Via"
enumeration — B bureau, D direct, E electronic, M manager (import-only) —
and say how a card travelled. OpsLog had one column for all three:
- the import folded QSL_SENT_VIA into QSL_VIA whenever QSL_VIA was empty,
which is exactly a Log4OM export (it defaults QSL_SENT_VIA to E), so
OE6CLD saw "E" everywhere OpsLog shows the manager;
- QSL_RCVD_VIA was listed in adifPromoted with no column behind it, so it
was not stored, not kept among the extras, and not exported — dropped
outright on import;
- neither was ever written on export, so an import followed by an export
destroyed both;
- and OpsLog polluted the field itself: the QSL Manager panel wrote
"Bureau" / "Direct" / "Electronic", in full words, into QSL_VIA.
Two columns added (migration 0027), carried through the five places a
promoted ADIF field has to touch, with round-trip tests pinning the reported
case. The QSL panel now offers Bureau / Direct / Electronic for each
direction and stores the enumeration; the manager field is labelled as the
manager and holds only that. M is kept when a file gives it and never
written back out.
Existing logs hold a mixture of the two in one column. The repair is offered,
not performed: the count is shown once per log with a plain question, and a
"no" is remembered. It moves only where QSL_SENT_VIA is still empty, and only
values that normalise to the enumeration — a manager is a callsign and can
never be one of those six words, which a test pins against real manager calls.
An operator asked why K1SEI showed "Middlesex" in Info (F2) and "Lower
Connecticut River Valley" in the cluster. Two sources: the entry panel has
a callbook answer, a spot carries only a callsign so the cluster derives one
from the FCC licence ZIP through GeoNames — and GeoNames has followed the
Census in replacing Connecticut's counties with the 2022 planning regions.
No award, callbook or log uses those, so every CT station matched nothing:
new county for ever, and counting toward nothing.
Measured against a full ULS import (1 556 444 US callsigns), 23 223 resolved
to a name the USA-CA reference does not contain. Three causes, three fixes:
- Spelling. "City and County of San Francisco", "Baltimore (city)",
"Nome (CA)", plus counties renamed since the award list was drawn
(Kusilvak, Oglala Lakota, the Valdez-Cordova split) and Alaska's four
"X City and Borough", whose reference codes read "JUNEAUCITYAND" because
the county-type suffix strip eats the wrong end. Normalised in
award.USCountyKey, which both sides already go through. 7 515 callsigns,
no re-download needed.
- Doña Ana, NM shipped into the reference as "NM/DO̱AANA" — mangled by a
non-UTF-8 CSV line, a code nothing could ever produce, so that county was
unwinnable and silent about it. Row repaired, cntygen now refuses such a
line, and a test makes every one of the 3 102 references reproduce its own
code from its own name.
- Connecticut. A planning region is drawn from towns in several counties, so
no name maps to a name — only the ZIP can resolve it. cmd/ctzipgen builds
the table from the Census 2020 crosswalk, filling PO-box-only ZIPs from the
nearest resolved centroid; all 11 ZIPs GeoNames still labels with a real
county agree with the result. 15 037 callsigns, applied at import, so the
store now carries a rules version and Settings says when a re-download is
needed.
Alignment itself is the last piece: a spot now shows the county the station is
logged with when we have one, and falls back to the ZIP-derived county only for
stations never worked.
The sampled colours were Warm light's own (#faf6ea / #e8dfc9 / #ddd2b8) —
the annotated screenshot was that theme, and the request written on it was
"just a little darker". Keep the cream on panels and the entry strip, take
the page, tab bar, toolbars and table headers a shade deeper, and move the
accent from burnt orange to ochre so the two are told apart in the picker.
Built from the three sands an operator sent: #faf6eb for panels, #e8dfca
for the page behind them, #ddd2ba for toolbars, table headers and rows.
The ordering is what makes it work — a panel reads as lifted off the page
and the log grid as settled into it, so rows stay scannable without extra
rules. Ink is a deep warm brown; pure black on sand glares.
A second profile is usually the same operator with a different rig or a
different locator — same callsign, same cards. Having to redraw a design
because the antenna changed is work nobody should do.
The designer now lists the designs the active profile cannot see and copies a
chosen one into it. Designs it already sees — its own, and the shared ones —
are left out: they are not something to copy.
A COPY, not a move or a share. The original profile keeps its design untouched
and the duplicate is free to diverge, which it usually will: a second profile
exists because something differs, and that something often ends up on the card.
The PICTURES are copied too, and that is the part worth getting right. Stored
documents reference photos by name relative to the template's own asset folder,
so duplicating the JSON alone would point the new design at files that are not
in its folder. It is validated against its own folder afterwards, and a failure
rolls the row back rather than leaving a design whose pictures are missing.
Sharing the source's folder was the other option and a worse one: deleting
either design would then have emptied the other.
From a user's log, repeating throughout the session:
rigctld: split off failed: cat: this radio's backend cannot set split
rigctld: unimplemented command "X PKTUSB -1"
Both are answered with an error, and JTDX treats a rig-control error as fatal:
it abandoned a transmission 0.86 seconds into a 13.8-second frame.
"S 0" — split OFF. Fake It uses no split, and JTDX still sends this to be sure.
A backend that cannot SET split was refusing a request that was ALREADY
satisfied. It now succeeds when the rig is simplex, without touching the rig at
all. Arming still fails loudly: there the request is real and unmet, and a
client that believes it transmits up the band while the radio sits on the DX's
frequency is exactly what that refusal exists to prevent.
"X <mode>" — set_split_mode. Sent during ordinary setup, Fake It included,
where there is no second VFO to give a mode to. Accepted now: applied when a
split is in force, and a plain success otherwise, because the transmit VFO
already has that mode — it is the same VFO. get_split_mode answers to match.
Both regressions fail without the fix with the exact codes from that log,
RPRT -9 and RPRT -11.
Only PTT transitions were recorded. So when JTDX aborted a transmission
0.86 seconds into a 13.8-second frame, the log showed the abort and the Fake It
frequency restore around it — and not the command that preceded them, which is
the one thing needed to tell whether OpsLog answered something the client could
not accept.
Behind the existing CAT wire-trace switch rather than a new one: this is a line
per poll and would drown an ordinary log, and an operator chasing a rig-control
fault turns that trace on already.
The first version was a single switch meaning "command them all", and that is
wrong the moment a station has three: two SPE on a combiner and a PowerGenius
on another antenna would all go into OPERATE together, keying an amplifier that
has nothing to do with the pair.
It is now a set. Each amplifier is ticked into the group or not, the group is
stored as a list of ids, and an amplifier outside it keeps its own buttons —
which is the entire point of it being a set.
A group of fewer than two members is stored as none: one amplifier coupled to
itself would make every command fan out to a single member for ever.
A remembered member that is no longer running is skipped rather than failing
the command — deleting one amplifier must not break the button on the other.
An amplifier saved without an id cannot join, and the panel says so instead of
quietly omitting it from the list.
It sat above the first amplifier card. With three cards configured — the case
it exists for — that is a scroll away from anything the operator is looking at,
and it was reported as missing rather than misplaced.
Moved next to Add amplifier: coupling belongs to the SET of amplifiers, not to
any one of them, so it belongs with the other set-level control.
The SPE CO1-2 combiner is an RF device: it sums two amplifiers and commands
nothing. So "combined" operation is really two amplifiers that must be held in
the same state, and one left in STANDBY while the other keys means the combiner
sees power on a single input.
A switch in Settings → Amplifier makes ON, OFF and OPERATE act on every
configured amplifier. Offered only with two or more: coupling one amplifier to
itself is a switch that cannot do anything.
The METERS stay per amplifier, deliberately. Two amps combined are still two
amps, and an operator watching for one of them to run away needs to see them
apart — a summed bar would hide exactly the fault worth catching.
Fanned out in AmpOperate/AmpPower rather than in the UI: the card and the docked
widget both call these, and a coupling built into one would be missing from the
other, which on a combiner means one amplifier keyed and one not. The clicked
amplifier goes first, so a partial failure still did what the operator asked
before it stopped.
While linked, an amplifier with no power command — a PGXL on its direct link —
is skipped silently rather than reported: it would make a successful pair look
broken.
RI1FJL/MM resolved to Franz Josef Land while the expedition was still sailing
there, telling the operator they had worked an entity they had not.
The old behaviour was deliberate — the comment read "strict DXCC says no
entity, but the log should still show the operator's country" — and that is
wrong for exactly this reason. A home country on a maritime mobile is not extra
information; it is a false claim about where the contact happened, and it is
the kind of false claim that gets a QSO submitted for an award it cannot win.
Trailing only. A LEADING "MM" is the Scotland prefix and "AM" is Spain, so
MM0ABC and AM5X keep their entities — getting that wrong would be a far larger
error than the one being fixed. /P, /M and /B are unaffected: a portable
station is still ashore.
In the cluster the column says "Maritime mobile — no DXCC" rather than going
blank, because blank is what an UNRESOLVED spot looks like and the two mean
opposite things — one is "we don't know yet", this is "there is nothing to
know". Display only: the QSO's own Country stays empty, since that field holds
a DXCC entity name and there is none.
ADIF defines CNTY as "STATE,COUNTY" — "GA,BARROW". OpsLog wrote the bare county
name, which a receiving logger cannot resolve: county names repeat across
states, and there is a Washington County in thirty of them. The award engine
here was never affected, since it reads the columns rather than the ADIF; the
damage was to every file we hand to someone else.
One writer covers everything — writeRecord — so this fixes file exports and the
LoTW, Club Log, HRDLog and QRZ uploads together. MY_CNTY gets the same
treatment.
The county alone is still written when no state is known: a bare name is worth
more than nothing, and inventing a prefix would be worse than either. A value
that already carries a comma passes through untouched, so re-exporting an
imported record cannot double the prefix.
Import is the mirror: "GA,BARROW" fills both columns, and a file carrying the
bare county — as OpsLog's own older exports do — still imports unchanged. The
state parsed out of CNTY only fills a blank STATE, never overrides it: STATE is
the dedicated field and the more specific statement.
"An existing connection was forcibly closed by the remote host" during DATA
reads the same whether the server refused the SIZE, hit an hourly quota, or
simply dropped the socket — and only the first is something an operator can act
on. The message now carries host:port, the encryption in use and the total
attachment size, so the size can be ruled in or out without a second attempt.
A path that does not exist is not counted: the mail library skips it, so
reporting it would describe a message that was never sent.
Puts "F4BPO is on air 14,074,000 FT8 IC-7300" on hrdlog.net's front page while
the station is operating, which is what HRDLog's own "Automatic HRDLog ON AIR"
setting does.
The endpoint and its field names come from HRDLog's own library
(github.com/iw1qlh/HRDLOG-net-library, HrdProtocol.SendOnAirAsync): OnAir.aspx,
with Frequency in Hz, Mode, Radio, Callsign, Code and App. Read rather than
guessed — an invented endpoint would have produced a switch that silently did
nothing.
HTTPS where that library uses plain HTTP: the upload code is a credential and
has no business crossing the network in clear, and the sibling NewEntry
endpoint on the same host already serves TLS.
A heartbeat every two minutes, not a change notification — HRDLog drops a
station from the list when it stops hearing from it. Silent when switched off,
unconfigured, or when the rig reports no frequency: announcing 0 Hz would say
the operator is on air on nothing. Failures are logged, never toasted; a
broadcast the operator did not ask for at that instant must not interrupt them
every two minutes on a flaky link.
Its own switch beside auto-upload rather than folded into it: this is a live
status and needs the rig readable, not a QSO.
compactH was 158px, "tuned so the compact entry strip fits in a single row".
A constant tuned against a layout stops being true the moment the layout
changes, and this one outlived a strip that had since shrunk — leaving about
70px of empty window under the fields.
The frontend now measures what it rendered and asks for that height, watched by
a ResizeObserver so a strip that wraps at a narrow width is followed too. The
topbar is added as its declared h-8 rather than measured, since it is fixed.
Bounded in the backend: a measurement of zero — a layout not yet painted —
must not collapse the window, and the call is ignored unless compact is on so
nothing can shrink the normal window. Rounded to whole pixels so a sub-pixel
reflow cannot start a resize loop.
It went under the Station Control rotator; the compass that wanted it is the
docked one beside the entry strip. Both are the same RotorCompass, so the
readout moves INTO that component: every compass in the app carries it, and no
caller draws its own.
The bezel already showed the short path as a red marker, but a marker is a
direction, not a number — and the numbers in the status bar are 10px, which is
what prompted this.
Clickable only when the caller passed onGoto, since a compass rendered without
one cannot turn anything and a button that does nothing is worse than a label.
The same pair already sits in the status bar, where it is 10px and several
operators reported not being able to read it. Repeated under the compass at a
size that can be read across a shack.
Clickable, like the ones in the status bar: a heading you can see and not act
on would be a step backwards from what is already there. Disabled and dashed
when no station is entered, rather than showing a heading to nowhere.
The same answer the Details tab already gave, moved to where the county is now
typed — a badge in a tab nobody has open while working a station is a badge
that does not do its job.
Debounced at 300 ms and keyed on state + county together: the field is typed
into as well as filled by the lookup, and the check queries the log. A county
name means nothing without its state, so both have to settle before asking.
The looked-up values now sit directly under the geography line they continue —
QTH, State, locator, then county and zones — and the free-text fields close the
block. Reading order follows what the lookup fills, rather than interleaving it
with what the operator types.
Comment and Note moving onto one line left the right column two rows shorter
than the left, and this fills it: county, both zones and the ADIF entity
number. All four are filled by the lookup and were only visible after opening a
tab — the wrong place for values an operator wants to SEE are right before
pressing Log.
County is a name, not a code ("St. Tammany Parish"), so it takes the flexible
width and the three numbers get fixed narrow boxes.
Cleared numeric fields store undefined, not 0. Zero is a real DXCC number and a
real zone, so an empty box has to stay empty rather than assert something the
operator did not type.
State stays up on the geography line beside QTH and the locator, which is how
the lookup fills them and how they are read.
stateBlock was built and then not placed in the row — the field simply did not
exist on screen. Now between QTH and the locator, where it was meant to go.
Left column 210 → 230px, and Comment and Note move onto ONE line side by side:
two fields that are usually a few words each were taking two full rows of the
entry strip. That frees a row for whatever goes there next.
Name, Band, Mode and Country go from 300px to 210px, which moves QTH, Comment
and Note left by the same amount. The width is set in TWO places — the Name row
and the Band/Mode/Country stack below it — and they have to stay equal or the
two rows stop lining up, so both were changed together.
State goes between QTH and the locator: it is part of the same "where is this
station" reading, and it is what WAS and the US county work key off. Narrow,
since it holds two or three characters, and upper-cased on the way in like the
other coded fields.
The Name label in this layout was hardcoded English while its twin went through
t(); fixed while here.
A log greets an operator; it does not address an envelope. "Robert Smith"
filled the Name field with something no one would ever send on the air, and it
is the first name that gets used when the contact is answered.
So fname alone, with the surname kept only as a last resort for a record that
has no first name at all — better a surname than an empty field. The nickname
option now falls back to the first name too, which is what it should have done
from the start.
joinName went with it: nothing composed a name any more.
QRZ publishes <nickname> — the name an operator goes BY on the air — and
OpsLog was composing fname + name instead. "Bob" is what belongs in a log;
"Robert J Smith" is what belongs on a licence.
QRZ only, and deliberately so: HamQTH's <nick> already fills the Name field
that way, so the same switch there would toggle a behaviour it has no way to
turn off.
A published nickname is optional, so an empty one falls through to the
registered name. That is the whole point of it being a fallback rather than a
swap, and it is what the test pins — a blank nickname must never blank the
name.
Spots were bounded by COUNT alone, so on a quiet band a two-hour-old spot sat
on the band map looking like something to chase. Settings → DX Cluster now
takes a lifetime: presets at 5/10/15/30/60 minutes, or any value up to twelve
hours, and 0 keeps the old behaviour.
Spots are REMOVED from the shared list rather than hidden at render. The
cluster list, every band map and the counts all read that one array, so pruning
it once is what makes the setting mean the same thing in every view — the
lesson already paid for when the band map ignored the cluster's filters. It
also gives the memory back.
A spot whose timestamp will not parse is never dropped: an unreadable stamp is
a reason to distrust the clock, not to throw away the spot.
Swept every 30 seconds — close enough that a 5-minute setting is honoured, and
invisible next to the spot stream. Clamped in the backend as well as the UI.
Reported on BH2SWB and 4X9AA: both flagged NEW PFX, neither ever worked, both
vanished the moment "hide worked" went on.
The predicate hid a spot when worked_call OR status === 'worked' — and that
second one is the ENTITY's status, not the callsign's. So any spot in an entity
worked years ago disappeared, however new it was for something else.
The extra markers are orthogonal to the entity status; the band map already
says so in as many words. They now win: a new prefix, county, grid or park is
never hidden, whatever the entity says. A worked entity that is new for nothing
is still hidden, which is what the filter is for.
Extracted to spotIsWorked in lib/spotDisplay, beside the other rule the cluster
and band map share, rather than left inline in a predicate that has already
grown once.
This repo has no frontend test runner, so this is verified by build and reading
rather than by a test.
Replaces the four fixed rules with the model Logger32 uses and the operator
asked for: Worked / Confirmed / QSL sent / To be sent, and inside each, which
channels count — paper QSL, LoTW, eQSL, QRZ.com. No ticks means every channel,
because a rule the operator has not narrowed must not quietly become a rule
about nothing.
"To be sent" is FIRST in the order, and that is the substantive decision here.
A contact can be confirmed on LoTW and still owe a paper card; the colour an
operator scans for is the one meaning "something is still owed". Placed after
"confirmed" that row goes green and the card never gets printed.
"Worked" is the catch-all — nothing sent, nothing requested, nothing back — and
is off by default, since turning it on paints every remaining row.
R and Q both count as owed: ADIF says requested and queued, and both mean the
card has not gone out.
"The ON button does nothing" has three different causes — the modem-line pulse
was refused, the pulse went out and the amplifier ignored it, or the amplifier
woke and the UI missed it — and the log could not separate them: it recorded
one combined error and never said whether the amp came up.
RTS and DTR are now reported separately (not every USB-serial chip honours the
modem lines, and some refuse them without saying so), alongside the model and
transport. A watcher then logs whether the amplifier answered within 15
seconds, off the caller's goroutine so the click still returns at once.
No change to the wake sequence itself, which is the part that took a long time
to get right on the 1.3K-FA.
"Was the grid in what WSJT-X sent, or did OpsLog put it there?" is the first
question asked when a contact is logged in the wrong square, and a byte count
cannot answer it. An expedition logged at its licence holder's home QTH stayed
undiagnosable for exactly that reason.
One line per QSO, around 280 bytes, whitespace collapsed so the record sits on
a single line.
Grid: RI0FA transmitted QN35 all evening and was logged with its licence
holder's home square. WSJT-X's logged ADIF usually carries no GRIDSQUARE, so
the lookup was the only source left — and QRZ/HamQTH describe where an operator
LIVES, which for an expedition is the wrong side of the planet.
The square heard on the air is now applied before the lookup runs, so
refineGrid has something to defend. It still upgrades a 4-character square to a
6-character one from the same field, and still refuses a finer square from a
different field.
SPE: decodeCSV marks the client connected on every frame it parses, and the
poll goroutine can be mid-read while PowerOff runs. The frame from a second ago
landed after PowerOff had marked the amp offline and put it straight back,
which is why OFF had to be pressed twice.
Frames are ignored for three seconds after the off key goes out — the window is
opened before the key is sent, so nothing already travelling can beat it. Time
bounded rather than latched: an amp switched back on at its own front panel has
to reappear without being told.
Reported: ten fresh contacts upload and appear on LoTW; twenty older ones,
bulk-edited to exactly the same LOTW_SENT/RCVD state, are accepted by OpsLog
and never arrive. The operator's reading was that a failed attempt could not be
overridden. It is simpler and worse than that.
TQSL's exit codes, from its own cmdline documentation:
8 NO QSOs were processed — already uploaded OR OUT OF DATE RANGE
9 some processed, some ignored — same two reasons
14 some already uploaded, the rest signed
8 and 9 were both read as plain success. So on 8 — nothing uploaded at all —
OpsLog announced "already uploaded (duplicate)" and stamped every selected
contact as sent. They were never on LoTW and now looked as if they were, which
is exactly the reported symptom, and it is not recoverable by re-uploading
because the operator has no reason to try.
"Out of date range" is the cause that bites here: a contact older than the
callsign certificate's validity is silently left out. Older QSOs failing while
today's succeed is the signature.
Now: 8 is a failure and nothing is stamped — a duplicate left at "R" is
harmless and will be refused again, whereas a contact wrongly marked sent is
one nobody will look at twice. 9 and 14 succeed but carry Ignored, and the
caller says so in the console and a toast.
TQSL's own sentence ("20 QSO records are out of date range") is captured and
shown. It was being read and thrown away, and it is the whole answer to "why is
my contact not on LoTW".
Two BandMap instances, and cleaning up a duplicated attribute took the prop off
the docked one as well as the copy. The multi-band tab drew the badge; the map
beside the entry form — the one actually in front of the operator — did not.
Logger32 draws a green square for a LoTW user and an X for a worked one. The
square is the part worth copying; the way it is drawn is not.
Colour on this map is already spoken for — it carries the entity status, and
the left edge of each pill stacks the new-park / new-county markers. Adding a
green fill for LoTW would put two unrelated meanings on one channel, and an
operator would have to work out which green meant what. So it is the same "L"
badge the cluster list draws, in the same muted blue, for the reason written
there: whether a station uploads to LoTW says nothing about whether the spot is
worth chasing.
Worked stations already read as worked here — the pill goes grey and the
status markers speak — so no X is needed.
Switchable in Settings → Appearance, and on by default: a `configured` flag
distinguishes "saved with this off" from "saved before the option existed", so
turning it off sticks instead of being undone by the next default.
The band map received the raw spot stream filtered by band alone, so none of
the cluster's filters reached it: switching on "LoTW users only" changed the
list and left the panadapter showing everyone. The same was true of hide
worked, the spotter continent, and the status and mode chips.
The predicate is extracted and shared rather than copied — two views of one
spot stream disagreeing about the same spot is the fault lib/spotDisplay
already exists to prevent, and a second copy would have drifted the first time
a filter was added.
The BAND filter is deliberately excluded from the shared part: a band map's
band is its filter, and applying the cluster's would empty every map but one.
The first version filled the whole row at 24%, and in a real log that meant
every row was painted: nearly every contact has SOME QSL state, so colour was
present everywhere and stopped being information — a striped background with
the data behind it.
The default is now a 3px stripe down the left edge. Same signal, nothing lost
to read it. A filled row is still offered, with a strength slider, for
operators who want the block — and the rule cards in Settings preview whichever
is chosen, so the decision is made by looking rather than by imagining.
Drawn as an inset shadow rather than a border: a border would shift the cells
three pixels on coloured rows only, and the columns would stop lining up.
Style and strength are normalised rather than rejected — a value out of range
is a slider that got away, not a reason to reset the operator's colours.
The "to send" rule only looked at qsl_sent, so a contact waiting to go to LoTW
— marked R, and the commonest not-gone-out-yet state in a digital log — matched
nothing and stayed uncoloured. It now covers any route still queued, and the
label says so.
New Settings → Appearance section: whole-row colouring in the log grid driven
by QSL / LoTW state, each rule with its own colour from a palette or a free
picker. What Logger32 does, with one difference that matters — the colour is
applied as a 24% tint, not a fill. Logger32's grid is white; this one is dark,
and a saturated user-picked colour behind white text is unreadable at exactly
the moment the operator is scanning for what still needs sending.
Rules are ORDERED and the first match wins, because a contact is usually
several of these at once: one confirmed on LoTW and by card is confirmed, not
"sent, awaiting reply". The order lives in the data so the panel can show the
rules in the order they actually apply, numbered.
The colour is interpolated into a CSS color-mix(), so anything that is not
plainly #rrggbb is refused on the way in and falls back to the default.
Also fixes the QSO number column, which was empty for contacts logged from
WSJT-X: that path inserts through the repo directly and never reached AddQSO.
Rather than chase each of the remaining bulk-insert paths — the POTA hunter
import and the LoTW/QRZ "add what I was missing" passes, which insert OLD
dates and so shift every number after them — the index now checks its length
against a COUNT and rebuilds when they disagree. One indexed count beats
remembering to invalidate in a place that does not exist yet.
OrderedIDs scanned qso_date straight into a time.Time. The column holds a
formatted STRING — the repo writes it with Format(isoMillis) and reads it back
through parseTimeLoose everywhere else — so the scan failed on every call, the
index was never built, and omitempty then dropped the zero from the JSON. A
column that shipped blank.
Scans the string and parses only the newest row; the ordering already came from
SQL, so 30 000 parses were never needed.
Tested against a real SQLite file with the date stored exactly as the repo
writes it, and with the ids running opposite to the dates — the imported-ADIF
case that is the whole reason this is not the id. It fails without the fix with
the scan error itself.
Not the id: the primary key follows insertion order, so importing an old ADIF
gives the oldest contacts the highest ids. This is a rank over qso_date.
Computed over the WHOLE log, not the query result — ranking inside the result
would renumber every contact the moment a filter is applied, and QSO #1 would
change identity as the operator typed.
Held as one id-to-rank map, built from a single ordered id query and dropped
with the other derived indexes when the log changes. A contact logged from the
entry form is the newest, so it takes the next number without rereading the
log: AddQSO deliberately avoids full invalidation because a contest run would
pay for it once per QSO. A contact entered with an OLDER date belongs in the
middle of the order, so there the map is dropped and rebuilt rather than
mis-numbered.