feat(hamlog): mark what has been sent, and a QSL Manager backlog

An upload now leaves a trace on the QSO: APP_OPSLOG_HAMLOG_SENT holds the day
it went. The date rather than a Y, so a log says WHEN — and so one shape serves
three readers: eligibility (a stamped QSO is not sent twice), the QSL Manager's
backlog, and the appearance rules' sent channel.

HAMLOG joins the QSL Manager's service list. It has no status column to select
on, so its backlog is the ABSENCE of that extras key — a LIKE over the extras
JSON, which is a full scan and is the right trade here: it answers a button an
operator presses, and the alternative is a promoted column for a field no other
program would ever read. The match is on the quoted key, so a QSO whose comment
merely mentions the text is not taken for one already sent — which is what the
test pins.
This commit is contained in:
2026-08-23 12:40:09 +02:00
parent 73806cbd70
commit 036dc53fe8
5 changed files with 104 additions and 3 deletions
+36
View File
@@ -3750,3 +3750,39 @@ func (r *Repo) IDByDedupeKey(ctx context.Context, callsign, qsoDateMinute, band,
}
return id, u.String, true, nil
}
// ListMissingExtra returns the QSOs that do NOT carry an ADIF extras key.
//
// For a service with no status column of its own — HAMLOG.online, whose field
// the ADIF standard does not define — this is the backlog: everything never
// sent. Matched with LIKE on the extras JSON, which is a full scan; that is
// acceptable here because it answers a button the operator presses, not a hot
// path, and the alternative is a promoted column for a field no other program
// would ever read.
func (r *Repo) ListMissingExtra(ctx context.Context, key string) ([]QSO, error) {
key = strings.ToUpper(strings.TrimSpace(key))
if key == "" {
return nil, fmt.Errorf("missing extras key")
}
// The JSON is written by encoding/json, so a present key always appears as
// "KEY": — quoted, followed by a colon. A value containing that text cannot
// produce a false positive: it would be escaped inside its own quotes.
like := "%\"" + key + "\":%"
rows, err := r.db.QueryContext(ctx,
`SELECT `+selectCols+` FROM qso
WHERE COALESCE(extras_json,'') NOT LIKE ?
ORDER BY qso_date DESC, id DESC`, like)
if err != nil {
return nil, fmt.Errorf("list missing extra: %w", err)
}
defer rows.Close()
out := make([]QSO, 0, 64)
for rows.Next() {
q, err := scanQSO(rows)
if err != nil {
return nil, err
}
out = append(out, q)
}
return out, rows.Err()
}
+34
View File
@@ -143,3 +143,37 @@ func TestIDByDedupeKeyRecognisesTheSameContact(t *testing.T) {
t.Error("an empty key matched a row")
}
}
// A service with no status column of its own — HAMLOG.online, whose field the
// ADIF standard does not define — finds its backlog by the ABSENCE of an extras
// key. The LIKE has to match the key, and only the key: a QSO whose comment
// happens to contain the same text must not be mistaken for one already sent.
func TestListMissingExtraMatchesTheKeyNotItsText(t *testing.T) {
r := openRepo(t)
ctx := context.Background()
sent := addQSO(t, r, "F4BPO", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
notSent, err := r.Add(ctx, QSO{
Callsign: "F1TRF", QSODate: time.Date(2026, 1, 2, 12, 0, 0, 0, time.UTC),
Band: "40m", Mode: "CW",
Comment: "mentions APP_OPSLOG_HAMLOG_SENT in the text",
})
if err != nil {
t.Fatal(err)
}
if err := r.SetExtra(ctx, sent, "APP_OPSLOG_HAMLOG_SENT", "20260823"); err != nil {
t.Fatal(err)
}
got, err := r.ListMissingExtra(ctx, "APP_OPSLOG_HAMLOG_SENT")
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].ID != notSent {
ids := make([]int64, len(got))
for i, q := range got {
ids[i] = q.ID
}
t.Fatalf("backlog = %v, want just %d (the one never sent)", ids, notSent)
}
}