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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user