7ace2cc602
Backend (Go 1.25 / Wails v2): - QSO storage on SQLite (modernc) with embedded migrations (0001..0005) - Streaming ADIF import (batch insert) + WorkedBefore per callsign and DXCC - Callsign lookup with QRZ.com + HamQTH providers (primary/failsafe routing) and SQLite-backed TTL cache - DXCC resolver from cty.dat (auto-download, longest-prefix-match) - Multi-profile operator identities (home/portable/SOTA/contest) — every QSO stamps MY_* from the active profile - CAT control via OmniRig COM on a single OS-locked goroutine, with bidirectional sync (freq/mode/band/split/VFOs) and Rig1/Rig2 hot-swap - Settings store (key/value), CAT debug log at %APPDATA%/HamLog/cat.log Frontend (React 18 + TypeScript + Tailwind v4 + shadcn-style): - Single-row entry strip with CAT-aware band/mode/freq, RST, Start/End UTC, per-field locks (band/mode/freq/start/end) for backdated QSOs - Topbar: live freq (MHz.kHz.Hz dotted), live UTC, band/mode/SPLIT badges, CAT pill with rig selector and clickable Azimuth pill (rotor TODO) - Settings tree: Profiles (Log4OM-style manager), Station Information (edits the active profile), unified Callsign Lookup with Test buttons, Bands/Modes lists, CAT - Worked-before matrix (band × mode × class) with new-DXCC highlighting - ADIF import from menu + Maintenance > Refresh cty.dat Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
// dbdiag inspects HamLog DB state. Optional 2nd arg: "wb CALL DXCC".
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const migrationsDir = "internal/db/migrations"
|
|
|
|
func main() {
|
|
base, _ := os.UserConfigDir()
|
|
dbPath := filepath.Join(base, "HamLog", "hamlog.db")
|
|
|
|
mode := "summary"
|
|
var call string
|
|
var dxcc int
|
|
if len(os.Args) >= 3 && os.Args[1] == "wb" {
|
|
mode = "wb"
|
|
call = strings.ToUpper(os.Args[2])
|
|
if len(os.Args) >= 4 {
|
|
dxcc, _ = strconv.Atoi(os.Args[3])
|
|
}
|
|
}
|
|
|
|
fmt.Println("DB path:", dbPath)
|
|
dsn := "file:" + dbPath + "?_pragma=foreign_keys(on)"
|
|
conn, err := sql.Open("sqlite", dsn)
|
|
must("open", err)
|
|
defer conn.Close()
|
|
must("ping", conn.Ping())
|
|
|
|
if mode == "wb" {
|
|
probeWB(conn, call, dxcc)
|
|
probeCache(conn, call)
|
|
return
|
|
}
|
|
summary(conn)
|
|
}
|
|
|
|
func probeCache(conn *sql.DB, call string) {
|
|
fmt.Println("\nCache row for", call, ":")
|
|
row := conn.QueryRow(`SELECT name, qth, country, grid, dxcc, source, fetched_at
|
|
FROM callsign_cache WHERE callsign = ?`, call)
|
|
var name, qth, country, grid, src, fetched sql.NullString
|
|
var dxcc sql.NullInt64
|
|
if err := row.Scan(&name, &qth, &country, &grid, &dxcc, &src, &fetched); err != nil {
|
|
fmt.Println(" (no cache row:", err, ")")
|
|
return
|
|
}
|
|
fmt.Printf(" name=%q qth=%q country=%q grid=%q\n", name.String, qth.String, country.String, grid.String)
|
|
fmt.Printf(" dxcc=%v(valid=%v) source=%s fetched_at=%s\n", dxcc.Int64, dxcc.Valid, src.String, fetched.String)
|
|
}
|
|
|
|
func probeWB(conn *sql.DB, call string, dxcc int) {
|
|
fmt.Printf("\nProbing WorkedBefore for call=%q dxcc=%d\n", call, dxcc)
|
|
|
|
var count int
|
|
must("count call", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE callsign = ?`, call).Scan(&count))
|
|
fmt.Printf(" count(callsign=%s) = %d\n", call, count)
|
|
|
|
if dxcc == 0 {
|
|
var d sql.NullInt64
|
|
_ = conn.QueryRow(`SELECT dxcc FROM qso
|
|
WHERE callsign = ? AND dxcc IS NOT NULL
|
|
ORDER BY qso_date DESC LIMIT 1`, call).Scan(&d)
|
|
if d.Valid {
|
|
dxcc = int(d.Int64)
|
|
fmt.Printf(" inferred dxcc from prior QSO: %d\n", dxcc)
|
|
}
|
|
}
|
|
if dxcc == 0 {
|
|
fmt.Println(" no DXCC available — skipping entity stats")
|
|
return
|
|
}
|
|
|
|
var dxccCount int
|
|
must("count dxcc", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc = ?`, dxcc).Scan(&dxccCount))
|
|
fmt.Printf(" count(dxcc=%d) = %d\n", dxcc, dxccCount)
|
|
|
|
// Distinct (band, mode) for this DXCC
|
|
rows, err := conn.Query(`SELECT band, mode, COUNT(*) FROM qso WHERE dxcc = ? GROUP BY band, mode ORDER BY band, mode`, dxcc)
|
|
must("group by band/mode", err)
|
|
defer rows.Close()
|
|
fmt.Printf(" distinct (band, mode) groups for dxcc=%d:\n", dxcc)
|
|
var n int
|
|
for rows.Next() {
|
|
var band, mode string
|
|
var c int
|
|
rows.Scan(&band, &mode, &c)
|
|
fmt.Printf(" %-8s %-12s %d QSOs\n", band, mode, c)
|
|
n++
|
|
}
|
|
if n == 0 {
|
|
fmt.Println(" (none)")
|
|
}
|
|
|
|
// Same query as backend
|
|
fmt.Println("\n --- exact backend query ---")
|
|
rows2, err := conn.Query(`
|
|
SELECT band, mode,
|
|
MAX(CASE WHEN callsign = ? THEN 1 ELSE 0 END),
|
|
MAX(CASE WHEN 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 = ?
|
|
GROUP BY band, mode
|
|
ORDER BY band, mode`, call, call, dxcc)
|
|
must("backend query", err)
|
|
defer rows2.Close()
|
|
n = 0
|
|
for rows2.Next() {
|
|
var band, mode string
|
|
var cw, cc, dc int
|
|
rows2.Scan(&band, &mode, &cw, &cc, &dc)
|
|
fmt.Printf(" %-8s %-12s callW=%d callC=%d dxccC=%d\n", band, mode, cw, cc, dc)
|
|
n++
|
|
}
|
|
fmt.Printf(" → %d rows\n", n)
|
|
}
|
|
|
|
func summary(conn *sql.DB) {
|
|
if st, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir("a")))), "")); err == nil {
|
|
fmt.Printf("(size info skipped) %v\n", st)
|
|
}
|
|
|
|
fmt.Println("Applied migrations:")
|
|
rows, err := conn.Query(`SELECT name FROM schema_migrations ORDER BY name`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var n string
|
|
rows.Scan(&n)
|
|
fmt.Println(" -", n)
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
var n int64
|
|
conn.QueryRow(`SELECT COUNT(*) FROM qso`).Scan(&n)
|
|
fmt.Println("\nQSO rows:", n)
|
|
|
|
var withDxcc int64
|
|
conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc IS NOT NULL`).Scan(&withDxcc)
|
|
fmt.Printf("Rows with non-null dxcc: %d (%.1f%%)\n", withDxcc, float64(withDxcc)/float64(n)*100)
|
|
|
|
var nDistinctDXCC int
|
|
conn.QueryRow(`SELECT COUNT(DISTINCT dxcc) FROM qso WHERE dxcc IS NOT NULL`).Scan(&nDistinctDXCC)
|
|
fmt.Println("Distinct DXCC entities in log:", nDistinctDXCC)
|
|
|
|
fmt.Println("\nTry: go run ./cmd/dbdiag wb 4X6TT")
|
|
fmt.Println(" go run ./cmd/dbdiag wb 4X6TT 336")
|
|
_ = context.Background
|
|
_ = sort.Strings
|
|
must("dummy", nil)
|
|
}
|
|
|
|
func must(label string, err error) {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, label+":", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|