chore: release v0.26.4
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user