feat(worked): fold portable callsigns into the worked-before history

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.
This commit is contained in:
2026-08-08 23:24:23 +02:00
parent 1d8cd25205
commit 642ed358c2
8 changed files with 134 additions and 8 deletions
+11
View File
@@ -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<ClusterServer[]>([]);
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
<Checkbox checked={autofocusWB} onCheckedChange={(c) => { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }} />
{t('gen.autofocusWB')}
</label>
{/* Backend setting, not a UI pref: the fold happens in the SQL. Saved
instantly like the rest of this panel. */}
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={workedVariants}
onCheckedChange={(c) => { const v = !!c; setWorkedVariants(v); SetWorkedCallVariants(v).catch((e: any) => setErr(String(e?.message ?? e))); }} />
{t('gen.workedVariants')} <span className="text-xs text-muted-foreground">{t('gen.workedVariantsHint')}</span>
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={showBeamMap} onCheckedChange={(c) => { const v = !!c; setShowBeamMap(v); writeUiPref('opslog.showBeamOnMap', v ? '1' : '0'); }} />
{t('gen.showBeam')}
+2
View File
@@ -186,6 +186,7 @@ const en: Dict = {
// General panel
'gen.hint': 'App behaviour (saved instantly).',
'gen.autofocusWB': 'Auto-focus "Worked before" for known stations',
'gen.workedVariants': '"Worked before" folds portable callsigns together', 'gen.workedVariantsHint': '(RK3DWA also finds RK3DWA/3, /P, /MM — and the other way round. Contest dupe checking stays exact.)',
'gen.showBeam': 'Show the antenna beam heading on the Main map',
'gen.startEqEnd': 'QSO start time = end time', 'gen.startEqEndHint': '(matches LoTW when you call a while)',
'gen.showQsoRate': 'Show QSO rate in the header', 'gen.showQsoRateHint': '(QSOs/hour, projected from the last 10 / 60 min)',
@@ -607,6 +608,7 @@ const fr: Dict = {
'relayauto.from': 'de', 'relayauto.to': 'à',
'gen.hint': 'Comportement de l\'application (enregistré immédiatement).',
'gen.autofocusWB': 'Focus auto sur « Déjà contacté » pour les stations connues',
'gen.workedVariants': '« Déjà contacté » regroupe les indicatifs portables', 'gen.workedVariantsHint': '(RK3DWA trouve aussi RK3DWA/3, /P, /MM — et inversement. Le contrôle de doublon en concours reste strict.)',
'gen.showBeam': 'Afficher le cap de l\'antenne sur la carte principale',
'gen.startEqEnd': 'Heure de début du QSO = heure de fin', 'gen.startEqEndHint': '(correspond à LoTW quand tu appelles un moment)',
'gen.showQsoRate': 'Afficher le rythme QSO dans la barre du haut', 'gen.showQsoRateHint': '(QSO/heure, projeté sur les 10 / 60 dernières min)',
+4
View File
@@ -515,6 +515,8 @@ export function GetWinkeyerSettings():Promise<main.WinkeyerSettings>;
export function GetWinkeyerStatus():Promise<winkeyer.Status>;
export function GetWorkedCallVariants():Promise<boolean>;
export function GetYaesuState():Promise<cat.YaesuTXState>;
export function HasBuiltinReferences(arg1:string):Promise<boolean>;
@@ -967,6 +969,8 @@ export function SetUltrabeamDirection(arg1:number):Promise<void>;
export function SetWinkeyerTrace(arg1:boolean):Promise<void>;
export function SetWorkedCallVariants(arg1:boolean):Promise<void>;
export function SetYaesuAFGain(arg1:number):Promise<void>;
export function SetYaesuAGC(arg1:string):Promise<void>;
+8
View File
@@ -978,6 +978,10 @@ export function GetWinkeyerStatus() {
return window['go']['main']['App']['GetWinkeyerStatus']();
}
export function GetWorkedCallVariants() {
return window['go']['main']['App']['GetWorkedCallVariants']();
}
export function GetYaesuState() {
return window['go']['main']['App']['GetYaesuState']();
}
@@ -1882,6 +1886,10 @@ export function SetWinkeyerTrace(arg1) {
return window['go']['main']['App']['SetWinkeyerTrace'](arg1);
}
export function SetWorkedCallVariants(arg1) {
return window['go']['main']['App']['SetWorkedCallVariants'](arg1);
}
export function SetYaesuAFGain(arg1) {
return window['go']['main']['App']['SetYaesuAFGain'](arg1);
}