'No QSOs processed' (exit 8) covers already-uploaded, out-of-certificate-
date-range, and a station callsign that does not match the location being
signed with -- TQSL's message does not say which. An operator whose QSO
was refused and then accepted after a round trip through another logger
is reporting a difference in the record itself, and the temp ADIF is
deleted the moment TQSL returns, so that difference could not be seen.
Logged on any non-zero exit: the record verbatim, the exit code and the
station location. It is QSO data; the key password is not logged.
HAMLOG.online is a seventh external service: one API key, one ADIF record per
QSO, immediate / delayed / on-close like the rest. They publish no API
documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent
(github.com/hamlogonline/Agent), which is the authoritative source short of
asking them:
POST https://hamlog.online/api/agent/
{"ADIFADD": {"APIKEY": k, "ADIFDATA": record}} → {"STATUS":"OK"}
{"KEYSTATUS": {"APIKEY": k}} → {"STATUS":"OK","CALLSIGN":…}
Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and
no STATUS, and reading an unknown reply — a proxy page, a maintenance notice —
as an acceptance is how a contact goes missing without anyone noticing.
KEYSTATUS buys something no other service here offers: the key can be checked
BEFORE the first QSO, and the answer names the account. A key pasted from
another callsign is caught in the settings panel rather than through a week of
silent refusals.
Their confirmations are also an award source now, ticked like LoTW rather than
expressed through "custom". It reads the ADIF extras, not a column: the standard
names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other
site's field would write a falsehood into every exported log. Three plausible
key names from their own export are accepted too, so nobody has to rename a
column by hand after an export.
Yaesu gains antenna selection (AN), remembered per band — the antenna picked on
a band comes back with it, with no table to fill in anywhere. Rigs with one
socket never answer AN and never show the row; the log says which case it is.
And a serial port that is refused now names its likely holder. OmniRig stays
resident once activated and keeps the port of the rig configured in it, so a
native backend never gets it — "Serial port busy" alone accused nobody, and an
FTDX10 spent a morning being blamed for it.
The embedded key was registered to XV9Q, not to OpsLog. Not a cosmetic
detail: Club Log identifies the client software by that key, so every
OpsLog upload in the world was attributed to that callsign. Its owner
received the abuse warning OpsLog earned when the on-close sweep was still
posting hundreds of QSOs through the realtime endpoint - and a revocation
aimed at them would have cut Club Log uploads for every user of this
program at once.
G7VJR issued a key for "OpsLog" on request. Same mechanism, same UX: the
key identifies the software, the operator still supplies their own e-mail
and password, so it authorises nothing on its own.
Club Log asks that it not be published in source code. The source is on a
private remote and only the built exe is released, but it remains
recoverable from that binary by anyone who looks - as it is for every
logger that embeds one. It is an identifier that can be attributed, not a
secret.
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.
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".
Club Log refuses with its ordinary web page rather than an error string, so the
new test reported a rejected login by pasting a 403 page — title, stylesheets,
navigation and all — into the status bar. The body now goes to the log, where a
real diagnosis happens, and the operator gets the one sentence there is to act
on: check the e-mail, the password and the logbook callsign.
Dropped the startyear=2099 filter with it, and that one matters more than it
looks. It was there to keep the reply small, but it was never verified against
Club Log's API — and Club Log answers an unrecognised request with the SAME 403
it uses for a refused login. An unverified parameter would therefore have made
every CORRECT password look wrong, which is precisely the failure this change
set out to end. The reply is capped at 4 KB and closed at once instead; the
status code arrives ahead of the body either way.
TestClublog checked that three fields were non-empty and returned "Ready — CALL
via EMAIL". Nothing was ever sent to Club Log, so a wrong password produced the
identical green message. It now signs in, through getadif.php because that is
the one authenticated endpoint that cannot change anything: a test must never
put a record into someone's log. A future start year keeps it from downloading
130 000 QSOs to prove a password, and a rejected login answers 403 before any
body arrives.
LoTW is two credentials doing two jobs and the button reported only the first.
Uploads go through TQSL signed by the certificate — the website password is
never involved — so a wrong one breaks nothing until the day confirmations are
downloaded, by which time nobody connects the two events. Both are now checked
and, more importantly, reported separately.
Also: the detector still refused 12 and 10 m while the PSK Reporter feed was
already subscribed to them, so those decodes were fetched and thrown away. The
rule was never "HF is out", it is "is an opening here an event" — 20 m being
open is the normal state of the band, 10 m opening is not.
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 single-worker upload queue introduced head-of-line blocking: one slow
upload (LoTW signing via TQSL, or a service on a 30 s timeout) stalled the
whole queue, so every following QSO's Club Log upload waited and the rows sat
at "R". Operators on the older, concurrent build had no such problem — the
tell that this was a fresh regression.
Back to one goroutine per upload (never serialised), so a slow upload never
holds up the rest, while keeping the transient-failure retry with back-off
(concurrent, so it can't block anything).
A QSO logged via UDP / ADIF-import fired `go m.upload` per QSO, so a pileup or
an ADIF-import burst hit a service with dozens of concurrent requests at once.
Club Log's nginx answers that burst with 403; the upload counts as failed and
the QSO stays at "R" while a lucky few go through — the real cause behind the
"it's uploaded but shows R" report (not the earlier display race).
Route all immediate/delayed auto-uploads through a single serialised worker
(one at a time, 250ms apart) so no burst ever trips a per-IP rate limiter.
upload() now reports whether a failure is transient (HTTP/rate-limit) vs
permanent (not eligible, wrong station callsign, no record); transient ones
are retried with exponential back-off (up to 4 attempts, 1s/2s/4s), so a QSO
that hit a momentary 403 ends up marked instead of stuck at R. A full queue
falls back to a goroutine rather than dropping an upload.