From 9172f504e555ffaf395841196e18ffc70785f54e Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 29 Aug 2026 00:43:30 +0200 Subject: [PATCH] fix(watchlist): judge generic DATA spots at digital-class grain; tidy the toolbar wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7.0589 falls in the band plan's generic [7.04-7.1] DATA segment — an F/H DXpedition off the standard dials — and 'DATA' is in nobody's log, so the spot read NEW MODE / Needed while the FT8 contact sat right there. A generic mode cannot be matched exactly; it is now judged at digital-class grain — worked if ANY digital mode of that call is logged on that band — against a second class-folded slot set built beside the exact one (shared when the grouping option already folds them). DXHunter answers this with a finer frequency table, which fails on exactly these off-dial expeditions; the class rule cannot. The toolbar also wraps as a whole with tighter gaps instead of dropping its last filter chip onto a lone second line. --- app.go | 18 ++++++++++---- app_watchlist.go | 30 +++++++++++++++++++++--- frontend/src/components/WatchlistTab.tsx | 4 ++-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/app.go b/app.go index a838173..abb2f2d 100644 --- a/app.go +++ b/app.go @@ -19717,10 +19717,11 @@ type SpotStatus struct { // that already holds the pointer keeps reading valid (stale-by-one-log) data // while a newer snapshot is being built. See clusterStatusMaps. type clusterStatusCache struct { - entities map[int]*qso.EntitySlot - workedCalls map[string]struct{} - workedCallSlots map[string]struct{} // nil unless the "same slot" option is on - workedCounties map[string]struct{} + entities map[int]*qso.EntitySlot + workedCalls map[string]struct{} + workedCallSlots map[string]struct{} + workedCallSlotsDig map[string]struct{} // same set, digital modes folded to DIG // nil unless the "same slot" option is on + workedCounties map[string]struct{} // callCounties holds callsign → "STATE,County" for stations already logged // with a county, so a spot shows the county the entry panel showed rather // than the one derived from the licence ZIP. See qso.CallCounties. @@ -19807,6 +19808,15 @@ func (a *App) clusterStatusMaps() *clusterStatusCache { // 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) + // And once more at digital-CLASS grain, for the spots whose mode the band + // plan can only call "DATA": a generic mode cannot be matched exactly, so it + // is judged against "any digital mode of this call on this band". When the + // grouping option is on, workedCallSlots is already at that grain. + if c.normMode != nil { + c.workedCallSlotsDig = c.workedCallSlots + } else { + c.workedCallSlotsDig, _ = a.qso.WorkedCallSlotKeys(a.ctx, qso.GroupDigitalMode) + } // 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 e7de6ee..64c9d88 100644 --- a/app_watchlist.go +++ b/app_watchlist.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "hamlog/internal/qso" "hamlog/internal/watchlist" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" @@ -99,6 +100,21 @@ func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool { } return up + "|" + b } + // A spot whose mode the band plan could only call "DATA" (an F/H DXpedition + // off the standard dials, a comment with no mode) cannot be matched exactly: + // "DATA" is in nobody's log. Such a spot is judged at digital-CLASS grain — + // worked if ANY digital mode of that call is logged on that band. Claiming + // NEW MODE because the label differs from FT8 was the reported bug. + generic := func(m string) bool { + switch strings.ToUpper(strings.TrimSpace(m)) { + case "", "DATA", "DIG", "DIGI", "DIGITAL": + return true + } + return false + } + digKey := func(call, band string) string { + return strings.ToUpper(strings.TrimSpace(call)) + "|" + strings.ToLower(strings.TrimSpace(band)) + "|DIG" + } needToday := false for _, q := range queries { if q.Contest { @@ -113,15 +129,23 @@ func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool { if err == nil { for _, r := range rows { today[slotKey(r.Callsign, r.Band, r.Mode)] = true + today[digKey(r.Callsign, r.Band)] = qso.ModeClass(r.Mode) == "DIG" || today[digKey(r.Callsign, r.Band)] } } } for i, q := range queries { - key := slotKey(q.Call, q.Band, q.Mode) if q.Contest { - out[i] = today[key] + if generic(q.Mode) { + out[i] = today[digKey(q.Call, q.Band)] + } else { + out[i] = today[slotKey(q.Call, q.Band, q.Mode)] + } + } else if generic(q.Mode) { + if idx.workedCallSlotsDig != nil { + _, out[i] = idx.workedCallSlotsDig[digKey(q.Call, q.Band)] + } } else if idx.workedCallSlots != nil { - _, out[i] = idx.workedCallSlots[key] + _, out[i] = idx.workedCallSlots[slotKey(q.Call, q.Band, q.Mode)] } } return out diff --git a/frontend/src/components/WatchlistTab.tsx b/frontend/src/components/WatchlistTab.tsx index b16607d..f17e16f 100644 --- a/frontend/src/components/WatchlistTab.tsx +++ b/frontend/src/components/WatchlistTab.tsx @@ -179,7 +179,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P // lines stretched across a 34-inch window are not readable, they are long.
{/* toolbar */} -
+
setAddCall(e.target.value)} @@ -194,7 +194,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
- setSearch(e.target.value)} />
{/* families kept together, told apart — per review of the DXHunter port,