fix: bug sending LoTW on close
This commit is contained in:
@@ -69,7 +69,7 @@ type ServiceConfig struct {
|
||||
TQSLPath string `json:"tqsl_path"` // LoTW: path to tqsl.exe
|
||||
StationLocation string `json:"station_location"` // LoTW: TQSL Station Location name
|
||||
KeyPassword string `json:"key_password"` // LoTW: certificate private-key password (optional)
|
||||
UploadFlag string `json:"upload_flag"` // LoTW: sent status that means "ready to upload" — "N" or "R"
|
||||
UploadFlags []string `json:"upload_flags"` // LoTW: set of lotw_sent values that mean "ready to upload" — any of "N"/"R"
|
||||
WriteLog bool `json:"write_log"` // LoTW: pass -t to write a TQSL diagnostic log
|
||||
AutoUpload bool `json:"auto_upload"`
|
||||
UploadMode UploadMode `json:"upload_mode"`
|
||||
@@ -84,13 +84,19 @@ func (c ServiceConfig) normalised() ServiceConfig {
|
||||
c.ForceStationCallsign = strings.ToUpper(strings.TrimSpace(c.ForceStationCallsign))
|
||||
c.TQSLPath = strings.TrimSpace(c.TQSLPath)
|
||||
c.StationLocation = strings.TrimSpace(c.StationLocation)
|
||||
// Upload flag is the LoTW sent-status that marks a QSO ready to upload.
|
||||
// Only "N" (no) and "R" (requested) are valid; default to "R".
|
||||
if uf := strings.ToUpper(strings.TrimSpace(c.UploadFlag)); uf == "N" || uf == "R" {
|
||||
c.UploadFlag = uf
|
||||
} else {
|
||||
c.UploadFlag = "R"
|
||||
// Upload flags are the LoTW sent-statuses that mark a QSO ready to upload.
|
||||
// Only "N" (no) and "R" (requested) are valid; clean + dedupe, no default
|
||||
// here (the caller injects N+R when nothing is configured).
|
||||
var flags []string
|
||||
seen := map[string]bool{}
|
||||
for _, f := range c.UploadFlags {
|
||||
f = strings.ToUpper(strings.TrimSpace(f))
|
||||
if (f == "N" || f == "R") && !seen[f] {
|
||||
seen[f] = true
|
||||
flags = append(flags, f)
|
||||
}
|
||||
}
|
||||
c.UploadFlags = flags
|
||||
switch c.UploadMode {
|
||||
case ModeDelayed, ModeOnClose:
|
||||
// keep
|
||||
|
||||
+65
-38
@@ -33,6 +33,11 @@ func sameBaseCall(a, b string) bool {
|
||||
return baseCall(a) == baseCall(b)
|
||||
}
|
||||
|
||||
// SameBaseCall is the exported form of sameBaseCall, so the host app can apply
|
||||
// the same "same operator?" rule when filtering an on-close upload batch by the
|
||||
// active logbook's callsign.
|
||||
func SameBaseCall(a, b string) bool { return sameBaseCall(a, b) }
|
||||
|
||||
// Deps are the host-app callbacks the Manager needs. Keeping them as
|
||||
// function fields decouples extsvc from the qso/adif/settings packages and
|
||||
// keeps the upload-scheduling logic testable.
|
||||
@@ -62,6 +67,13 @@ type Deps struct {
|
||||
// option would otherwise silently relabel it). "" → no station call known.
|
||||
StationCallOf func(id int64) string
|
||||
|
||||
// CloseUploadIDs returns the QSO ids to upload for a service when the app
|
||||
// closes — scanning the WHOLE logbook, not just this session: LoTW returns
|
||||
// rows whose lotw_sent matches the configured status set; QRZ/Club Log
|
||||
// return anything not yet "Y". This is what makes an imported ADIF (old
|
||||
// QSOs still marked unsent) upload on close. nil → nothing to do.
|
||||
CloseUploadIDs func(svc Service) []int64
|
||||
|
||||
// Logf is an optional diagnostic logger.
|
||||
Logf func(format string, args ...any)
|
||||
}
|
||||
@@ -72,10 +84,9 @@ type Deps struct {
|
||||
type Manager struct {
|
||||
deps Deps
|
||||
|
||||
mu sync.Mutex
|
||||
cfg ExternalServices
|
||||
rnd *rand.Rand
|
||||
pending map[Service][]int64 // QSO ids queued for ModeOnClose upload
|
||||
mu sync.Mutex
|
||||
cfg ExternalServices
|
||||
rnd *rand.Rand
|
||||
}
|
||||
|
||||
func NewManager(deps Deps) *Manager {
|
||||
@@ -86,8 +97,7 @@ func NewManager(deps Deps) *Manager {
|
||||
deps: deps,
|
||||
// Seeded from the clock; the delay only needs to be unpredictable
|
||||
// enough to spread bursts, not cryptographically random.
|
||||
rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
pending: map[Service][]int64{},
|
||||
rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +113,8 @@ func (m *Manager) SetConfig(cfg ExternalServices) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cfg.QRZ = cfg.QRZ.normalised()
|
||||
cfg.Clublog = cfg.Clublog.normalised()
|
||||
cfg.LoTW = cfg.LoTW.normalised()
|
||||
m.cfg = cfg
|
||||
}
|
||||
|
||||
@@ -145,11 +157,9 @@ func (m *Manager) OnQSOLogged(id int64) {
|
||||
// app-close batch, or schedule an immediate / delayed upload.
|
||||
func (m *Manager) route(svc Service, id int64, cfg ServiceConfig) {
|
||||
if cfg.UploadMode == ModeOnClose {
|
||||
m.mu.Lock()
|
||||
m.pending[svc] = append(m.pending[svc], id)
|
||||
n := len(m.pending[svc])
|
||||
m.mu.Unlock()
|
||||
m.logf("extsvc: %s queued QSO %d for on-close upload (%d pending)", svc, id, n)
|
||||
// Nothing to queue: on-close upload sweeps the whole logbook from the
|
||||
// database at shutdown (see FlushOnClose), so this QSO is picked up by
|
||||
// its sent-status then — no in-memory tracking needed.
|
||||
return
|
||||
}
|
||||
m.scheduleUpload(svc, id, cfg)
|
||||
@@ -166,47 +176,64 @@ func (m *Manager) scheduleUpload(svc Service, id int64, cfg ServiceConfig) {
|
||||
go m.upload(svc, id, cfg)
|
||||
}
|
||||
|
||||
// PendingCount returns how many QSOs are queued for on-close upload across
|
||||
// all services. The shutdown sequence uses it to decide whether to show the
|
||||
// upload step.
|
||||
func (m *Manager) PendingCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// onCloseServices returns the services configured for on-close auto-upload,
|
||||
// with the minimum credentials to actually run.
|
||||
func (m *Manager) onCloseServices() []Service {
|
||||
cfg := m.Config()
|
||||
var out []Service
|
||||
if q := cfg.QRZ; q.AutoUpload && q.UploadMode == ModeOnClose && q.APIKey != "" {
|
||||
out = append(out, ServiceQRZ)
|
||||
}
|
||||
if c := cfg.Clublog; c.AutoUpload && c.UploadMode == ModeOnClose && c.Email != "" && c.Password != "" {
|
||||
out = append(out, ServiceClublog)
|
||||
}
|
||||
if l := cfg.LoTW; l.AutoUpload && l.UploadMode == ModeOnClose && l.TQSLPath != "" && l.StationLocation != "" {
|
||||
out = append(out, ServiceLoTW)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CloseUploadCount returns how many QSOs across the whole logbook would be
|
||||
// uploaded at app close (sum over every on-close service). The shutdown
|
||||
// sequence uses it to decide whether to show the upload step and its label.
|
||||
func (m *Manager) CloseUploadCount() int {
|
||||
if m.deps.CloseUploadIDs == nil {
|
||||
return 0
|
||||
}
|
||||
n := 0
|
||||
for _, ids := range m.pending {
|
||||
n += len(ids)
|
||||
for _, svc := range m.onCloseServices() {
|
||||
n += len(m.deps.CloseUploadIDs(svc))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// FlushOnClose uploads every queued QSO. Called from the shutdown sequence.
|
||||
// QRZ/Club Log go one-by-one (fast HTTP); LoTW is signed and uploaded as a
|
||||
// single TQSL batch. Returns the number of QSOs uploaded successfully.
|
||||
// FlushOnClose uploads every QSO due for an on-close push, scanning the whole
|
||||
// logbook (not just this session). Called from the shutdown sequence. QRZ/Club
|
||||
// Log go one-by-one (fast HTTP); LoTW is signed and uploaded as a single TQSL
|
||||
// batch. Returns the number of QSOs uploaded successfully.
|
||||
func (m *Manager) FlushOnClose() int {
|
||||
m.mu.Lock()
|
||||
pending := m.pending
|
||||
m.pending = map[Service][]int64{}
|
||||
cfg := m.cfg
|
||||
m.mu.Unlock()
|
||||
|
||||
if m.deps.CloseUploadIDs == nil {
|
||||
return 0
|
||||
}
|
||||
cfg := m.Config()
|
||||
uploaded := 0
|
||||
for svc, ids := range pending {
|
||||
for _, svc := range m.onCloseServices() {
|
||||
ids := m.deps.CloseUploadIDs(svc)
|
||||
if len(ids) == 0 {
|
||||
continue
|
||||
}
|
||||
switch svc {
|
||||
case ServiceLoTW:
|
||||
uploaded += m.flushLoTWBatch(ids, cfg.LoTW)
|
||||
default:
|
||||
var sc ServiceConfig
|
||||
switch svc {
|
||||
case ServiceQRZ:
|
||||
sc = cfg.QRZ
|
||||
case ServiceClublog:
|
||||
sc = cfg.Clublog
|
||||
}
|
||||
case ServiceQRZ:
|
||||
for _, id := range ids {
|
||||
if m.upload(svc, id, sc) {
|
||||
if m.upload(svc, id, cfg.QRZ) {
|
||||
uploaded++
|
||||
}
|
||||
}
|
||||
case ServiceClublog:
|
||||
for _, id := range ids {
|
||||
if m.upload(svc, id, cfg.Clublog) {
|
||||
uploaded++
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user