diff --git a/app.go b/app.go index 7dd2935..8f8b1fb 100644 --- a/app.go +++ b/app.go @@ -402,6 +402,7 @@ const ( keyExtLoTWTQSLPath = "extsvc.lotw.tqsl_path" keyExtLoTWStationLoc = "extsvc.lotw.station_location" + keyExtLoTWAllCalls = "extsvc.lotw.download_all_calls" // download confirmations for EVERY call on the account, not just this profile's 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" // legacy single flag (migrated to upload_flags) @@ -12096,6 +12097,17 @@ func manualRefFor(existing, code string) string { return "" } +// GetLoTWDownloadAllCalls reports whether the LoTW download ignores the +// profile's own call and pulls every callsign on the account. +func (a *App) GetLoTWDownloadAllCalls() bool { + return a.settingOr(keyExtLoTWAllCalls, "") == "1" +} + +// SetLoTWDownloadAllCalls stores that choice. +func (a *App) SetLoTWDownloadAllCalls(on bool) { + a.setSetting(keyExtLoTWAllCalls, map[bool]string{true: "1", false: "0"}[on]) +} + // DownloadConfirmations pulls confirmed QSOs from a service and updates the // matching local QSOs' received status. LoTW only for now (the canonical // confirmation system); runs in the background emitting the same @@ -12167,6 +12179,16 @@ func (a *App) runDownloadConfirmations(ctx context.Context, svc extsvc.Service, case extsvc.ServiceLoTW: sinceDate := resolveSince(keyExtLoTWLastDownload) ownCall := a.uploadOwnerCall(extsvc.ServiceLoTW) + // A LoTW account holds every call its owner operates — F4BPO, F4BPO/P, + // TM2Q — and the download is normally scoped to the profile's own call so + // one profile does not pull another's confirmations. That scope also + // silently hides them: a QSO made as F4BPO/P is confirmed at LoTW and can + // never be downloaded from the F4BPO profile, so it stays unconfirmed here + // for good while the ARRL counts it. Off by default, because the scope is + // right for anyone whose profiles are separate stations. + if a.settingOr(keyExtLoTWAllCalls, "") == "1" { + ownCall = "" + } callLabel := ownCall if callLabel == "" { callLabel = "all callsigns" diff --git a/changelog.json b/changelog.json index bf68c9f..2576e30 100644 --- a/changelog.json +++ b/changelog.json @@ -9,7 +9,8 @@ "Elecraft KPA: the status-bar chip now shows the amplifier as connected and switches it between OPERATE and STANDBY like the other brands, and an offline KPA no longer calls itself an Acom.", "Cluster: a SOTA column, read from the summit reference the SOTA feeds put in the spot comment. Clicking the spot fills the QSO’s SOTA award reference, as a POTA spot already did. Turn the column on in Columns.", "Awards: a \"Slots to confirm\" filter and a running count beside the reference total, so the gap between worked and confirmed band-slots — the Challenge difference — can be seen reference by reference instead of only as two numbers.", - "QSL Manager: a QRZ button next to the Paper QSL search, opening the callsign on QRZ.com." + "QSL Manager: a QRZ button next to the Paper QSL search, opening the callsign on QRZ.com.", + "LoTW: an \"All my callsigns\" option beside the download. The download is scoped to the profile’s own callsign, so a QSO made as a portable or contest call was confirmed at LoTW and never marked here." ], "fr": [ "Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change.", @@ -18,7 +19,8 @@ "Elecraft KPA : la pastille de la barre d'état montre enfin l'amplificateur comme connecté et bascule OPERATE / STANDBY comme les autres marques, et un KPA hors ligne ne s'annonce plus comme un Acom.", "Cluster : une colonne SOTA, lue dans la référence de sommet que les flux SOTA mettent dans le commentaire du spot. Cliquer le spot remplit la référence SOTA du QSO, comme le faisait déjà un spot POTA. Colonne à activer dans Colonnes.", "Awards : un filtre « Slots à confirmer » et un compteur à côté du total de références, pour voir l'écart entre créneaux contactés et confirmés — la différence du Challenge — référence par référence et non plus seulement en deux chiffres.", - "Gestionnaire QSL : un bouton QRZ à côté de la recherche QSL papier, qui ouvre l'indicatif sur QRZ.com." + "Gestionnaire QSL : un bouton QRZ à côté de la recherche QSL papier, qui ouvre l'indicatif sur QRZ.com.", + "LoTW : une option « Tous mes indicatifs » à côté du téléchargement. Celui-ci est limité à l'indicatif du profil, si bien qu'un QSO fait sous un indicatif portable ou de contest était confirmé chez LoTW sans jamais être marqué ici." ] }, { diff --git a/frontend/src/components/QSLManagerModal.tsx b/frontend/src/components/QSLManagerModal.tsx index af75346..dbae2b8 100644 --- a/frontend/src/components/QSLManagerModal.tsx +++ b/frontend/src/components/QSLManagerModal.tsx @@ -8,7 +8,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; -import { OpenExternalURL, FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, CancelConfirmations, ImportHamlogConfirmations, ExportHamlogUnmatched, OpenADIFFile, SaveADIFFile, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign, GetSlotStats } from '../../wailsjs/go/main/App'; +import { GetLoTWDownloadAllCalls, SetLoTWDownloadAllCalls, OpenExternalURL, FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, CancelConfirmations, ImportHamlogConfirmations, ExportHamlogUnmatched, OpenADIFFile, SaveADIFFile, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign, GetSlotStats } from '../../wailsjs/go/main/App'; import { Input } from '@/components/ui/input'; import { RecentQSOsGrid } from '@/components/RecentQSOsGrid'; import { EventsOn } from '../../wailsjs/runtime/runtime'; @@ -255,6 +255,9 @@ export function QSLManagerPanel({ onEditQSO, actions, paperRequest }: { const [searching, setSearching] = useState(false); const [error, setError] = useState(''); const [addNotFound, setAddNotFound] = useState(false); + // LoTW only: pull the whole account rather than this profile's callsign. + const [lotwAllCalls, setLotwAllCalls] = useState(false); + useEffect(() => { GetLoTWDownloadAllCalls().then((v: boolean) => setLotwAllCalls(!!v)).catch(() => {}); }, []); // Download date window: 'last' = incremental since last pull, 'date' = from a // chosen date, 'all' = everything. const [sinceMode, setSinceMode] = useState<'last' | 'date' | 'all'>('last'); @@ -748,6 +751,12 @@ export function QSLManagerPanel({ onEditQSO, actions, paperRequest }: { setAddNotFound(!!c)} /> {t('qslm.addNotFound')} + {service === 'lotw' && ( + + )} )}