rigs completed

This commit is contained in:
2026-05-28 18:35:22 +02:00
parent d3c9982c66
commit e8cac569e3
26 changed files with 3834 additions and 391 deletions
+63 -17
View File
@@ -257,6 +257,7 @@ func decodeExtras(s string) map[string]string {
// Add inserts a QSO and returns its ID.
func (r *Repo) Add(ctx context.Context, q QSO) (int64, error) {
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
if q.Callsign == "" {
return 0, fmt.Errorf("empty callsign")
}
@@ -293,6 +294,7 @@ func (r *Repo) AddBatch(ctx context.Context, qsos []QSO) (int64, error) {
var inserted int64
for _, q := range qsos {
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
if q.Callsign == "" {
continue
}
@@ -322,6 +324,7 @@ func (r *Repo) Update(ctx context.Context, q QSO) error {
if q.ID == 0 {
return fmt.Errorf("missing id")
}
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
if q.Callsign == "" {
return fmt.Errorf("empty callsign")
}
@@ -412,8 +415,9 @@ func (r *Repo) List(ctx context.Context, f ListFilter) ([]QSO, error) {
q := `SELECT ` + selectCols + ` FROM qso WHERE 1=1`
args := []any{}
if f.Callsign != "" {
// Contains-match so a search for "XYZ" finds F4XYZ, F4XYZ/P, etc.
q += " AND callsign LIKE ?"
args = append(args, f.Callsign+"%")
args = append(args, "%"+f.Callsign+"%")
}
if f.Band != "" {
q += " AND band = ?"
@@ -428,8 +432,12 @@ func (r *Repo) List(ctx context.Context, f ListFilter) ([]QSO, error) {
args = append(args, f.StationCallsign)
}
q += " ORDER BY qso_date DESC, id DESC"
if f.Limit <= 0 || f.Limit > 1000 {
f.Limit = 200
if f.Limit <= 0 {
f.Limit = 500
}
// Hard upper bound: 1M is enough to fit any realistic personal log.
if f.Limit > 1_000_000 {
f.Limit = 1_000_000
}
q += " LIMIT ? OFFSET ?"
args = append(args, f.Limit, f.Offset)
@@ -549,14 +557,14 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
// ---- Per-callsign stats ----
if err := r.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM qso WHERE callsign = ?`, wb.Callsign).Scan(&wb.Count); err != nil {
`SELECT COUNT(*) FROM qso WHERE upper(trim(callsign)) = ?`, wb.Callsign).Scan(&wb.Count); err != nil {
return wb, fmt.Errorf("count worked: %w", err)
}
if wb.Count > 0 {
rows, err := r.db.QueryContext(ctx, `
SELECT id, qso_date, band, mode, rst_sent, rst_rcvd,
qsl_sent, qsl_rcvd, lotw_sent, lotw_rcvd
FROM qso WHERE callsign = ?
FROM qso WHERE upper(trim(callsign)) = ?
ORDER BY qso_date DESC, id DESC
LIMIT ?`, wb.Callsign, maxWorkedEntries)
if err != nil {
@@ -569,14 +577,17 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
var (
e WorkedEntry
dateStr string
band, mode sql.NullString
rstS, rstR, qslS, qslR, lotwS, lotwR sql.NullString
)
if err := rows.Scan(&e.ID, &dateStr, &e.Band, &e.Mode,
if err := rows.Scan(&e.ID, &dateStr, &band, &mode,
&rstS, &rstR, &qslS, &qslR, &lotwS, &lotwR); err != nil {
rows.Close()
return wb, fmt.Errorf("scan worked: %w", err)
}
e.QSODate = parseTimeLoose(dateStr)
e.Band = band.String
e.Mode = mode.String
e.RSTSent = rstS.String
e.RSTRcvd = rstR.String
e.QSLSent = qslS.String
@@ -584,9 +595,15 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
e.LOTWSent = lotwS.String
e.LOTWRcvd = lotwR.String
wb.Entries = append(wb.Entries, e)
bandsSet[e.Band] = struct{}{}
modesSet[e.Mode] = struct{}{}
bmSet[e.Band+"|"+e.Mode] = BandMode{Band: e.Band, Mode: e.Mode}
if e.Band != "" {
bandsSet[e.Band] = struct{}{}
}
if e.Mode != "" {
modesSet[e.Mode] = struct{}{}
}
if e.Band != "" && e.Mode != "" {
bmSet[e.Band+"|"+e.Mode] = BandMode{Band: e.Band, Mode: e.Mode}
}
if wb.Last.IsZero() {
wb.Last = e.QSODate
}
@@ -597,7 +614,7 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
if wb.Count > maxWorkedEntries {
var firstStr sql.NullString
_ = r.db.QueryRowContext(ctx,
`SELECT MIN(qso_date) FROM qso WHERE callsign = ?`, wb.Callsign).Scan(&firstStr)
`SELECT MIN(qso_date) FROM qso WHERE upper(trim(callsign)) = ?`, wb.Callsign).Scan(&firstStr)
if firstStr.Valid {
wb.First = parseTimeLoose(firstStr.String)
}
@@ -622,7 +639,7 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
var d sql.NullInt64
_ = r.db.QueryRowContext(ctx, `
SELECT dxcc FROM qso
WHERE callsign = ? AND dxcc IS NOT NULL
WHERE upper(trim(callsign)) = ? AND dxcc IS NOT NULL
ORDER BY qso_date DESC LIMIT 1`, wb.Callsign).Scan(&d)
if d.Valid {
dxcc = int(d.Int64)
@@ -656,15 +673,17 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
}
if err := r.collectDistinct(ctx, &wb.DXCCBands,
`SELECT DISTINCT band FROM qso WHERE dxcc = ?`, dxcc); err != nil {
`SELECT DISTINCT band FROM qso WHERE dxcc = ? AND band IS NOT NULL AND band != ''`, dxcc); err != nil {
return wb, err
}
if err := r.collectDistinct(ctx, &wb.DXCCModes,
`SELECT DISTINCT mode FROM qso WHERE dxcc = ?`, dxcc); err != nil {
`SELECT DISTINCT mode FROM qso WHERE dxcc = ? AND mode IS NOT NULL AND mode != ''`, dxcc); err != nil {
return wb, err
}
bmRows, err := r.db.QueryContext(ctx,
`SELECT DISTINCT band, mode FROM qso WHERE dxcc = ?`, dxcc)
`SELECT DISTINCT band, mode FROM qso WHERE dxcc = ?
AND band IS NOT NULL AND band != ''
AND mode IS NOT NULL AND mode != ''`, dxcc)
if err != nil {
return wb, fmt.Errorf("dxcc band_modes: %w", err)
}
@@ -684,15 +703,21 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int)
// One pass over every distinct (band, mode) in the DXCC, aggregating
// "did this call work it?" and "was anything confirmed?" via MAX.
// Status precedence: call_c > call_w > dxcc_c > dxcc_w.
// Filter NULL/empty band+mode rows — they'd create a NULL group key
// that Scan into *string can't handle and would error out the whole
// WorkedBefore call, blanking the matrix in the UI.
statusRows, err := r.db.QueryContext(ctx, `
SELECT band, mode,
MAX(CASE WHEN callsign = ? THEN 1 ELSE 0 END),
MAX(CASE WHEN callsign = ?
MAX(CASE WHEN upper(trim(callsign)) = ? THEN 1 ELSE 0 END),
MAX(CASE WHEN upper(trim(callsign)) = ?
AND (lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y')
THEN 1 ELSE 0 END),
MAX(CASE WHEN lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y'
THEN 1 ELSE 0 END)
FROM qso WHERE dxcc = ?
FROM qso
WHERE dxcc = ?
AND band IS NOT NULL AND band != ''
AND mode IS NOT NULL AND mode != ''
GROUP BY band, mode`, wb.Callsign, wb.Callsign, dxcc)
if err != nil {
return wb, fmt.Errorf("band status: %w", err)
@@ -886,6 +911,27 @@ func (r *Repo) EntitySlotMap(ctx context.Context, resolveEntity func(callsign st
return out, rows.Err()
}
// WorkedCallsigns returns the set of every callsign ever logged (uppercased).
// One pass, used by the cluster spot colouring to flag "already worked this
// exact call" regardless of band/mode — Log4OM/RUMlogNG-style call highlight.
func (r *Repo) WorkedCallsigns(ctx context.Context) (map[string]struct{}, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT DISTINCT upper(callsign) FROM qso WHERE callsign != ''`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]struct{}, 1024)
for rows.Next() {
var c string
if err := rows.Scan(&c); err != nil {
return nil, err
}
out[c] = struct{}{}
}
return out, rows.Err()
}
// Count returns the total number of QSOs in the database.
func (r *Repo) Count(ctx context.Context) (int64, error) {
var n int64