diff --git a/app_watchlist.go b/app_watchlist.go index 5f64bb4..16c77f3 100644 --- a/app_watchlist.go +++ b/app_watchlist.go @@ -200,6 +200,17 @@ func (a *App) watchSpot(dxCall, band, mode, country, comment string, freqHz int6 if !ok || !notify || a.ctx == nil { return } + // Already worked? Then nothing to announce. Judged HERE, before the sound — + // the report was an alert ringing for a slot the tab showed as Worked a + // moment later, because the frontend's verdict arrives on a debounce while + // the alert used to fire on the raw spot. Same verdict as the tab: exact + // slot for named modes, digital class for a generic DATA spot, today-only + // for a contest entry. + if e, found := a.watchlist.Get(entry); found { + if a.WatchlistWorkedSlots([]WatchlistSlotQuery{{Call: dxCall, Band: band, Mode: mode, Contest: e.IsContest}})[0] { + return + } + } // Throttled per entry: a DXpedition lights up every skimmer on the planet, // and forty alerts a minute for one station is a alarm nobody keeps on. a.watchAlertMu.Lock() diff --git a/frontend/src/components/WatchlistTab.tsx b/frontend/src/components/WatchlistTab.tsx index cdc64b3..801ba09 100644 --- a/frontend/src/components/WatchlistTab.tsx +++ b/frontend/src/components/WatchlistTab.tsx @@ -65,6 +65,17 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P const setNeededOnly = (f: (v: boolean) => boolean) => setNeededOnlyRaw((v) => { const nv = f(v); try { localStorage.setItem('opslog.wlNeededOnly', nv ? '1' : '0'); } catch {} return nv; }); const setActiveOnly = (f: (v: boolean) => boolean) => setActiveOnlyRaw((v) => { const nv = f(v); try { localStorage.setItem('opslog.wlActiveOnly', nv ? '1' : '0'); } catch {} return nv; }); const setFamily = (v: 'all' | 'normal' | 'contest') => { setFamilyRaw(v); try { localStorage.setItem('opslog.wlFamily', v); } catch {} }; + // Mode filter — DXHunter's "All Modes" select. DIGI matches the digital class + // (FT8, FT4, a generic DATA spot…), the named modes match exactly. + const [modeFilter, setModeFilterRaw] = useState(() => localStorage.getItem('opslog.wlMode') || 'ALL'); + const setModeFilter = (v: string) => { setModeFilterRaw(v); try { localStorage.setItem('opslog.wlMode', v); } catch {} }; + const modeMatches = (s2: ClusterSpot): boolean => { + if (modeFilter === 'ALL') return true; + const m = (inferSpotMode(s2.comment ?? '', s2.freq_hz) || '').toUpperCase(); + if (modeFilter === 'DIGI') return !['', 'CW', 'SSB', 'USB', 'LSB', 'FM', 'AM'].includes(m); + if (modeFilter === 'SSB') return m === 'SSB' || m === 'USB' || m === 'LSB'; + return m === modeFilter; + }; const [error, setError] = useState(''); const [removeArm, setRemoveArm] = useState(''); // worked answer per "call|band|modeclass|contest" key. @@ -162,11 +173,23 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P if (family === 'normal' && e.isContest) return false; if (family === 'contest' && !e.isContest) return false; if (search && !e.callsign.includes(search.trim().toUpperCase())) return false; - if (activeOnly && (spotsFor.get(e.callsign) ?? []).length === 0) return false; - if (neededOnly && !(spotsFor.get(e.callsign) ?? []).some((s) => !workedFor(e, s))) return false; + const list = (spotsFor.get(e.callsign) ?? []).filter(modeMatches); + if (activeOnly && list.length === 0) return false; + if (neededOnly && !list.some((s2) => !workedFor(e, s2))) return false; return true; }); + const counters = useMemo(() => { + let active = 0, needed = 0; + for (const e of entries) { + const list = (spotsFor.get(e.callsign) ?? []).filter(modeMatches); + if (list.length > 0) active++; + if (list.some((s2) => !workedFor(e, s2))) needed++; + } + return { total: entries.length, active, needed }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entries, spotsFor, worked, modeFilter]); + // The cluster's own badge for a spot, read from the shared status index. const dxccBadge = (s: ClusterSpot): { label: string; color: string } | null => { const st = spotStatus[spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz)]; @@ -194,6 +217,13 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P {/* toolbar */}
+ + {t('wl.cTotal')} {counters.total} + · + {t('wl.cActive')} {counters.active} + · + {t('wl.cNeeded')} {counters.needed} + setAddCall(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void add(); }} /> @@ -226,6 +256,13 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P ))}
+