fix: bug sending LoTW on close
This commit is contained in:
@@ -196,7 +196,8 @@ const (
|
||||
keyExtLoTWStationLoc = "extsvc.lotw.station_location"
|
||||
keyExtLoTWForceCall = "extsvc.lotw.force_station_callsign" // override STATION_CALLSIGN at sign time (e.g. F4BPO/P on the F4BPO cert)
|
||||
keyExtLoTWKeyPassword = "extsvc.lotw.key_password"
|
||||
keyExtLoTWUploadFlag = "extsvc.lotw.upload_flag"
|
||||
keyExtLoTWUploadFlag = "extsvc.lotw.upload_flag" // legacy single flag (migrated to upload_flags)
|
||||
keyExtLoTWUploadFlags = "extsvc.lotw.upload_flags" // CSV set of lotw_sent values to upload (e.g. "N,R")
|
||||
keyExtLoTWWriteLog = "extsvc.lotw.write_log"
|
||||
keyExtLoTWAutoUpload = "extsvc.lotw.auto_upload"
|
||||
keyExtLoTWUploadMode = "extsvc.lotw.upload_mode"
|
||||
@@ -724,12 +725,13 @@ func (a *App) startup(ctx context.Context) {
|
||||
// from settings and host callbacks to build ADIF, stamp the upload
|
||||
// status and surface errors to the UI.
|
||||
a.extsvc = extsvc.NewManager(extsvc.Deps{
|
||||
BuildADIF: a.buildUploadADIF,
|
||||
MarkUploaded: a.markExtUploaded,
|
||||
NotifyError: a.notifyExtError,
|
||||
ShouldUpload: a.extShouldUpload,
|
||||
StationCallOf: a.stationCallOf,
|
||||
Logf: applog.Printf,
|
||||
BuildADIF: a.buildUploadADIF,
|
||||
MarkUploaded: a.markExtUploaded,
|
||||
NotifyError: a.notifyExtError,
|
||||
ShouldUpload: a.extShouldUpload,
|
||||
StationCallOf: a.stationCallOf,
|
||||
CloseUploadIDs: a.closeUploadIDs,
|
||||
Logf: applog.Printf,
|
||||
})
|
||||
a.extsvc.SetConfig(a.loadExternalServices())
|
||||
|
||||
@@ -855,7 +857,7 @@ func (a *App) plannedShutdownSteps() []shutdownStep {
|
||||
}
|
||||
}
|
||||
if a.extsvc != nil {
|
||||
if n := a.extsvc.PendingCount(); n > 0 {
|
||||
if n := a.extsvc.CloseUploadCount(); n > 0 {
|
||||
out = append(out, shutdownStep{
|
||||
ID: "extsvc-upload",
|
||||
Label: fmt.Sprintf("Uploading %d QSO(s) to online logbooks", n),
|
||||
@@ -4822,6 +4824,31 @@ func (a *App) getManyScoped(prefix string, keys ...string) (map[string]string, e
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseUploadFlags resolves the LoTW "treat as unsent" status set: prefer the
|
||||
// CSV (new multi-select), fall back to the legacy single flag, and default to
|
||||
// N+R when nothing is configured (covers an imported ADIF still marked unsent).
|
||||
func parseUploadFlags(csv, legacy string) []string {
|
||||
add := func(dst []string, seen map[string]bool, raw string) []string {
|
||||
for _, p := range strings.Split(raw, ",") {
|
||||
f := strings.ToUpper(strings.TrimSpace(p))
|
||||
if (f == "N" || f == "R") && !seen[f] {
|
||||
seen[f] = true
|
||||
dst = append(dst, f)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
out := add(nil, seen, csv)
|
||||
if len(out) == 0 {
|
||||
out = add(out, seen, legacy)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []string{"N", "R"}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *App) loadExternalServices() extsvc.ExternalServices {
|
||||
var out extsvc.ExternalServices
|
||||
if a.settings == nil {
|
||||
@@ -4838,7 +4865,7 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
|
||||
keyExtClublogEmail, keyExtClublogPassword, keyExtClublogCallsign,
|
||||
keyExtClublogAPIKey, keyExtClublogAutoUpload, keyExtClublogUploadMode,
|
||||
keyExtLoTWTQSLPath, keyExtLoTWStationLoc, keyExtLoTWForceCall, keyExtLoTWKeyPassword,
|
||||
keyExtLoTWUploadFlag, keyExtLoTWWriteLog,
|
||||
keyExtLoTWUploadFlag, keyExtLoTWUploadFlags, keyExtLoTWWriteLog,
|
||||
keyExtLoTWAutoUpload, keyExtLoTWUploadMode,
|
||||
keyExtLoTWUsername, keyExtLoTWWebPassword)
|
||||
if err != nil {
|
||||
@@ -4870,12 +4897,15 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
|
||||
StationLocation: m[keyExtLoTWStationLoc],
|
||||
ForceStationCallsign: m[keyExtLoTWForceCall],
|
||||
KeyPassword: m[keyExtLoTWKeyPassword],
|
||||
UploadFlag: m[keyExtLoTWUploadFlag],
|
||||
UploadFlags: parseUploadFlags(m[keyExtLoTWUploadFlags], m[keyExtLoTWUploadFlag]),
|
||||
WriteLog: m[keyExtLoTWWriteLog] == "1",
|
||||
Username: m[keyExtLoTWUsername],
|
||||
Password: m[keyExtLoTWWebPassword],
|
||||
AutoUpload: m[keyExtLoTWAutoUpload] == "1",
|
||||
UploadMode: extsvc.UploadMode(m[keyExtLoTWUploadMode]),
|
||||
// LoTW only ever uploads as an on-close batch (ARRL discourages per-QSO
|
||||
// uploads), so the UI offers no other timing. Force it here so configs
|
||||
// saved by older builds — which stored "immediate" — still batch at close.
|
||||
UploadMode: extsvc.ModeOnClose,
|
||||
}
|
||||
// Default the TQSL path to the standard install location when unset, so
|
||||
// the field is pre-populated if TQSL is present.
|
||||
@@ -4896,34 +4926,35 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
mode := string(extsvc.ModeImmediate)
|
||||
if cfg.QRZ.UploadMode == extsvc.ModeDelayed {
|
||||
mode = string(extsvc.ModeDelayed)
|
||||
// Preserve the chosen upload timing — including "on_close", which the LoTW
|
||||
// batch flush at shutdown depends on. (A previous version collapsed anything
|
||||
// that wasn't "delayed" to "immediate", silently disabling on-close upload.)
|
||||
modeOf := func(m extsvc.UploadMode) string {
|
||||
switch m {
|
||||
case extsvc.ModeDelayed:
|
||||
return string(extsvc.ModeDelayed)
|
||||
case extsvc.ModeOnClose:
|
||||
return string(extsvc.ModeOnClose)
|
||||
default:
|
||||
return string(extsvc.ModeImmediate)
|
||||
}
|
||||
}
|
||||
mode := modeOf(cfg.QRZ.UploadMode)
|
||||
auto := "0"
|
||||
if cfg.QRZ.AutoUpload {
|
||||
auto = "1"
|
||||
}
|
||||
clMode := string(extsvc.ModeImmediate)
|
||||
if cfg.Clublog.UploadMode == extsvc.ModeDelayed {
|
||||
clMode = string(extsvc.ModeDelayed)
|
||||
}
|
||||
clMode := modeOf(cfg.Clublog.UploadMode)
|
||||
clAuto := "0"
|
||||
if cfg.Clublog.AutoUpload {
|
||||
clAuto = "1"
|
||||
}
|
||||
ltMode := string(extsvc.ModeImmediate)
|
||||
if cfg.LoTW.UploadMode == extsvc.ModeDelayed {
|
||||
ltMode = string(extsvc.ModeDelayed)
|
||||
}
|
||||
ltMode := modeOf(cfg.LoTW.UploadMode)
|
||||
ltAuto := "0"
|
||||
if cfg.LoTW.AutoUpload {
|
||||
ltAuto = "1"
|
||||
}
|
||||
ltFlag := strings.ToUpper(strings.TrimSpace(cfg.LoTW.UploadFlag))
|
||||
if ltFlag != "N" && ltFlag != "R" {
|
||||
ltFlag = "R"
|
||||
}
|
||||
ltFlags := strings.Join(parseUploadFlags(strings.Join(cfg.LoTW.UploadFlags, ","), ""), ",")
|
||||
ltWriteLog := "0"
|
||||
if cfg.LoTW.WriteLog {
|
||||
ltWriteLog = "1"
|
||||
@@ -4946,7 +4977,7 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
|
||||
keyExtLoTWStationLoc: strings.TrimSpace(cfg.LoTW.StationLocation),
|
||||
keyExtLoTWForceCall: strings.ToUpper(strings.TrimSpace(cfg.LoTW.ForceStationCallsign)),
|
||||
keyExtLoTWKeyPassword: cfg.LoTW.KeyPassword,
|
||||
keyExtLoTWUploadFlag: ltFlag,
|
||||
keyExtLoTWUploadFlags: ltFlags,
|
||||
keyExtLoTWWriteLog: ltWriteLog,
|
||||
keyExtLoTWAutoUpload: ltAuto,
|
||||
keyExtLoTWUploadMode: ltMode,
|
||||
@@ -5682,6 +5713,70 @@ func (a *App) stationCallOf(id int64) string {
|
||||
return strings.ToUpper(strings.TrimSpace(q.StationCallsign))
|
||||
}
|
||||
|
||||
// closeUploadIDs returns the QSO ids to upload to a service at app close,
|
||||
// scanning the whole logbook: LoTW matches the configured sent-status set
|
||||
// (N/R), QRZ/Club Log return anything not yet "Y". This is what lets an
|
||||
// imported ADIF (old QSOs still unsent) flush on close.
|
||||
func (a *App) closeUploadIDs(svc extsvc.Service) []int64 {
|
||||
if a.qso == nil {
|
||||
return nil
|
||||
}
|
||||
col := uploadColumnFor(string(svc))
|
||||
if col == "" {
|
||||
return nil
|
||||
}
|
||||
cfg := a.loadExternalServices()
|
||||
|
||||
// owner is the callsign this logbook signs/uploads as. Each external
|
||||
// logbook belongs to ONE call, so in a mixed-call DB (F4BPO, F4BPO/P, TM2Q)
|
||||
// we must only sweep the QSOs that belong to it — otherwise TM2Q QSOs would
|
||||
// be signed under the F4BPO certificate, or pushed to the wrong QRZ/Club Log
|
||||
// logbook. Prefer the configured force/owner call; fall back to the active
|
||||
// profile's callsign.
|
||||
var statuses []string
|
||||
owner := ""
|
||||
switch svc {
|
||||
case extsvc.ServiceLoTW:
|
||||
statuses = cfg.LoTW.UploadFlags
|
||||
if len(statuses) == 0 {
|
||||
return nil
|
||||
}
|
||||
owner = cfg.LoTW.ForceStationCallsign
|
||||
case extsvc.ServiceQRZ:
|
||||
owner = cfg.QRZ.ForceStationCallsign
|
||||
case extsvc.ServiceClublog:
|
||||
owner = cfg.Clublog.Callsign
|
||||
}
|
||||
owner = strings.ToUpper(strings.TrimSpace(owner))
|
||||
if owner == "" && a.profiles != nil {
|
||||
if p, perr := a.profiles.Active(a.ctx); perr == nil {
|
||||
owner = strings.ToUpper(strings.TrimSpace(p.Callsign))
|
||||
}
|
||||
}
|
||||
|
||||
cands, err := a.qso.ListUploadCandidates(a.ctx, col, statuses)
|
||||
if err != nil {
|
||||
applog.Printf("extsvc: close-upload candidate scan for %s failed: %v", svc, err)
|
||||
return nil
|
||||
}
|
||||
out := make([]int64, 0, len(cands))
|
||||
skipped := 0
|
||||
for _, c := range cands {
|
||||
// Keep QSOs that belong to this logbook's call. A blank STATION_CALLSIGN
|
||||
// is assumed to be ours (it gets signed/labelled as owner on upload),
|
||||
// mirroring the per-QSO guard in extsvc.upload.
|
||||
if owner == "" || c.StationCallsign == "" || extsvc.SameBaseCall(c.StationCallsign, owner) {
|
||||
out = append(out, c.ID)
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
if skipped > 0 {
|
||||
applog.Printf("extsvc: %s close-upload skipped %d QSO(s) not matching logbook callsign %q", svc, skipped, owner)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extShouldUpload reports whether a QSO is eligible for upload to a service,
|
||||
// based on its sent status. QRZ/Club Log upload anything not yet "Y"; LoTW
|
||||
// uploads only QSOs whose lotw_sent matches the configured Upload flag
|
||||
@@ -5708,15 +5803,12 @@ func (a *App) extShouldUpload(svc extsvc.Service, id int64) bool {
|
||||
}
|
||||
return true
|
||||
case extsvc.ServiceLoTW:
|
||||
flag := "R"
|
||||
if a.settings != nil {
|
||||
if m, e := a.settings.GetMany(a.ctx, keyExtLoTWUploadFlag); e == nil {
|
||||
if v := strings.ToUpper(strings.TrimSpace(m[keyExtLoTWUploadFlag])); v == "N" || v == "R" {
|
||||
flag = v
|
||||
}
|
||||
for _, f := range a.loadExternalServices().LoTW.UploadFlags {
|
||||
if strings.EqualFold(q.LOTWSent, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.EqualFold(q.LOTWSent, flag)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user