fix(watchlist): judge generic DATA spots at digital-class grain; tidy the toolbar wrap
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.
This commit is contained in:
@@ -19719,7 +19719,8 @@ type SpotStatus struct {
|
|||||||
type clusterStatusCache struct {
|
type clusterStatusCache struct {
|
||||||
entities map[int]*qso.EntitySlot
|
entities map[int]*qso.EntitySlot
|
||||||
workedCalls map[string]struct{}
|
workedCalls map[string]struct{}
|
||||||
workedCallSlots map[string]struct{} // nil unless the "same slot" option is on
|
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{}
|
workedCounties map[string]struct{}
|
||||||
// callCounties holds callsign → "STATE,County" for stations already logged
|
// callCounties holds callsign → "STATE,County" for stations already logged
|
||||||
// with a county, so a spot shows the county the entry panel showed rather
|
// with a county, so a spot shows the county the entry panel showed rather
|
||||||
@@ -19807,6 +19808,15 @@ func (a *App) clusterStatusMaps() *clusterStatusCache {
|
|||||||
// needs this set whatever the cluster's own display options say, and the
|
// needs this set whatever the cluster's own display options say, and the
|
||||||
// scan already runs at cache-rebuild cadence, not per batch.
|
// scan already runs at cache-rebuild cadence, not per batch.
|
||||||
c.workedCallSlots, _ = a.qso.WorkedCallSlotKeys(a.ctx, c.normMode)
|
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
|
// Orthogonal dimensions: worked US counties (for the ULS callsign→county
|
||||||
// lookup) and worked POTA parks.
|
// lookup) and worked POTA parks.
|
||||||
c.workedCounties, _ = a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey)
|
c.workedCounties, _ = a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey)
|
||||||
|
|||||||
+27
-3
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hamlog/internal/qso"
|
||||||
"hamlog/internal/watchlist"
|
"hamlog/internal/watchlist"
|
||||||
|
|
||||||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
@@ -99,6 +100,21 @@ func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool {
|
|||||||
}
|
}
|
||||||
return up + "|" + b
|
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
|
needToday := false
|
||||||
for _, q := range queries {
|
for _, q := range queries {
|
||||||
if q.Contest {
|
if q.Contest {
|
||||||
@@ -113,15 +129,23 @@ func (a *App) WatchlistWorkedSlots(queries []WatchlistSlotQuery) []bool {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
today[slotKey(r.Callsign, r.Band, r.Mode)] = true
|
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 {
|
for i, q := range queries {
|
||||||
key := slotKey(q.Call, q.Band, q.Mode)
|
|
||||||
if q.Contest {
|
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 {
|
} else if idx.workedCallSlots != nil {
|
||||||
_, out[i] = idx.workedCallSlots[key]
|
_, out[i] = idx.workedCallSlots[slotKey(q.Call, q.Band, q.Mode)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -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.
|
// lines stretched across a 34-inch window are not readable, they are long.
|
||||||
<div className="flex flex-col min-h-0 flex-1 gap-2 p-2 w-full max-w-4xl mx-auto">
|
<div className="flex flex-col min-h-0 flex-1 gap-2 p-2 w-full max-w-4xl mx-auto">
|
||||||
{/* toolbar */}
|
{/* toolbar */}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-x-2 gap-y-1.5 flex-wrap">
|
||||||
<Eye className="size-4 text-primary shrink-0" />
|
<Eye className="size-4 text-primary shrink-0" />
|
||||||
<Input className="h-8 w-44 font-mono uppercase" placeholder={t('wl.addPh')} value={addCall}
|
<Input className="h-8 w-44 font-mono uppercase" placeholder={t('wl.addPh')} value={addCall}
|
||||||
onChange={(e) => setAddCall(e.target.value)}
|
onChange={(e) => setAddCall(e.target.value)}
|
||||||
@@ -194,7 +194,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
|||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="size-3.5 absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
<Search className="size-3.5 absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input className="h-8 w-40 pl-7 text-sm" placeholder={t('wl.searchPh')} value={search}
|
<Input className="h-8 w-32 pl-7 text-sm" placeholder={t('wl.searchPh')} value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)} />
|
onChange={(e) => setSearch(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
{/* families kept together, told apart — per review of the DXHunter port,
|
{/* families kept together, told apart — per review of the DXHunter port,
|
||||||
|
|||||||
Reference in New Issue
Block a user