This commit is contained in:
2026-06-14 01:35:40 +02:00
parent 67203cd4a8
commit 29fd832bcd
7 changed files with 290 additions and 58 deletions
+103 -27
View File
@@ -556,31 +556,10 @@ func (a *App) startup(ctx context.Context) {
}
a.db = conn
// Choose where the QSO logbook lives. On a MySQL failure we fall back to the
// local SQLite logbook so the operator can still log (and fix the config).
logbookConn := conn
if mb := readBootstrap(dataDir).MySQL; mb != nil && mb.Enabled {
applog.Printf("startup: logbook backend = MySQL (%s:%d/%s)", mb.Host, mb.Port, mb.Database)
mysqlConn, mErr := db.OpenMySQL(db.MySQLConfig{
Host: mb.Host, Port: mb.Port, User: mb.User, Password: mb.Password, Database: mb.Database,
})
if mErr != nil {
applog.Printf("startup: MySQL open failed (%v) — falling back to SQLite logbook", mErr)
a.dbBackendErr = "MySQL: " + mErr.Error()
} else {
logbookConn = mysqlConn
a.dbBackend = "mysql"
}
}
if a.dbBackend == "" {
a.dbBackend = "sqlite"
}
// db.Dialect describes the LOGBOOK backend — the only place SQL actually
// varies (qso JSON extraction). Config repos always run on SQLite.
db.SetDialect(a.dbBackend)
applog.Printf("startup: logbook backend = %s", a.dbBackend)
a.logDb = logbookConn
a.qso = qso.NewRepo(logbookConn)
// Wire the LOCAL config repos first — they're backed by the already-open
// SQLite file, so the station/profiles/settings are ready instantly. Doing
// this BEFORE the (possibly slow, remote) MySQL logbook connect means the UI
// doesn't briefly think the station is unconfigured while MySQL is dialing.
a.settings = settings.NewStore(conn)
a.settings.SetSensitivePredicate(isSensitiveSetting) // encrypt passwords at rest when a passphrase is set
a.profiles = profile.NewRepo(conn)
@@ -609,6 +588,32 @@ func (a *App) startup(ctx context.Context) {
a.lookup = lookup.NewManager(a.cache)
a.reloadLookupProviders()
// Now choose where the QSO logbook lives. On a MySQL failure we fall back to
// the local SQLite logbook so the operator can still log (and fix config).
logbookConn := conn
if mb := readBootstrap(dataDir).MySQL; mb != nil && mb.Enabled {
applog.Printf("startup: logbook backend = MySQL (%s:%d/%s)", mb.Host, mb.Port, mb.Database)
mysqlConn, mErr := db.OpenMySQL(db.MySQLConfig{
Host: mb.Host, Port: mb.Port, User: mb.User, Password: mb.Password, Database: mb.Database,
})
if mErr != nil {
applog.Printf("startup: MySQL open failed (%v) — falling back to SQLite logbook", mErr)
a.dbBackendErr = "MySQL: " + mErr.Error()
} else {
logbookConn = mysqlConn
a.dbBackend = "mysql"
}
}
if a.dbBackend == "" {
a.dbBackend = "sqlite"
}
// db.Dialect describes the LOGBOOK backend — the only place SQL actually
// varies (qso JSON extraction). Config repos always run on SQLite.
db.SetDialect(a.dbBackend)
applog.Printf("startup: logbook backend = %s", a.dbBackend)
a.logDb = logbookConn
a.qso = qso.NewRepo(logbookConn)
// cty.dat for offline DXCC / country resolution. Cached on disk; first
// run downloads it from country-files.com in the background so startup
// stays fast even if the network is slow.
@@ -877,12 +882,22 @@ func (a *App) runBackupForShutdown() error {
if folder == "" {
folder = s.DefaultFolder
}
if backup.HasBackupToday(folder) {
mysql := a.dbBackend == "mysql"
done := backup.HasBackupToday(folder)
if mysql {
done = backup.HasADIFBackupToday(folder)
}
if done {
return nil
}
if _, err := backup.Run(a.ctx, a.db, a.dbPath, 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))
}
@@ -1103,6 +1118,30 @@ func (a *App) GetDBBackendStatus() DBBackendStatus {
}
}
// DBConnectionInfo is a compact description of where the QSO logbook lives, for
// the status bar: a MySQL server endpoint or the local SQLite file path.
type DBConnectionInfo struct {
Backend string `json:"backend"` // "sqlite" | "mysql"
Label string `json:"label"` // "host:port/database" or the .db path
}
// GetDBConnectionInfo reports the logbook connection for display in the status
// bar. For MySQL it shows host:port/database (the shared logbook); for SQLite
// it shows the local database file path.
func (a *App) GetDBConnectionInfo() DBConnectionInfo {
if a.dbBackend == "mysql" {
if mb := readBootstrap(a.dataDir).MySQL; mb != nil {
port := mb.Port
if port == 0 {
port = 3306
}
return DBConnectionInfo{Backend: "mysql", Label: fmt.Sprintf("%s:%d/%s", mb.Host, port, mb.Database)}
}
return DBConnectionInfo{Backend: "mysql", Label: "MySQL"}
}
return DBConnectionInfo{Backend: "sqlite", Label: a.dbPath}
}
// GetMySQLSettings returns the stored shared-database config from the bootstrap
// file (config.json), with defaults applied. Read before the DB is open, so it
// must not depend on the settings table.
@@ -5748,14 +5787,38 @@ 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)
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
}
// backupLogADIF writes a rotating ADIF export of the (MySQL) logbook into the
// backup folder. The full set of ADIF + app fields is included so the backup is
// a complete, re-importable copy of the log.
func (a *App) backupLogADIF(folder string, rotation int, zip bool) (string, error) {
if a.qso == nil {
return "", fmt.Errorf("logbook not initialized")
}
return backup.RunADIF(folder, rotation, zip, func(p string) error {
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1", IncludeAppFields: true}
_, e := ex.ExportFile(a.ctx, p)
return e
})
}
// maybeShutdownBackup runs a backup at shutdown if the user enabled it
// and no backup for today already exists. Running at shutdown (not at
// startup) means the snapshot includes the QSOs the user just logged
@@ -5773,13 +5836,26 @@ func (a *App) maybeShutdownBackup() {
if folder == "" {
folder = s.DefaultFolder
}
if backup.HasBackupToday(folder) {
mysql := a.dbBackend == "mysql"
// In MySQL mode the ADIF log export is the backup that matters; gate the
// "already done today" skip on whichever backup type applies.
done := backup.HasBackupToday(folder)
if mysql {
done = backup.HasADIFBackupToday(folder)
}
if done {
return
}
if _, err := backup.Run(a.ctx, a.db, a.dbPath, 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))
}