From 29d79fb5d13add7f7f71b3a4b0674ecfb7fd20a3 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 29 Aug 2026 00:33:52 +0200 Subject: [PATCH] fix(watchlist): read the cluster's slot index, and cap the card width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version asked the alerts' worked-index — keyed by RAW mode (FT8) — with a CLASS name (DIGI). Nothing ever matched, and a DXpedition worked on five bands showed every slot as Needed. The verdict now reads the cluster's own WorkedCallSlotKeys set, built in the cached status maps (now unconditionally — it used to exist only when a display option wanted it). Same index, same mode normalisation as the grid, so the watchlist can never contradict the cluster about the same spot. The contest today-set is normalised through the same function. And the cards column is capped at a reading width instead of stretching callsign-to-badge lines across the whole window. --- app.go | 7 ++-- app_watchlist.go | 43 +++++++++++++++++------- frontend/src/components/WatchlistTab.tsx | 4 ++- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/app.go b/app.go index d1bc21f..a838173 100644 --- a/app.go +++ b/app.go @@ -19803,9 +19803,10 @@ func (a *App) clusterStatusMaps() *clusterStatusCache { // "Already worked only on the same slot" option (Settings → DX Cluster): the // WORKED-call flag then needs the SAME band and mode (digital-grouped through // the same normMode when that option is on) rather than the call anywhere. - if sameSlot || slotHighlight { - c.workedCallSlots, _ = a.qso.WorkedCallSlotKeys(a.ctx, c.normMode) - } + // Built UNCONDITIONALLY since the watchlist arrived: its per-slot verdict + // needs this set whatever the cluster's own display options say, and the + // scan already runs at cache-rebuild cadence, not per batch. + c.workedCallSlots, _ = a.qso.WorkedCallSlotKeys(a.ctx, c.normMode) // Orthogonal dimensions: worked US counties (for the ULS callsign→county // lookup) and worked POTA parks. c.workedCounties, _ = a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey) diff --git a/app_watchlist.go b/app_watchlist.go index f26821d..e7de6ee 100644 --- a/app_watchlist.go +++ b/app_watchlist.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "hamlog/internal/qso" "hamlog/internal/watchlist" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" @@ -68,18 +67,38 @@ type WatchlistSlotQuery struct { // WatchlistWorkedSlots answers a batch of slot questions for the tab. // -// One pass over today's contacts and the in-memory worked index rather than a -// query per spot: the tab refreshes on every spot burst, and a busy evening -// must not turn into a query storm. Contest entries read the TODAY set — the -// midnight-UTC reset is the query's date bound, nothing stored, nothing to -// reset. Mode is compared at CLASS grain (FT8 and FT4 are both Digital), -// matching how the cluster's own worked_slot judges a slot. +// Normal entries read the CLUSTER's own slot set — the same cached +// WorkedCallSlotKeys index that colours the grid — so the watchlist can never +// contradict the cluster about the same spot. That index normalises the mode +// exactly as the operator configured (digital grouping on → FT4 counts as FT8's +// class; off → exact mode), and the first version of this ignored that: it +// asked the alerts' raw-mode index with a CLASS name, matched nothing, and +// showed a fully-worked DXpedition as all Needed. +// +// Contest entries read TODAY's contacts instead — the midnight-UTC reset is +// the query's date bound, nothing stored, nothing to reset — normalised through +// the same function so the two answers use one grammar. func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool { out := make([]bool, len(queries)) if a.qso == nil || len(queries) == 0 { return out } - // Today's slots, only if some entry needs them. + idx := a.clusterStatusMaps() + norm := func(m string) string { + m = strings.ToUpper(strings.TrimSpace(m)) + if idx.normMode != nil { + m = idx.normMode(m) + } + return m + } + slotKey := func(call, band, mode string) string { + up := strings.ToUpper(strings.TrimSpace(call)) + b := strings.ToLower(strings.TrimSpace(band)) + if m := norm(mode); m != "" { + return up + "|" + b + "|" + m + } + return up + "|" + b + } needToday := false for _, q := range queries { if q.Contest { @@ -93,16 +112,16 @@ func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool { rows, err := a.qso.SlotsSince(a.ctx, midnight) if err == nil { for _, r := range rows { - today[wcbmKey(r.Callsign, r.Band, qso.ModeClass(r.Mode))] = true + today[slotKey(r.Callsign, r.Band, r.Mode)] = true } } } for i, q := range queries { - key := wcbmKey(q.Call, q.Band, qso.ModeClass(q.Mode)) + key := slotKey(q.Call, q.Band, q.Mode) if q.Contest { out[i] = today[key] - } else { - out[i] = a.isWorkedBandMode(q.Call, q.Band, qso.ModeClass(q.Mode)) + } else if idx.workedCallSlots != nil { + _, out[i] = idx.workedCallSlots[key] } } return out diff --git a/frontend/src/components/WatchlistTab.tsx b/frontend/src/components/WatchlistTab.tsx index b35ad4e..29972a7 100644 --- a/frontend/src/components/WatchlistTab.tsx +++ b/frontend/src/components/WatchlistTab.tsx @@ -167,7 +167,9 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P ); return ( -
+ // Capped and centred: a card is a reading surface, and callsign-to-badge + // lines stretched across a 34-inch window are not readable, they are long. +
{/* toolbar */}