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. */} +