fix(qsl): every confirmation service has a default, and the wiki explains them

HAMLOG.online was added after most profiles were configured, so it had no
entry in the shipped defaults and no stored value either: it came back
blank, and blank is not a status anybody chose. Every service now starts
the same way — the sent side at R, the received side at N — and a blank
left by a service that did not exist when the operator last saved is
filled in from that. A status they chose themselves is untouched.

Two tests hold the line: no sent side may default to Y, and no field may
be left without a default. Y means "already sent", so it makes the
uploader skip the contact for ever — an operator with eQSL Sent at Y had
a logbook that never reached eQSL, and the only trace was one line in the
application log.

Wiki, both from operator reports:

QSL Management opens with Confirmations — what the page actually is (the
status stamped on every new QSO, not an action), what each status does,
and the warning about Y in the plainest words available, because it fails
silently and by design.

Digital Modes and GridTracker is new. Unicast and multicast explained
from the operating problem rather than the networking: one letterbox that
two programs watch, against a broadcast everyone can tune to. It carries
the real evidence — two starts of one station an hour apart, decodes in
the second and none in the first, the only difference being whether
GridTracker or OpsLog reached port 2237 first — then the settings for
WSJT-X, JTDX, MSHV, GridTracker and OpsLog, the 127.0.0.1-in-the-group-box
mistake, what to do if unicast is unavoidable, and how to check it from
the log.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-08 09:37:37 +02:00
co-authored by Claude Opus 5
parent 612e265837
commit 82cd5c5d0b
8 changed files with 360 additions and 3 deletions
+44 -1
View File
@@ -11139,6 +11139,19 @@ func (a *App) UILog(msg string) {
// that hasn't customised them: request the online confirmations (eQSL/LoTW/
// Clublog/HRDLog/QRZ "sent"=R so OpsLog knows they still need uploading), and
// "N" for paper and everything received.
// defaultQSLDefaults is the status stamped on a new QSO when the operator has
// not chosen otherwise.
//
// One rule, for every service: the SENT side starts at R (requested — this
// contact is waiting to go out) and the RECEIVED side at N (nothing has come
// back yet). Paper QSL is the exception on the sent side: a card is only
// "requested" once somebody asks for one.
//
// The sent side must NEVER default to Y. Y means "already sent", so the
// uploader skips the contact for ever — which is exactly how an operator ends
// up with a logbook that never reaches eQSL and no error to explain it. Every
// service added here has to appear in this list for that reason; a service left
// out gets an empty status, which is a state nobody chose.
func defaultQSLDefaults() QSLDefaults {
return QSLDefaults{
QSLSent: "N", QSLRcvd: "N",
@@ -11146,10 +11159,40 @@ func defaultQSLDefaults() QSLDefaults {
LOTWSent: "R", LOTWRcvd: "N",
ClublogStatus: "R", ClublogCfm: "N", HRDLogStatus: "R",
HamqthStatus: "R",
HamlogStatus: "R", HamlogCfm: "N",
QRZComStatus: "R", QRZComCfm: "N",
}
}
// fillMissingQSLDefaults replaces an empty status with the shipped one.
//
// A service added to OpsLog after an operator last saved their confirmations
// has no stored value, so it came back blank — and blank is not one of the
// statuses anything acts on, it is the absence of one. HAMLOG.online arrived
// that way and every existing profile got nothing for it.
//
// An operator who genuinely wants a column left alone has "N" for that, which
// says the same thing and is a status. So an empty value here means "this was
// never asked", and the answer is what a fresh profile would have got.
func fillMissingQSLDefaults(d QSLDefaults) QSLDefaults {
def := defaultQSLDefaults()
for _, f := range []struct{ cur, def *string }{
{&d.QSLSent, &def.QSLSent}, {&d.QSLRcvd, &def.QSLRcvd},
{&d.LOTWSent, &def.LOTWSent}, {&d.LOTWRcvd, &def.LOTWRcvd},
{&d.EQSLSent, &def.EQSLSent}, {&d.EQSLRcvd, &def.EQSLRcvd},
{&d.ClublogStatus, &def.ClublogStatus}, {&d.ClublogCfm, &def.ClublogCfm},
{&d.HRDLogStatus, &def.HRDLogStatus},
{&d.QRZComStatus, &def.QRZComStatus}, {&d.QRZComCfm, &def.QRZComCfm},
{&d.HamlogStatus, &def.HamlogStatus}, {&d.HamlogCfm, &def.HamlogCfm},
{&d.HamqthStatus, &def.HamqthStatus},
} {
if strings.TrimSpace(*f.cur) == "" {
*f.cur = *f.def
}
}
return d
}
func (a *App) GetQSLDefaults() (QSLDefaults, error) {
out := QSLDefaults{}
if a.settings == nil {
@@ -11186,7 +11229,7 @@ func (a *App) GetQSLDefaults() (QSLDefaults, error) {
out.HamlogStatus = m[keyQSLDefaultHamlogStatus]
out.HamqthStatus = m[keyQSLDefaultHamqthStatus]
out.HamlogCfm = m[keyQSLDefaultHamlogCfm]
return out, nil
return fillMissingQSLDefaults(out), nil
}
// SaveQSLDefaults persists the configured defaults. Future QSO inserts