feat: QSO rate meter (10/60 min) in the header
Opt-in via Settings→General (portable pref opslog.showQsoRate). Shows the contest-style QSO rate in QSOs/hour, projected from the trailing 10-minute (count ×6) and 60-minute windows, between the widget icons and propagation. Backend: qso.RecentRate counts QSOs whose start time falls in each trailing window, scanning only the last 400 rows (cheap on a large log); App.GetQSORate exposes the 10/60-min counts. Frontend refreshes on qso:logged and a 30s tick. The meter shares the propagation grid cell — the header is a fixed 6-column grid, so adding it as its own child pushed profile/band-map/compact onto a second row. i18n EN + FR.
This commit is contained in:
@@ -1863,6 +1863,40 @@ func (r *Repo) Count(ctx context.Context) (int64, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// RecentRate counts QSOs whose start time falls within each trailing window from
|
||||
// `now` — the live "QSO rate" meter shown in the header. 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, windows ...time.Duration) ([]int, error) {
|
||||
counts := make([]int, len(windows))
|
||||
// 400 rows covers a full hour even at a blistering contest rate (>300/h); any
|
||||
// QSO inside the trailing windows is among the most recently inserted.
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT qso_date FROM qso ORDER BY id DESC LIMIT 400`)
|
||||
if err != nil {
|
||||
return counts, err
|
||||
}
|
||||
defer rows.Close()
|
||||
now = now.UTC()
|
||||
for rows.Next() {
|
||||
var dateStr sql.NullString
|
||||
if err := rows.Scan(&dateStr); err != nil {
|
||||
return counts, err
|
||||
}
|
||||
t := parseTimeLoose(dateStr.String).UTC()
|
||||
if t.IsZero() || t.After(now) {
|
||||
continue
|
||||
}
|
||||
age := now.Sub(t)
|
||||
for i, w := range windows {
|
||||
if age <= w {
|
||||
counts[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts, rows.Err()
|
||||
}
|
||||
|
||||
// ExistingDedupeKeys returns a set of every QSO key currently in the DB,
|
||||
// used by the ADIF importer to skip records that would re-create the
|
||||
// same contact. The key is callsign|YYYY-MM-DDTHH:MM|band|mode — minute
|
||||
|
||||
Reference in New Issue
Block a user