feat(backup): option to back up on every exit, not just once a day

New BackupSettings.EveryExit (keyBackupEveryExit) with a Settings → Backup
checkbox. When on, the three shutdown-backup gates (plannedShutdownSteps,
runBackupForShutdown, maybeShutdownBackup) bypass the HasBackupToday /
HasADIFBackupToday "already done today" check and always run, so a second
session's QSOs are captured instead of skipped until the next day. The dated
backup file for today is refreshed (overwritten) each exit; prior days are still
kept by rotation.
This commit is contained in:
2026-08-07 09:46:23 +02:00
parent 970127cc16
commit 5338ad0b29
5 changed files with 62 additions and 44 deletions
+44 -39
View File
@@ -291,11 +291,12 @@ const (
keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on
keyBackupEnabled = "backup.enabled"
keyBackupFolder = "backup.folder"
keyBackupRotation = "backup.rotation"
keyBackupZip = "backup.zip"
keyBackupLast = "backup.last_at"
keyBackupEnabled = "backup.enabled"
keyBackupFolder = "backup.folder"
keyBackupRotation = "backup.rotation"
keyBackupZip = "backup.zip"
keyBackupEveryExit = "backup.every_exit" // "1" → back up on every quit, not just once/day
keyBackupLast = "backup.last_at"
keyQSLDefaultQSLSent = "qsl.qsl_sent"
keyQSLDefaultQSLRcvd = "qsl.qsl_rcvd"
@@ -569,8 +570,8 @@ type App struct {
// wcbm is an in-memory "CALL|BAND|MODE" worked-index so the alert engine never
// queries the DB per cluster spot (an FT8 firehose would swamp a remote MySQL).
// Loaded once, appended to on each log, rebuilt after bulk changes.
wcbm map[string]struct{}
wcbmMu sync.RWMutex
wcbm map[string]struct{}
wcbmMu sync.RWMutex
// clusterStatusIdx caches the whole-logbook maps ClusterSpotStatuses colours
// spots against (worked entities/calls/counties/POTA/prefixes). Building them
// per spot batch re-scanned the entire logbook ~20×/second under an RBN
@@ -580,30 +581,30 @@ type App struct {
clusterStatusIdx *clusterStatusCache
clusterStatusMu sync.Mutex
pota *pota.Cache
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
awardRefs *awardref.Repo
qslTemplates *qslcard.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
motorInhibStop chan struct{} // stops the "inhibit TX while moving" loop; nil when off
motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window)
motorInhibited atomic.Bool // TX currently inhibited by the motor-antenna watcher
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
tunergenius *tunergenius.Client // Tuner Genius XL (4O3A) ATU (TCP); nil when disabled
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
spe *spe.Client // legacy pointer: FIRST enabled SPE amp (kept for the pre-multi bindings)
acom *acom.Client // legacy pointer: FIRST enabled ACOM amp
ampsMu sync.Mutex // guards ampInsts
ampInsts map[string]*ampInst // one running client per enabled configured amplifier, by config ID
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
awardRefs *awardref.Repo
qslTemplates *qslcard.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
motorInhibStop chan struct{} // stops the "inhibit TX while moving" loop; nil when off
motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window)
motorInhibited atomic.Bool // TX currently inhibited by the motor-antenna watcher
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
tunergenius *tunergenius.Client // Tuner Genius XL (4O3A) ATU (TCP); nil when disabled
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
spe *spe.Client // legacy pointer: FIRST enabled SPE amp (kept for the pre-multi bindings)
acom *acom.Client // legacy pointer: FIRST enabled ACOM amp
ampsMu sync.Mutex // guards ampInsts
ampInsts map[string]*ampInst // one running client per enabled configured amplifier, by config ID
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
// qsoRecManual marks a take the operator started by hand while automatic
// recording is OFF. Such a take opened the sound devices itself, so it must
// close them again when it ends — an operator who records one contact does
@@ -1450,7 +1451,8 @@ func (a *App) plannedShutdownSteps() []shutdownStep {
if folder == "" {
folder = s.DefaultFolder
}
if !backup.HasBackupToday(folder) {
// "Back up on every exit" bypasses the once-a-day gate.
if s.EveryExit || !backup.HasBackupToday(folder) {
out = append(out, shutdownStep{ID: "backup", Label: "Backing up database", Status: "pending"})
}
}
@@ -1530,7 +1532,7 @@ func (a *App) runBackupForShutdown() error {
if mysql {
done = backup.HasADIFBackupToday(folder)
}
if done {
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
return nil
}
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
@@ -11775,6 +11777,7 @@ type BackupSettings struct {
Folder string `json:"folder"`
Rotation int `json:"rotation"`
Zip bool `json:"zip"`
EveryExit bool `json:"every_exit"` // back up on every quit, not just the first of the day
LastBackupAt string `json:"last_backup_at"`
DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint
}
@@ -11789,7 +11792,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) {
return out, nil
}
m, err := a.settings.GetMany(a.ctx,
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupLast)
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupLast)
if err != nil {
return out, err
}
@@ -11799,6 +11802,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) {
out.Rotation = n
}
out.Zip = m[keyBackupZip] == "1"
out.EveryExit = m[keyBackupEveryExit] == "1"
out.LastBackupAt = m[keyBackupLast]
return out, nil
}
@@ -11821,10 +11825,11 @@ func (a *App) SaveBackupSettings(s BackupSettings) error {
doZip = "1"
}
for k, v := range map[string]string{
keyBackupEnabled: enabled,
keyBackupFolder: strings.TrimSpace(s.Folder),
keyBackupRotation: strconv.Itoa(s.Rotation),
keyBackupZip: doZip,
keyBackupEnabled: enabled,
keyBackupFolder: strings.TrimSpace(s.Folder),
keyBackupRotation: strconv.Itoa(s.Rotation),
keyBackupZip: doZip,
keyBackupEveryExit: boolStr(s.EveryExit),
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
@@ -11917,7 +11922,7 @@ func (a *App) maybeShutdownBackup() {
if mysql {
done = backup.HasADIFBackupToday(folder)
}
if done {
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
return
}
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {