fix(chase): the unconfirmed flag survives the next QSO

Reported as 'South Africa shows NEW BAND although I have worked 15m'.
The verdict was right — his hunt counts only LoTW and paper QSL, and no
15m contact is confirmed by either — but the badge drew SOLID, saying
'never worked here' about a band with four contacts in the log.

Proved with his own data: the confirmed ledger holds 10/12/17/20/30/40/80
and not 15, the all-QSO ledger holds 15m FT8 four times, and the backend
sets UnconfStatus correctly for exactly that case (new test, end to end
through ClusterSpotStatuses).

The flag died in the frontend. FOUR hand-written copies of the same
backend-result mapping, and they had drifted: the two that RE-fetch —
after a QSO is logged, and when a pane becomes visible — rebuilt each
entry without the unconf_* flags, so the first refresh after any contact
turned every dimmed badge solid for good. None of the four ever copied
grid_state either, so a known-but-unconfirmed square never dimmed at all.
One mapper now, used by all four.
This commit is contained in:
2026-09-01 11:56:20 +02:00
parent ab8ecd65fe
commit f39bda110a
4 changed files with 187 additions and 52 deletions
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"path/filepath"
"testing"
"time"
"hamlog/internal/db"
"hamlog/internal/dxcc"
"hamlog/internal/qso"
"hamlog/internal/settings"
)
// The operator's exact case, end to end through the real verdict path.
//
// South Africa is confirmed (LoTW) on 20m and worked-but-unconfirmed on 15m,
// with the hunt set to "new + unconfirmed" over LoTW and paper QSL. A 15m FT8
// decode must come back NEW BAND — that is what the settings ask — and carry
// the UNCONFIRMED flag, which is what draws the badge as a missing QSL rather
// than a band never worked.
func TestClusterStatusFlagsUnconfirmedBand(t *testing.T) {
dir := t.TempDir()
conn, err := db.Open(filepath.Join(dir, "log.db"))
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { conn.Close() })
a := &App{ctx: context.Background(), qso: qso.NewRepo(conn), settings: settings.NewStore(conn)}
a.settingsScoped.Store(true)
if err := a.settings.Set(a.ctx, keyChaseMode, "new_unconfirmed"); err != nil {
t.Fatalf("set chase mode: %v", err)
}
if err := a.settings.Set(a.ctx, keyChaseConfirm, "lotw,card"); err != nil {
t.Fatalf("set chase sources: %v", err)
}
// The real prefix table, so ZS4AW resolves the way it does in the app.
a.dxcc = dxcc.NewManager(filepath.Join("build", "bin", "data"))
if err := a.dxcc.LoadFromDisk(); err != nil {
t.Skipf("cty.dat not available here: %v", err)
}
za := 462
add := func(call, band, mode, lotw string) {
hz := int64(21074000)
if _, err := a.qso.Add(a.ctx, qso.QSO{
Callsign: call, Band: band, Mode: mode, FreqHz: &hz,
QSODate: time.Now().UTC().Add(-72 * time.Hour),
DXCC: &za, Country: "South Africa", LOTWRcvd: lotw,
}); err != nil {
t.Fatalf("add %s: %v", call, err)
}
}
add("ZS1AAA", "20m", "FT8", "Y") // the entity IS confirmed, elsewhere
add("ZS6GAV", "15m", "FT8", "N") // 15m worked, never confirmed
add("ZS4AW", "15m", "FT8", "N")
got := a.ClusterSpotStatuses([]SpotQuery{{Call: "ZS4AW", Band: "15m", Mode: "FT8"}})
if len(got) != 1 {
t.Fatalf("got %d results", len(got))
}
if got[0].Status != "new-band" {
t.Fatalf("status = %q, want new-band", got[0].Status)
}
if !got[0].UnconfStatus {
t.Error("UnconfStatus is false — the badge draws solid, as if 15m had never been worked")
}
}