chore: release v0.26.4

This commit is contained in:
2026-08-22 08:35:49 +02:00
parent d48420485f
commit abfed4afa2
16 changed files with 864 additions and 23 deletions
+67 -3
View File
@@ -1209,6 +1209,7 @@ func (a *App) startup(ctx context.Context) {
applog.Printf("startup: logbook backend = %s", backend)
a.logDb = logbookConn
a.qso = qso.NewRepo(logbookConn)
a.tidyLogbookSchema(logbookConn)
a.backfillAwardRefsOnce() // one-time: materialise award_refs for pre-existing QSOs
go a.rebuildWorkedIndex() // in-memory worked-index for per-spot alert checks
go a.adifMonitorLoop() // watch external ADIF files (fldigi, N1MM…) for new QSOs
@@ -2319,6 +2320,11 @@ func (a *App) connectLogbook(cfg profile.ProfileDB) (*sql.DB, string, error) {
}
if lp == "" {
a.logDbPath = "" // settings db serves as the logbook (split failed) — backup snapshots a.db
// It may have had its unused qso table dropped while a separate logbook
// was in use (see tidyLogbookSchema); rebuild it before handing it over.
if err := db.EnsureQSOTable(a.db); err != nil {
return nil, "", err
}
return a.db, "sqlite", nil
}
// Resolve against THIS install before opening. Without it, a profile carried
@@ -2330,7 +2336,7 @@ func (a *App) connectLogbook(cfg profile.ProfileDB) (*sql.DB, string, error) {
applog.Printf("logbook: profile path %q resolved to %q", lp, r)
lp = r
}
c, err := db.Open(lp)
c, err := db.OpenLogbook(lp)
if err != nil {
return nil, "", fmt.Errorf("open logbook %s: %w", lp, err)
}
@@ -14875,7 +14881,9 @@ func (a *App) reloadCAT() {
// Clicking one of our spots on the panadapter fills the entry form.
fb.OnSpotClick = func(call string, hz int64, mode string) {
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "flex:spot_clicked", map[string]any{"call": call, "freq_hz": hz})
wruntime.EventsEmit(a.ctx, "flex:spot_clicked", map[string]any{
"call": call, "freq_hz": hz, "pota_ref": a.potaRefFor(call),
})
}
// A spot clicked ON the panadapter deserves the same zoom as one
// clicked in the band map: the operator did the same thing and is
@@ -14970,7 +14978,9 @@ func (a *App) reloadCAT() {
// Clicking one of our spots on the ExpertSDR panorama fills the entry form.
tb.OnSpotClick = func(call string, hz int64) {
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "tci:spot_clicked", map[string]any{"call": call, "freq_hz": hz})
wruntime.EventsEmit(a.ctx, "tci:spot_clicked", map[string]any{
"call": call, "freq_hz": hz, "pota_ref": a.potaRefFor(call),
})
}
}
a.cat.Start(tb)
@@ -19993,3 +20003,57 @@ func lookupWhen(qsoDate string) time.Time {
}
return t
}
// potaRefFor returns the park a callsign is currently activating, or "".
//
// A spot clicked on the RADIO's panadapter arrives as a callsign and a
// frequency — the radio knows nothing of parks — so the park has to be found
// again here, from the same live POTA index that tags the cluster spots. Without
// it the Awards tab stayed empty for a click on the panadapter while the very
// same spot clicked in OpsLog filled it in, which is a difference the operator
// has no way to explain.
func (a *App) potaRefFor(call string) string {
if a.pota == nil || strings.TrimSpace(call) == "" {
return ""
}
if info, ok := a.pota.Lookup(call); ok {
return info.Reference
}
return ""
}
// tidyLogbookSchema removes settings tables that an older OpsLog left inside the
// logbook.
//
// Two ways they got there: every database used to receive the full migration set
// (fixed in internal/db/roles.go, which now filters by role), and the one-time
// legacy split copies the whole settings database with VACUUM INTO and then
// treats the copy as the logbook. Empty ones are already dropped when the
// logbook is opened; the copies from the split have ROWS, and are dropped here
// because we can see the settings database still holding the originals.
//
// The check is the whole point: only when the logbook is a DIFFERENT database
// from the settings one, and that settings database still has its own profiles,
// is a profile row inside the logbook provably a duplicate. A profile pointing
// at a genuine legacy combined database as its logbook fails that test and keeps
// everything.
func (a *App) tidyLogbookSchema(logbookConn *sql.DB) {
if logbookConn == nil || a.db == nil || logbookConn == a.db {
return // the settings database IS the logbook: everything in it is in use
}
if a.dbBackend == "sqlite" && a.logDbPath != "" &&
strings.EqualFold(filepath.Clean(a.logDbPath), filepath.Clean(a.dbPath)) {
return // same file reached twice
}
var profiles int
if err := a.db.QueryRowContext(a.ctx, "SELECT COUNT(*) FROM station_profiles").Scan(&profiles); err != nil || profiles == 0 {
// No authoritative copy to point at — leave the logbook exactly as it is.
return
}
db.DropRedundantSettingsTables(logbookConn, "logbook")
// And the mirror image: the settings database keeps an empty qso table from
// the days when every database got the whole schema. It is not needed while
// the contacts have a database of their own, and EnsureQSOTable puts it back
// if this one is ever asked to serve as the logbook again.
db.DropEmptyQSOTable(a.db, filepath.Base(a.dbPath))
}