From 642ed358c2ecd3c1e5d53f8a23153d647957470a Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Sat, 8 Aug 2026 23:24:23 +0200 Subject: [PATCH] feat(worked): fold portable callsigns into the worked-before history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing RK3DWA found nothing while RK3DWA/3 found 21 QSOs, so a station's history was only visible if you happened to type the exact form it had been logged under — and an operator who worked it as /0, /P or /MM saw none of it. The other RDA tools and Log4OM fold these together; this does too. The predicate strips the suffix from what was typed and matches "call = base OR call LIKE base/%", so it works from either end: the base call finds the portable QSOs and a portable call finds the plain ones. Deliberately not a bare prefix LIKE 'RK3DWA%', which would also match RK3DWAB — a different station. The '/' is what makes it the same operator. Settings -> General to turn it off. Default ON, hence the inverted storage: an existing install has no key, and reading that as OFF would leave everyone with the behaviour we were asked to change. Contest dupe checking is untouched — it runs through ContestDupe, a separate binding, and stays an exact match as a contest requires. --- app.go | 34 +++++++++++++++++++- changelog.json | 10 ++++++ frontend/src/components/SettingsModal.tsx | 11 +++++++ frontend/src/lib/i18n.tsx | 2 ++ frontend/wailsjs/go/main/App.d.ts | 4 +++ frontend/wailsjs/go/main/App.js | 8 +++++ internal/qso/callmatch_test.go | 34 ++++++++++++++++++++ internal/qso/qso.go | 39 +++++++++++++++++++---- 8 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 internal/qso/callmatch_test.go diff --git a/app.go b/app.go index 7c0a9da..837f909 100644 --- a/app.go +++ b/app.go @@ -293,6 +293,12 @@ const ( keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on + // Worked-before: fold an operator's portable forms (X, X/3, X/P) together. + // Stored inverted — "0" means OFF — so the feature is ON for an existing + // install that has never seen the key, which is the behaviour operators asked + // for. See GetWorkedCallVariants. + keyWorkedCallVariants = "worked.call_variants" + keyBackupEnabled = "backup.enabled" keyBackupFolder = "backup.folder" keyBackupRotation = "backup.rotation" @@ -6091,6 +6097,31 @@ func (a *App) bulkSetFrequency(ids []int64, value string) (int64, error) { return n, nil } +// GetWorkedCallVariants reports whether "worked before" folds an operator's +// portable forms together (RK3DWA ↔ RK3DWA/3 ↔ RK3DWA/P). +// +// Defaults to ON, hence the inverted storage: an install that predates the +// setting has no key at all, and reading that as OFF would leave every existing +// operator with the old narrow behaviour they asked us to change. +func (a *App) GetWorkedCallVariants() (bool, error) { + if a.settings == nil { + return true, fmt.Errorf("db not initialized") + } + v, err := a.settings.Get(a.ctx, keyWorkedCallVariants) + if err != nil { + return true, err + } + return v != "0", nil +} + +// SetWorkedCallVariants persists the toggle. +func (a *App) SetWorkedCallVariants(on bool) error { + if a.settings == nil { + return fmt.Errorf("db not initialized") + } + return a.settings.Set(a.ctx, keyWorkedCallVariants, boolStr(on)) +} + // WorkedBefore returns prior contacts with the given callsign at both // call and DXCC granularity. Pass dxccHint=0 when unknown — the function // will infer it from past QSOs with the same call when possible. @@ -6107,7 +6138,8 @@ func (a *App) WorkedBefore(callsign string, dxccHint int) (qso.WorkedBefore, err dxccHint = dxcc.EntityDXCC(m.Entity.Name) } } - wb, err := a.qso.WorkedBefore(a.ctx, callsign, dxccHint) + variants, _ := a.GetWorkedCallVariants() + wb, err := a.qso.WorkedBefore(a.ctx, callsign, dxccHint, variants) // Attach the ClubLog Most Wanted rank for this entity (opt-in) so the entry // matrix can show it next to the country name. if err == nil && wb.DXCC > 0 && a.clublogMW != nil && a.clublogMostWantedEnabled() { diff --git a/changelog.json b/changelog.json index f31bec5..b6e3d5b 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "0.24.0", + "date": "", + "en": [ + "Worked before: an operator's portable callsigns now count as the same station. Typing RK3DWA finds the RK3DWA/3, /P and /MM contacts too, and the other way round — before, the history only appeared if you typed the exact form. Can be turned off in Settings → General; contest dupe checking is unaffected and stays exact." + ], + "fr": [ + "Déjà contacté : les indicatifs portables d'un opérateur comptent désormais comme la même station. Taper RK3DWA retrouve aussi les QSO en RK3DWA/3, /P et /MM, et inversement — avant, l'historique n'apparaissait que si tu tapais la forme exacte. Désactivable dans Réglages → Général ; le contrôle de doublon en concours n'est pas touché et reste strict." + ] + }, { "version": "0.23.9", "date": "", diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 39e9274..45134ac 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -27,6 +27,7 @@ import { AudioStartTX, AudioStopTX, AudioTXActive, ListClusterServers, SaveClusterServer, DeleteClusterServer, GetClusterAutoConnect, SetClusterAutoConnect, GetSelfSpotSettings, SaveSelfSpotSettings, + GetWorkedCallVariants, SetWorkedCallVariants, ConnectClusterServer, DisconnectClusterServer, ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder, @@ -1524,6 +1525,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan const [clusterServers, setClusterServers] = useState([]); const [clusterAutoConnect, setClusterAutoConnectState] = useState(false); + // Defaults to true so the checkbox matches the backend before the read lands. + const [workedVariants, setWorkedVariants] = useState(true); // Self-spot. SELF_SPOT_MIN_MIN mirrors the backend floor — the input clamps on // blur, not per keystroke, or typing "10" would be rewritten to "5" the moment // the "1" landed and the field would fight the operator. @@ -1632,6 +1635,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan try { setAudioCfg(await GetAudioSettings() as any); } catch {} try { setEmailCfg(await GetEmailSettings() as any); } catch {} try { setEqslCfg(await QSLGetEmailTemplates() as any); } catch {} + try { setWorkedVariants(await GetWorkedCallVariants()); } catch {} reloadAudioDevices(); reloadDvk(); } catch (e: any) { @@ -5666,6 +5670,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }} /> {t('gen.autofocusWB')} + {/* Backend setting, not a UI pref: the fold happens in the SQL. Saved + instantly like the rest of this panel. */} +