feat(watchlist): the DXHunter watchlist as an OpsLog tab
The concept, transplanted: a list of callsigns or prefixes being hunted, matched against the live spot stream, one card per entry with the spots underneath and the two questions that matter answered on every line — is this slot still needed, and what is it worth (the cluster's own NEW badges, read from the same status index). The file is DXHunter's own watchlist.json, field for field, ClubLog block included though phase 2 will fill it — a file that round-trips unchanged is the whole of 'same format', and a test pins it with a real DXHunter entry. Global (dataDir), not per profile. CONTEST is per entry, not the global mode DXHunter has: a contest entry is judged against the current UTC day — the boundary lives in the query's date bound, so midnight needs no timer and resets nothing. Normal entries read the same in-memory worked index the alerts use. Prefix matching is why RI0SP catches RI0SP/MM, pinned by test. Notify goes through the existing alert:fired event — the frontend already toasts and sounds it — throttled to one alert per entry per two minutes, because a DXpedition lights every skimmer on the planet. Tab wired like NET Control: opt-in from Tools, persisted, closable. Single click fills the callsign, double click works the spot — the cluster's own gesture, kept.
This commit is contained in:
@@ -2003,6 +2003,43 @@ func bandStatusCode(callWorked, callConfirmed, entityConfirmed bool) int {
|
||||
|
||||
// modeClass collapses ADIF modes into the three buckets DXers care about.
|
||||
// Anything not voice and not CW is treated as digital.
|
||||
// ModeClass is modeClass for callers outside the package — the watchlist
|
||||
// judges slots at the same grain the cluster does.
|
||||
func ModeClass(mode string) string { return modeClass(mode) }
|
||||
|
||||
// SlotRow is one (callsign, band, mode) triple from SlotsSince.
|
||||
type SlotRow struct {
|
||||
Callsign string
|
||||
Band string
|
||||
Mode string
|
||||
}
|
||||
|
||||
// SlotsSince lists the slots of every contact made since the given instant —
|
||||
// the contest watchlist's "worked today", where the day boundary lives in this
|
||||
// query's bound and nowhere else.
|
||||
func SlotsSince(r *Repo, ctx context.Context, since time.Time) ([]SlotRow, error) {
|
||||
return r.SlotsSince(ctx, since)
|
||||
}
|
||||
|
||||
func (r *Repo) SlotsSince(ctx context.Context, since time.Time) ([]SlotRow, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
"SELECT callsign, band, mode FROM qso WHERE qso_date >= ?",
|
||||
since.UTC().Format(isoMillis))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SlotRow
|
||||
for rows.Next() {
|
||||
var sr SlotRow
|
||||
if err := rows.Scan(&sr.Callsign, &sr.Band, &sr.Mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, sr)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func modeClass(mode string) string {
|
||||
switch strings.ToUpper(mode) {
|
||||
case "SSB", "USB", "LSB", "AM", "FM", "DIGITALVOICE", "PHONE":
|
||||
|
||||
Reference in New Issue
Block a user