fix(awards): the mode filter only hid rows, it did not filter the award

Selecting CW showed FT8 and SSB contacts. The filter ran client-side over
the reference list and nothing else, so a reference kept because it had
one CW contact still displayed the band cells it earned on SSB, still
counted its SSB confirmations, and opening it listed every contact
regardless of mode.

The class now narrows the log BEFORE the engine runs, so the bands, the
totals and the confirmations all describe the selected mode, and
AwardCellQSOs takes the same class. The panel cache is keyed by
"CODE|MODECLASS" — one key per award showed the previous mode's numbers
after switching.

The snapshot is cached by log revision, so this costs a matching pass and
no database read.
This commit is contained in:
2026-08-01 16:10:22 +02:00
parent b6ea07e3a3
commit 85bf0da006
5 changed files with 62 additions and 20 deletions
+34 -3
View File
@@ -3514,10 +3514,17 @@ func (a *App) GetAwards() ([]award.Result, error) {
// GetAward computes progress for a single award by code (one whole-log scan,
// matching only that award). This is what the awards UI calls when an award is
// selected, so opening the panel doesn't scan every award up front.
func (a *App) GetAward(code string) (award.Result, error) {
// GetAward computes one award. modeClass ("CW", "PHONE", "DIGI", or "" for all)
// narrows it to contacts of that class BEFORE the engine runs, so the bands, the
// counts and the confirmations all describe that mode.
//
// Filtering only the reference list was not enough: a reference kept because it
// has one CW contact still showed the band cells it earned on SSB, which is
// precisely what an operator chasing a CW award must not be told.
func (a *App) GetAward(code, modeClass string) (award.Result, error) {
for _, d := range a.awardDefs() {
if strings.EqualFold(d.Code, code) {
results, err := a.computeAwards([]award.Def{d})
results, err := a.computeAwardsForMode([]award.Def{d}, modeClass)
if err != nil {
return award.Result{}, err
}
@@ -3533,6 +3540,13 @@ func (a *App) GetAward(code string) (award.Result, error) {
// computeAwards runs the engine for the given award definitions over the whole
// log and enriches dynamic awards (totals + worked-reference names).
func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
return a.computeAwardsForMode(defs, "")
}
// computeAwardsForMode is computeAwards restricted to one mode class. The
// snapshot is shared and cached by log revision, so this costs a matching pass,
// not a database read.
func (a *App) computeAwardsForMode(defs []award.Def, modeClass string) ([]award.Result, error) {
if a.qso == nil {
return nil, fmt.Errorf("db not initialized")
}
@@ -3540,6 +3554,15 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
if err != nil {
return nil, err
}
if want := strings.ToUpper(strings.TrimSpace(modeClass)); want != "" && want != "ALL" {
kept := make([]qso.QSO, 0, len(all))
for _, q := range all {
if award.ModeClass(q.Mode) == want {
kept = append(kept, q)
}
}
all = kept
}
nameOf := func(field, ref string) string {
switch field {
case "dxcc":
@@ -3590,7 +3613,11 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
// AwardCellQSOs returns the QSOs that contribute to one award reference,
// optionally on a single band (band="" = all bands). Powers the award-grid
// cell drill-down ("show me every Canada contact on 20m").
func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) {
// AwardCellQSOs lists the contacts behind one cell. modeClass ("CW", "PHONE",
// "DIGI", or "" for all) applies the SAME filter as the reference list above it
// — without it, filtering the list to CW and then opening a reference showed the
// FT8 and SSB contacts that filter exists to hide.
func (a *App) AwardCellQSOs(code, ref, band, modeClass string) ([]qso.QSO, error) {
if a.qso == nil {
return nil, fmt.Errorf("db not initialized")
}
@@ -3608,12 +3635,16 @@ func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) {
metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
wantRef := strings.ToUpper(strings.TrimSpace(ref))
wantBand := strings.ToLower(strings.TrimSpace(band))
wantMode := strings.ToUpper(strings.TrimSpace(modeClass))
var out []qso.QSO
err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand {
return nil
}
if wantMode != "" && wantMode != "ALL" && award.ModeClass(q.Mode) != wantMode {
return nil
}
a.enrichQSOForAwards(&q)
for _, c := range award.MatchQSO(*def, metas, &q) {
if strings.ToUpper(c) == wantRef {