feat: QSO rate meter — per-operator AND team totals
GetQSORate now returns both the active operator's rate (their own performance) and the whole station's rate (all operators combined). RecentRateBreakdown computes both in one scan of the recent rows (per-operator + all-operators), replacing RecentRate.
This commit is contained in:
+19
-19
@@ -1921,21 +1921,20 @@ func (r *Repo) LastQSOTime(ctx context.Context, operator string) (time.Time, boo
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// RecentRate counts QSOs whose start time falls within each trailing window from
|
||||
// `now` — the live "QSO rate" meter shown in the header. When operator is non-empty
|
||||
// (multi-op on a shared logbook) only that operator's QSOs are counted, so each op
|
||||
// sees their OWN performance, not the cumulative rate; empty operator matches every
|
||||
// QSO. It scans only the most recently inserted rows (ORDER BY id DESC LIMIT), since
|
||||
// any QSO in the last hour was inserted recently; that keeps it cheap even on a large
|
||||
// log. qso_date is the repo's text column, parsed with parseTimeLoose (backend-format
|
||||
// agnostic).
|
||||
func (r *Repo) RecentRate(ctx context.Context, now time.Time, operator string, windows ...time.Duration) ([]int, error) {
|
||||
counts := make([]int, len(windows))
|
||||
// 2000 rows covers a full hour for one operator even in a busy multi-op run
|
||||
// (other operators' rows are discarded before counting).
|
||||
// RecentRateBreakdown counts, in ONE pass over the most recent rows, QSOs whose
|
||||
// start time falls within each trailing window from `now` — for a specific operator
|
||||
// (their own rate, `op`) AND for ALL operators combined (the team/station rate,
|
||||
// `all`). The header rate meter shows both. It scans only recently inserted rows
|
||||
// (ORDER BY id DESC LIMIT), since any QSO in the last hour was inserted recently, so
|
||||
// it stays cheap on a large log. qso_date is parsed with parseTimeLoose (backend-
|
||||
// format agnostic).
|
||||
func (r *Repo) RecentRateBreakdown(ctx context.Context, now time.Time, operator string, windows ...time.Duration) (op []int, all []int, err error) {
|
||||
op = make([]int, len(windows))
|
||||
all = make([]int, len(windows))
|
||||
// 2000 rows covers a full hour even in a busy multi-op run.
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT operator, qso_date FROM qso ORDER BY id DESC LIMIT 2000`)
|
||||
if err != nil {
|
||||
return counts, err
|
||||
return op, all, err
|
||||
}
|
||||
defer rows.Close()
|
||||
now = now.UTC()
|
||||
@@ -1943,23 +1942,24 @@ func (r *Repo) RecentRate(ctx context.Context, now time.Time, operator string, w
|
||||
for rows.Next() {
|
||||
var oper, dateStr sql.NullString
|
||||
if err := rows.Scan(&oper, &dateStr); err != nil {
|
||||
return counts, err
|
||||
}
|
||||
if strings.ToUpper(strings.TrimSpace(oper.String)) != opFilter {
|
||||
continue // a different operator's QSO — not part of my rate
|
||||
return op, all, err
|
||||
}
|
||||
t := parseTimeLoose(dateStr.String).UTC()
|
||||
if t.IsZero() || t.After(now) {
|
||||
continue
|
||||
}
|
||||
mine := strings.ToUpper(strings.TrimSpace(oper.String)) == opFilter
|
||||
age := now.Sub(t)
|
||||
for i, w := range windows {
|
||||
if age <= w {
|
||||
counts[i]++
|
||||
all[i]++
|
||||
if mine {
|
||||
op[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts, rows.Err()
|
||||
return op, all, rows.Err()
|
||||
}
|
||||
|
||||
// ExistingDedupeKeys returns a set of every QSO key currently in the DB,
|
||||
|
||||
Reference in New Issue
Block a user