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
+33 -1
View File
@@ -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() {