diff --git a/app.go b/app.go index 73cef4a..e6a7d5c 100644 --- a/app.go +++ b/app.go @@ -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 { diff --git a/changelog.json b/changelog.json index 37f9333..44b72eb 100644 --- a/changelog.json +++ b/changelog.json @@ -2,8 +2,12 @@ { "version": "0.23.9", "date": "", - "en": [], - "fr": [] + "en": [ + "Backup: new \"Back up on every exit\" option (Settings → Backup). With it on, OpsLog backs up the database each time you quit instead of only the first time of the day — so a second session's QSOs are always captured." + ], + "fr": [ + "Sauvegarde : nouvelle option \"Sauvegarder à chaque fermeture\" (Réglages → Sauvegarde). Activée, OpsLog sauvegarde la base à chaque fois que tu quittes au lieu d'une seule fois par jour — les QSO d'une seconde session sont ainsi toujours pris." + ] }, { "version": "0.23.8", diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 2f53d9d..862d4f7 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -4361,13 +4361,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan }} /> -