chore: release v0.21.3

This commit is contained in:
2026-07-26 16:57:19 +02:00
parent 4fd70f6a9d
commit 91b5af1c7b
46 changed files with 2491 additions and 355 deletions
+28 -7
View File
@@ -36,8 +36,8 @@ func DownloadLoTWConfirmations(ctx context.Context, client *http.Client, cfg Ser
q.Set("login", user)
q.Set("password", cfg.Password)
q.Set("qso_query", "1")
q.Set("qso_qsl", "yes") // only QSLed (confirmed) records
q.Set("qso_qsldetail", "yes") // include QSL_RCVD / QSLRDATE detail
q.Set("qso_qsl", "yes") // only QSLed (confirmed) records
q.Set("qso_qsldetail", "yes") // include QSL_RCVD / QSLRDATE detail
if c := strings.TrimSpace(ownCall); c != "" {
q.Set("qso_owncall", c) // restrict to this station callsign
}
@@ -65,11 +65,32 @@ func DownloadLoTWConfirmations(ctx context.Context, client *http.Client, cfg Ser
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("lotw: http %d", resp.StatusCode)
}
// LoTW returns a plain-text error (not ADIF) on bad login.
// Not ADIF. Two very different failures land here, and telling them apart is
// the difference between a fixable message and a wall of markup.
if !strings.Contains(strings.ToUpper(text), "<EOH>") && !strings.Contains(strings.ToLower(text), "<eor>") {
msg := strings.TrimSpace(text)
trimmed := strings.TrimSpace(text)
// Keep the whole thing in the log — that is where a real diagnosis happens,
// and a 200-character excerpt of an HTML page tells nobody anything.
snippet := trimmed
if len(snippet) > 2000 {
snippet = snippet[:2000]
}
LogSink("lotw: expected ADIF, got %d bytes of non-ADIF; first 2000: %s", len(text), snippet)
// LoTW answers a REJECTED LOGIN with its ordinary web page rather than an
// error string, so an HTML body here means the credentials were not
// accepted — not that the download is broken.
lower := strings.ToLower(trimmed)
if strings.HasPrefix(lower, "<!doctype html") || strings.HasPrefix(lower, "<html") || strings.Contains(lower, "logbook of the world</title>") {
return "", fmt.Errorf("LoTW returned its web page instead of a log, which is how it answers a login it did not accept. " +
"Check the username and password in Settings → External services: LoTW wants your lotw.arrl.org WEBSITE login, " +
"not your callsign certificate or your ARRL member number")
}
// Anything else: a plain-text complaint from LoTW, or a maintenance notice.
msg := trimmed
if len(msg) > 200 {
msg = msg[:200]
msg = msg[:200] + "…"
}
return "", fmt.Errorf("lotw: unexpected response: %s", msg)
}
@@ -220,8 +241,8 @@ func UploadLoTW(ctx context.Context, cfg ServiceConfig, tempDir, adifRecord stri
// AppCompat RUNASADMIN entry), but OpsLog isn't elevated so Windows
// refuses to launch it. Actionable message instead of the raw error.
return UploadResult{}, fmt.Errorf(
"lotw: Windows won't launch tqsl.exe because it's marked \"Run as administrator\". " +
"Fix: right-click %q → Properties → Compatibility → UNTICK \"Run this program as an administrator\" (Apply). " +
"lotw: Windows won't launch tqsl.exe because it's marked \"Run as administrator\". "+
"Fix: right-click %q → Properties → Compatibility → UNTICK \"Run this program as an administrator\" (Apply). "+
"Or run OpsLog itself as administrator.", tqsl)
} else {
return UploadResult{}, fmt.Errorf("lotw: run tqsl: %w", runErr)