From cc0f9ffc64ba8abb4e900dbdb7b7ffe05cb97599 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 18 Jun 2026 12:34:53 +0200 Subject: [PATCH] fix: download lotw only for current callsign in case of mixed logs (tm2q & f4bpo in same log) --- app.go | 77 +++++++++++++-------- frontend/src/components/QSLManagerModal.tsx | 21 +++++- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ internal/extsvc/lotw.go | 10 ++- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/app.go b/app.go index 8dc14ea..755f474 100644 --- a/app.go +++ b/app.go @@ -5243,12 +5243,17 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe switch svc { case extsvc.ServiceLoTW: sinceDate := resolveSince(keyExtLoTWLastDownload) - if sinceDate != "" { - emit("Downloading LoTW confirmations received since " + sinceDate + "…") - } else { - emit("Downloading all LoTW confirmations…") + ownCall := a.uploadOwnerCall(extsvc.ServiceLoTW) + callLabel := ownCall + if callLabel == "" { + callLabel = "all callsigns" } - adifText, err := extsvc.DownloadLoTWConfirmations(ctx, nil, cfg.LoTW, sinceDate) + if sinceDate != "" { + emit(fmt.Sprintf("Downloading LoTW confirmations for %s received since %s…", callLabel, sinceDate)) + } else { + emit(fmt.Sprintf("Downloading all LoTW confirmations for %s…", callLabel)) + } + adifText, err := extsvc.DownloadLoTWConfirmations(ctx, nil, cfg.LoTW, sinceDate, ownCall) if err != nil { emit("Download failed: " + err.Error()) done(matched, total) @@ -5713,34 +5718,14 @@ 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 - } +// uploadOwnerCall returns the callsign OpsLog signs/uploads/downloads as for a +// service in the active profile: the configured Force/owner callsign, else the +// active profile's callsign. "" when nothing is known (don't scope by call). +func (a *App) uploadOwnerCall(svc extsvc.Service) string { 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 @@ -5753,6 +5738,40 @@ func (a *App) closeUploadIDs(svc extsvc.Service) []int64 { owner = strings.ToUpper(strings.TrimSpace(p.Callsign)) } } + return owner +} + +// UploadCallsign exposes uploadOwnerCall to the UI so the QSL Manager can show +// which of the operator's callsigns a download/upload targets in this profile. +func (a *App) UploadCallsign(service string) string { + return a.uploadOwnerCall(extsvc.Service(service)) +} + +// 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 + } + // 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. + var statuses []string + if svc == extsvc.ServiceLoTW { + statuses = a.loadExternalServices().LoTW.UploadFlags + if len(statuses) == 0 { + return nil + } + } + owner := a.uploadOwnerCall(svc) cands, err := a.qso.ListUploadCandidates(a.ctx, col, statuses) if err != nil { diff --git a/frontend/src/components/QSLManagerModal.tsx b/frontend/src/components/QSLManagerModal.tsx index 9f6b51c..03e928e 100644 --- a/frontend/src/components/QSLManagerModal.tsx +++ b/frontend/src/components/QSLManagerModal.tsx @@ -6,7 +6,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; -import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL } from '../../wailsjs/go/main/App'; +import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign } from '../../wailsjs/go/main/App'; import { Input } from '@/components/ui/input'; import { EventsOn } from '../../wailsjs/runtime/runtime'; @@ -77,6 +77,14 @@ export function fmtQslDate(s?: string): string { // and download confirmations, while the rest of the app stays usable. export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => void } = {}) { const [service, setService] = useState('lotw'); + // The callsign this profile signs/uploads/downloads as for the selected + // service (Force station callsign, else the profile call). Shown so the user + // knows WHICH of their calls a download/upload targets in a mixed-call log. + const [uploadCall, setUploadCall] = useState(''); + useEffect(() => { + if (service === 'pota' || service === 'paper') { setUploadCall(''); return; } + UploadCallsign(service).then((c) => setUploadCall(c || '')).catch(() => setUploadCall('')); + }, [service]); const [potaSyncing, setPotaSyncing] = useState(false); const [potaRes, setPotaRes] = useState(null); const [potaErr, setPotaErr] = useState(''); @@ -251,6 +259,17 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi {SERVICES.map((s) => {s.label})} + {uploadCall && ( +
+ + + {uploadCall} + +
+ )} {service === 'pota' ? ( <>