Files
OpsLog/cmd/dbdiag/main.go
T
rouggy 7ace2cc602 Initial codebase: Go + Wails amateur radio logbook
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>
2026-05-26 00:16:45 +02:00

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)
}
}