test: prove the status filter filters; fix a regex escape in the filter builder
An operator reports that "QRZ.com received status = N" returns rows showing both N and Y (issue #5 follow-up). The SQL is a plain col = ?, but reading it cannot distinguish a wrong query from a UI that kept the previous rows after an error — so this runs it: five QSOs inserted, filtered, and both the list and the count asserted. The backend filters correctly, list and count agree. The fault is not in the query. Found while looking: the filter builder tested an ADIF date with /^d{8}$/ instead of /^\d{8}$/. The escape was missing, so the branch never matched and the calendar input was handed "20260728", which type=date rejects — an empty box over a value that was really stored.
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
"version": "0.21.9",
|
||||
"date": "2026-07-28",
|
||||
"en": [
|
||||
"Filter builder: a confirmation date typed into a condition showed an empty box. The value was stored correctly, only the calendar field failed to display it.",
|
||||
"The Icom model list gains the IC-7300MKII (CI-V B6), plus the IC-7100, IC-7410, IC-7600 and IC-7851. The IC-7700 and IC-7800 were listed at the IC-7100's and IC-7410's addresses, so picking them set an address the rig never answers on.",
|
||||
"Icom over the LAN: the frequency no longer stays frozen after another program (WSJT-X through OmniRig, the Remote Utility) takes the CI-V session. The rig kept answering pings while sending nothing, so OpsLog showed the last known frequency until it was restarted; it now reconnects on its own.",
|
||||
"The offline FCC (ULS) database now answers while you type a US callsign, filling state, county and a 6-character grid. It was only applied when the QSO was saved, so without a QRZ.com or HamQTH account you saw nothing but the country and a coarse grid. Online lookups still win — ULS only fills what is blank.",
|
||||
"CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes."
|
||||
],
|
||||
"fr": [
|
||||
"Constructeur de filtres : une date de confirmation saisie dans une condition s'affichait dans une case vide. La valeur était bien enregistrée, seul le champ calendrier ne la montrait pas.",
|
||||
"La liste des modèles Icom gagne l'IC-7300MKII (CI-V B6), ainsi que les IC-7100, IC-7410, IC-7600 et IC-7851. Les IC-7700 et IC-7800 y figuraient avec les adresses des IC-7100 et IC-7410 : les choisir réglait une adresse sur laquelle la radio ne répond jamais.",
|
||||
"Icom via le réseau : la fréquence ne reste plus figée après qu'un autre programme (WSJT-X via OmniRig, la Remote Utility) a pris la session CI-V. La radio continuait de répondre aux pings sans rien émettre, si bien qu'OpsLog affichait la dernière fréquence connue jusqu'à son redémarrage ; il se reconnecte désormais tout seul.",
|
||||
"La base FCC (ULS) hors ligne répond désormais pendant la saisie d'un indicatif américain, en remplissant l'état, le comté et un locator 6 caractères. Elle n'était appliquée qu'à l'enregistrement du QSO : sans compte QRZ.com ou HamQTH, vous ne voyiez que le pays et un locator approximatif. Les recherches en ligne restent prioritaires — l'ULS ne comble que ce qui est vide.",
|
||||
|
||||
@@ -278,7 +278,11 @@ export function FilterBuilder({ open, initial, onApply, onClose }: Props) {
|
||||
className="h-8 flex-1 text-xs"
|
||||
disabled={!needsValue}
|
||||
placeholder={needsValue ? (fieldType === 'date' ? 'YYYY-MM-DD' : t('fltb.valuePh')) : '—'}
|
||||
value={fieldType === 'adifdate' && /^d{8}$/.test(c.value)
|
||||
// \d, not d: the escape was missing, so the test never matched
|
||||
// an 8-digit ADIF date and the calendar input was handed
|
||||
// "20260728" — which type=date rejects, showing an empty box
|
||||
// over a value that was really there.
|
||||
value={fieldType === 'adifdate' && /^\d{8}$/.test(c.value)
|
||||
? `${c.value.slice(0, 4)}-${c.value.slice(4, 6)}-${c.value.slice(6, 8)}`
|
||||
: c.value}
|
||||
onChange={(e) => setCond(i, {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package qso
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"hamlog/internal/db"
|
||||
)
|
||||
|
||||
// End-to-end proof that filtering a confirmation STATUS actually filters.
|
||||
//
|
||||
// Reported from the field: "QRZ.com received status = N" returned rows showing
|
||||
// both N and Y. The SQL is a plain `col = ?`, so either the query is right and
|
||||
// the fault is elsewhere (the UI keeping the previous rows on an error, say), or
|
||||
// it is wrong here. Reading the code cannot tell those apart — running it can.
|
||||
func TestFilterOnConfirmationStatus(t *testing.T) {
|
||||
conn, err := db.Open(filepath.Join(t.TempDir(), "logbook.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
r := NewRepo(conn)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, c := range []struct{ call, status string }{
|
||||
{"W1AAA", "Y"},
|
||||
{"W1BBB", "N"},
|
||||
{"W1CCC", "Y"},
|
||||
{"W1DDD", "N"},
|
||||
{"W1EEE", ""}, // never touched by a download — NULL/empty, neither Y nor N
|
||||
} {
|
||||
q := QSO{Callsign: c.call, Band: "20m", Mode: "SSB", QRZComDownloadStatus: c.status}
|
||||
if _, err := r.Add(ctx, q); err != nil {
|
||||
t.Fatalf("insert %s: %v", c.call, err)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := r.ListFiltered(ctx, QueryFilter{
|
||||
Conditions: []Condition{{Field: "qrzcom_qso_download_status", Op: "eq", Value: "N"}},
|
||||
Match: "AND",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiltered: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("filter = N returned %d rows, want 2", len(got))
|
||||
}
|
||||
for _, q := range got {
|
||||
if q.QRZComDownloadStatus != "N" {
|
||||
t.Errorf("filter = N returned %s with status %q", q.Callsign, q.QRZComDownloadStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// A count that disagreed with the list would show "2 matches" above a grid of
|
||||
// five rows — the exact shape of the report.
|
||||
n, err := r.CountFiltered(ctx, QueryFilter{
|
||||
Conditions: []Condition{{Field: "qrzcom_qso_download_status", Op: "eq", Value: "N"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CountFiltered: %v", err)
|
||||
}
|
||||
if n != 2 {
|
||||
t.Errorf("CountFiltered = %d, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown field must ERROR rather than be silently dropped: a dropped
|
||||
// condition returns the whole logbook, which reads as "the filter did nothing".
|
||||
func TestFilterRejectsUnknownField(t *testing.T) {
|
||||
if _, _, err := conditionSQL(Condition{Field: "not_a_column", Op: "eq", Value: "N"}); err == nil {
|
||||
t.Error("unknown filter field accepted — it would silently return every QSO")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user