fix(backup): back up the contacts (logbook), not just the settings db

Since the logbook was split into its own SQLite file, backup.Run was still
snapshotting a.db (the settings/config database) — so the scheduled and manual
backups silently stopped including the QSOs. The operator was backing up config
and thinking it was their log.

Track the resolved logbook file path (a.logDbPath, set in connectLogbook) and
route the backup through a new runConfiguredBackup: the CONTACTS become the
primary "opslog-*" backup (the logbook file on SQLite, an ADIF export on MySQL),
and the settings/config db is snapshotted separately as "opslogcfg-*" so nothing
is lost. backup.Run takes a name prefix; the two sets rotate independently.
This commit is contained in:
2026-08-06 23:22:04 +02:00
parent 6e2e2cc3aa
commit e2bfe73bdb
3 changed files with 45 additions and 35 deletions
+35 -24
View File
@@ -644,6 +644,7 @@ type App struct {
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
logDbPath string // resolved SQLite file actually backing logDb ("" on MySQL, or when the settings db serves as the logbook) — the file the backup snapshots
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
@@ -1066,6 +1067,7 @@ func (a *App) startup(ctx context.Context) {
applog.Printf("startup: logbook open failed (%v) — falling back to SQLite logbook", lerr)
a.dbBackendErr = strings.TrimPrefix(lerr.Error(), "")
logbookConn, backend = conn, "sqlite"
a.logDbPath = "" // fell back to the settings db as the logbook — backup snapshots a.db
}
a.dbBackend = backend
// db.Dialect describes the LOGBOOK backend — the only place SQL actually
@@ -1523,14 +1525,9 @@ func (a *App) runBackupForShutdown() error {
if done {
return nil
}
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, s.Rotation, s.Zip); err != nil {
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
return err
}
if mysql {
if _, err := a.backupLogADIF(folder, s.Rotation, s.Zip); err != nil {
return err
}
}
return a.settings.Set(a.ctx, keyBackupLast, time.Now().UTC().Format(time.RFC3339))
}
@@ -2053,6 +2050,7 @@ func (a *App) connectLogbook(cfg profile.ProfileDB) (*sql.DB, string, error) {
if err != nil {
return nil, "", err
}
a.logDbPath = "" // MySQL: no local file to snapshot (the log is exported to ADIF instead)
return c, "mysql", nil
}
// SQLite logbook FILE, separate from the settings/config database. A profile
@@ -2066,6 +2064,7 @@ func (a *App) connectLogbook(cfg profile.ProfileDB) (*sql.DB, string, error) {
lp = a.logbookPath
}
if lp == "" {
a.logDbPath = "" // settings db serves as the logbook (split failed) — backup snapshots a.db
return a.db, "sqlite", nil
}
// Resolve against THIS install before opening. Without it, a profile carried
@@ -2081,6 +2080,7 @@ func (a *App) connectLogbook(cfg profile.ProfileDB) (*sql.DB, string, error) {
if err != nil {
return nil, "", fmt.Errorf("open logbook %s: %w", lp, err)
}
a.logDbPath = lp // the SQLite file the backup snapshots for the contacts
return c, "sqlite", nil
}
@@ -11815,6 +11815,33 @@ func (a *App) SaveBackupSettings(s BackupSettings) error {
return nil
}
// runConfiguredBackup writes the backup set to folder: the CONTACTS as the
// primary "opslog-*" backup, plus a separate "opslogcfg-*" snapshot of the
// settings/config db, so neither is lost. The contacts are what the user means
// by "the log": on SQLite they live in the split-out logbook file (a.logDbPath),
// NOT the settings db — the old code snapshotted a.db and so silently stopped
// backing up the QSOs once the logbook was split out. On MySQL the contacts
// aren't in a local file, so they're exported to ADIF instead. Returns the path
// of the contacts backup.
func (a *App) runConfiguredBackup(folder string, rotation int, zip bool) (string, error) {
// Config snapshot (profiles, hardware, awards). Best-effort — a config-backup
// failure must never stop the contacts from being protected.
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, rotation, zip, "opslogcfg"); err != nil {
applog.Printf("backup: config snapshot failed: %v", err)
}
if a.dbBackend == "mysql" {
// The live log is on MySQL; VACUUM INTO can't reach it — export to ADIF.
return a.backupLogADIF(folder, rotation, zip)
}
// SQLite: snapshot the logbook file that holds the contacts. Fall back to the
// settings db only when it doubles as the logbook (rare split-failure case).
conn, path := a.logDb, a.logDbPath
if conn == nil || path == "" {
conn, path = a.db, a.dbPath
}
return backup.Run(a.ctx, conn, path, folder, rotation, zip, "opslog")
}
// RunBackupNow forces an immediate backup using the persisted settings.
// Returns the destination path of the file that was written.
func (a *App) RunBackupNow() (string, error) {
@@ -11826,20 +11853,10 @@ func (a *App) RunBackupNow() (string, error) {
if folder == "" {
folder = s.DefaultFolder
}
// Always snapshot the local SQLite (config + any pre-MySQL local QSOs).
path, err := backup.Run(a.ctx, a.db, a.dbPath, folder, s.Rotation, s.Zip)
path, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip)
if err != nil {
return path, err
}
// On MySQL the live QSO log isn't in the local DB — export it to ADIF so the
// contacts are actually protected. The ADIF path is the one we surface.
if a.dbBackend == "mysql" {
adiPath, aerr := a.backupLogADIF(folder, s.Rotation, s.Zip)
if aerr != nil {
return adiPath, aerr
}
path = adiPath
}
a.setSetting(keyBackupLast, time.Now().UTC().Format(time.RFC3339))
return path, nil
}
@@ -11885,16 +11902,10 @@ func (a *App) maybeShutdownBackup() {
if done {
return
}
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, s.Rotation, s.Zip); err != nil {
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
fmt.Println("OpsLog: shutdown backup failed:", err)
return
}
if mysql {
if _, err := a.backupLogADIF(folder, s.Rotation, s.Zip); err != nil {
fmt.Println("OpsLog: shutdown ADIF log backup failed:", err)
return
}
}
a.setSetting(keyBackupLast, time.Now().UTC().Format(time.RFC3339))
}