fix: QSL Manager properly calculating new slot (Digi modes are grouped together)

This commit is contained in:
2026-07-16 23:26:04 +02:00
parent 46e3619a38
commit cd13921322
7 changed files with 160 additions and 16 deletions
+67 -4
View File
@@ -1958,18 +1958,80 @@ func closestRef(refs []matchRef, when time.Time, window time.Duration) (int64, b
// ConfirmedSets captures which DXCC / band / slot combinations are already
// confirmed (by any QSL system), so a freshly-downloaded confirmation can be
// flagged as a NEW DXCC / NEW BAND / NEW SLOT.
// ConfirmedSets captures confirmed combinations for the QSL Manager's NEW flags.
// Modes are grouped into CLASSES (Phone/CW/Digital) — a digital confirmation is a
// "new mode/slot" only if no digital mode was confirmed there before (RTTY and FT8
// are the same DIGI class). Raw-mode granularity lives only in the cluster/matrix.
type ConfirmedSets struct {
DXCC map[int]bool // dxcc entity confirmed
Band map[string]bool // "dxcc|band"
Slot map[string]bool // "dxcc|band|mode"
Mode map[string]bool // "dxcc|class"
Slot map[string]bool // "dxcc|band|class"
}
// SlotKey / BandKey build the composite keys used in ConfirmedSets.
// Key builders for ConfirmedSets. Band is mode-agnostic; Mode/Slot use the class.
func BandKey(dxcc int, band string) string { return fmt.Sprintf("%d|%s", dxcc, strings.ToLower(band)) }
func ModeClassKey(dxcc int, mode string) string {
return fmt.Sprintf("%d|%s", dxcc, modeClass(mode))
}
func SlotClassKey(dxcc int, band, mode string) string {
return fmt.Sprintf("%d|%s|%s", dxcc, strings.ToLower(band), modeClass(mode))
}
// SlotKey is the RAW-mode slot key, kept for the cluster/matrix new-slot flag.
func SlotKey(dxcc int, band, mode string) string {
return fmt.Sprintf("%d|%s|%s", dxcc, strings.ToLower(band), strings.ToUpper(mode))
}
// SlotStats is the worked/confirmed tally for the QSL Manager, counting slots by
// mode CLASS (Phone / CW / Digital) — NOT raw mode. Raw-mode granularity (RTTY ≠
// FT8) is kept only for the cluster/matrix "new slot" flag; the totals here match
// how slots are conventionally counted (a band in a class, not in each digital
// sub-mode).
type SlotStats struct {
SlotsWorked int `json:"slots_worked"` // distinct DXCC × band × class, all QSOs
SlotsConfirmed int `json:"slots_confirmed"` // distinct DXCC × band × class, confirmed
DXCCWorked int `json:"dxcc_worked"` // distinct DXCC entities worked
DXCCConfirmed int `json:"dxcc_confirmed"` // distinct DXCC entities confirmed
}
// GetSlotStats computes the worked/confirmed slot and DXCC tallies in one pass.
// "Confirmed" = LoTW or paper QSL received (the award-valid sources).
func (r *Repo) GetSlotStats(ctx context.Context) (SlotStats, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT COALESCE(dxcc,0), LOWER(COALESCE(band,'')), UPPER(COALESCE(mode,'')),
CASE WHEN lotw_rcvd='Y' OR qsl_rcvd='Y' THEN 1 ELSE 0 END
FROM qso`)
if err != nil {
return SlotStats{}, err
}
defer rows.Close()
dxccW, slotW := map[int]bool{}, map[string]bool{}
dxccC, slotC := map[int]bool{}, map[string]bool{}
for rows.Next() {
var dxcc, conf int
var band, mode string
if err := rows.Scan(&dxcc, &band, &mode, &conf); err != nil {
return SlotStats{}, err
}
if dxcc == 0 {
continue
}
key := SlotClassKey(dxcc, band, mode)
dxccW[dxcc] = true
if band != "" {
slotW[key] = true
}
if conf == 1 {
dxccC[dxcc] = true
if band != "" {
slotC[key] = true
}
}
}
return SlotStats{SlotsWorked: len(slotW), SlotsConfirmed: len(slotC), DXCCWorked: len(dxccW), DXCCConfirmed: len(dxccC)}, rows.Err()
}
// confirmedCols whitelists the received-status columns ConfirmedSlots may
// OR together (guards the dynamic SQL).
var confirmedCols = map[string]bool{
@@ -1985,7 +2047,7 @@ var confirmedCols = map[string]bool{
// {lotw_rcvd, qsl_rcvd} (the award-valid sources), QRZ uses
// {qrzcom_qso_download_status}.
func (r *Repo) ConfirmedSlots(ctx context.Context, cols []string) (ConfirmedSets, error) {
sets := ConfirmedSets{DXCC: map[int]bool{}, Band: map[string]bool{}, Slot: map[string]bool{}}
sets := ConfirmedSets{DXCC: map[int]bool{}, Band: map[string]bool{}, Mode: map[string]bool{}, Slot: map[string]bool{}}
var conds []string
for _, c := range cols {
if confirmedCols[c] {
@@ -2014,7 +2076,8 @@ func (r *Repo) ConfirmedSlots(ctx context.Context, cols []string) (ConfirmedSets
}
sets.DXCC[dxcc] = true
sets.Band[BandKey(dxcc, band)] = true
sets.Slot[SlotKey(dxcc, band, mode)] = true
sets.Mode[ModeClassKey(dxcc, mode)] = true
sets.Slot[SlotClassKey(dxcc, band, mode)] = true
}
return sets, rows.Err()
}