feat(watchlist): the auto-contest pattern — DXHunter's contest_prefix

A field in the tab's toolbar (e.g. WWA): while non-empty, any spotted callsign
CONTAINING it joins the watchlist as a contest entry by itself — a
special-event fleet (HB9WWA, DL0WWA, F4WWA/P…) is collected as it appears
instead of typed in one by one, and each is judged per UTC day like any contest
entry. Contains, not prefix, because the event string sits anywhere in those
calls.

Global setting, cached in an atomic so the spot pipeline never touches the
settings store per spot. Emptying the field stops the collecting but keeps what
was collected — DXHunter deletes non-matching contest entries on a pattern
change, and a setting that silently empties an operator's list is the one
behaviour of the original this port refuses.

The tab also refreshes on backend auto-adds (event) and on a slow tick, so
last-seen and the counters stay honest while it sits open.
This commit is contained in:
2026-08-29 00:50:14 +02:00
parent c4de5f2ea0
commit 8f64e3fee3
4 changed files with 58 additions and 6 deletions
+9 -6
View File
@@ -96,12 +96,13 @@ const (
keyLookupFailsafe = "lookup.failsafe"
keyLookupImages = "lookup.download_images" // 1 = expose QRZ ImageURL to UI
keyStationCallsign = "station.callsign"
keyStationOperator = "station.operator"
keyStationMyGrid = "station.my_grid"
keyStationCountry = "station.my_country"
keyStationSOTA = "station.my_sota_ref"
keyStationPOTA = "station.my_pota_ref"
keyStationCallsign = "station.callsign"
keyStationOperator = "station.operator"
keyWatchlistContestPattern = "watchlist.contest_pattern" // auto-add contest calls containing this
keyStationMyGrid = "station.my_grid"
keyStationCountry = "station.my_country"
keyStationSOTA = "station.my_sota_ref"
keyStationPOTA = "station.my_pota_ref"
keyListsBands = "lists.bands"
keyListsModes = "lists.modes"
@@ -739,6 +740,7 @@ type App struct {
watchlist *watchlist.Store // Tools → Watchlist (global watchlist.json)
watchAlertMu sync.Mutex // throttles watchlist alerts…
watchAlertAt map[string]time.Time // …per entry
watchPattern atomic.Value // auto-contest pattern (string), loaded at startup
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
@@ -1391,6 +1393,7 @@ func (a *App) startup(ctx context.Context) {
a.pota = pota.New(func(format string, args ...any) { applog.Printf(format, args...) })
a.watchlist = watchlist.New(filepath.Join(a.dataDir, "watchlist.json"))
a.watchAlertAt = map[string]time.Time{}
a.watchPattern.Store(strings.ToUpper(strings.TrimSpace(a.settingOr(keyWatchlistContestPattern, ""))))
go a.pota.Run(a.ctx)
// DX Cluster (multi-server): the spot callback enriches each spot