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
))}
+