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
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"reflect"
"strings"
"testing"
)
// The sent side must never start at Y.
//
// Y means "already sent", so the uploader skips the contact for ever. An
// operator whose eQSL default was Y had a logbook that never reached eQSL and
// nothing on screen to explain it — the log said "not eligible, EQSLSent
// already Y" and that is the only place it was ever said.
func TestNoConfirmationDefaultsToAlreadySent(t *testing.T) {
d := defaultQSLDefaults()
v := reflect.ValueOf(d)
for i := 0; i < v.NumField(); i++ {
name := v.Type().Field(i).Name
val := strings.ToUpper(strings.TrimSpace(v.Field(i).String()))
if val == "Y" {
t.Errorf("%s defaults to Y — every new QSO would be skipped by its uploader for ever", name)
}
}
}
// Every field has a default. A service added without one comes back blank,
// which is not a status anybody chose — HAMLOG.online arrived that way and
// every existing profile got nothing for it.
func TestEveryConfirmationHasADefault(t *testing.T) {
d := defaultQSLDefaults()
v := reflect.ValueOf(d)
for i := 0; i < v.NumField(); i++ {
if strings.TrimSpace(v.Field(i).String()) == "" {
t.Errorf("%s has no default — add it to defaultQSLDefaults", v.Type().Field(i).Name)
}
}
}
// A profile saved before a service existed has no value stored for it, and
// gets the shipped one rather than a blank.
func TestStoredBlanksAreFilledFromTheDefaults(t *testing.T) {
// What an old profile looks like: the services it knew about are set, the
// ones added later are empty.
stored := QSLDefaults{
QSLSent: "N", QSLRcvd: "N",
EQSLSent: "R", EQSLRcvd: "N",
LOTWSent: "R", LOTWRcvd: "N",
ClublogStatus: "R", ClublogCfm: "N", HRDLogStatus: "R",
QRZComStatus: "R", QRZComCfm: "N",
}
got := fillMissingQSLDefaults(stored)
if got.HamlogStatus != "R" || got.HamlogCfm != "N" || got.HamqthStatus != "R" {
t.Errorf("services added later were left blank: hamlog=%q/%q hamqth=%q",
got.HamlogStatus, got.HamlogCfm, got.HamqthStatus)
}
// And a value the operator DID choose is untouched.
chosen := QSLDefaults{QSLSent: "I", EQSLSent: "N"}
filled := fillMissingQSLDefaults(chosen)
if filled.QSLSent != "I" || filled.EQSLSent != "N" {
t.Errorf("a chosen status was overwritten: %q / %q", filled.QSLSent, filled.EQSLSent)
}
}