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:
@@ -295,6 +295,7 @@ const (
|
|||||||
keyBackupFolder = "backup.folder"
|
keyBackupFolder = "backup.folder"
|
||||||
keyBackupRotation = "backup.rotation"
|
keyBackupRotation = "backup.rotation"
|
||||||
keyBackupZip = "backup.zip"
|
keyBackupZip = "backup.zip"
|
||||||
|
keyBackupEveryExit = "backup.every_exit" // "1" → back up on every quit, not just once/day
|
||||||
keyBackupLast = "backup.last_at"
|
keyBackupLast = "backup.last_at"
|
||||||
|
|
||||||
keyQSLDefaultQSLSent = "qsl.qsl_sent"
|
keyQSLDefaultQSLSent = "qsl.qsl_sent"
|
||||||
@@ -1450,7 +1451,8 @@ func (a *App) plannedShutdownSteps() []shutdownStep {
|
|||||||
if folder == "" {
|
if folder == "" {
|
||||||
folder = s.DefaultFolder
|
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"})
|
out = append(out, shutdownStep{ID: "backup", Label: "Backing up database", Status: "pending"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1530,7 +1532,7 @@ func (a *App) runBackupForShutdown() error {
|
|||||||
if mysql {
|
if mysql {
|
||||||
done = backup.HasADIFBackupToday(folder)
|
done = backup.HasADIFBackupToday(folder)
|
||||||
}
|
}
|
||||||
if done {
|
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
||||||
@@ -11775,6 +11777,7 @@ type BackupSettings struct {
|
|||||||
Folder string `json:"folder"`
|
Folder string `json:"folder"`
|
||||||
Rotation int `json:"rotation"`
|
Rotation int `json:"rotation"`
|
||||||
Zip bool `json:"zip"`
|
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"`
|
LastBackupAt string `json:"last_backup_at"`
|
||||||
DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint
|
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
|
return out, nil
|
||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx,
|
m, err := a.settings.GetMany(a.ctx,
|
||||||
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupLast)
|
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupLast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -11799,6 +11802,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) {
|
|||||||
out.Rotation = n
|
out.Rotation = n
|
||||||
}
|
}
|
||||||
out.Zip = m[keyBackupZip] == "1"
|
out.Zip = m[keyBackupZip] == "1"
|
||||||
|
out.EveryExit = m[keyBackupEveryExit] == "1"
|
||||||
out.LastBackupAt = m[keyBackupLast]
|
out.LastBackupAt = m[keyBackupLast]
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -11825,6 +11829,7 @@ func (a *App) SaveBackupSettings(s BackupSettings) error {
|
|||||||
keyBackupFolder: strings.TrimSpace(s.Folder),
|
keyBackupFolder: strings.TrimSpace(s.Folder),
|
||||||
keyBackupRotation: strconv.Itoa(s.Rotation),
|
keyBackupRotation: strconv.Itoa(s.Rotation),
|
||||||
keyBackupZip: doZip,
|
keyBackupZip: doZip,
|
||||||
|
keyBackupEveryExit: boolStr(s.EveryExit),
|
||||||
} {
|
} {
|
||||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -11917,7 +11922,7 @@ func (a *App) maybeShutdownBackup() {
|
|||||||
if mysql {
|
if mysql {
|
||||||
done = backup.HasADIFBackupToday(folder)
|
done = backup.HasADIFBackupToday(folder)
|
||||||
}
|
}
|
||||||
if done {
|
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
||||||
|
|||||||
+6
-2
@@ -2,8 +2,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.23.9",
|
"version": "0.23.9",
|
||||||
"date": "",
|
"date": "",
|
||||||
"en": [],
|
"en": [
|
||||||
"fr": []
|
"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",
|
"version": "0.23.8",
|
||||||
|
|||||||
@@ -4361,13 +4361,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer pb-2">
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!backupCfg.zip}
|
checked={!!backupCfg.zip}
|
||||||
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, zip: !!c }))}
|
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, zip: !!c }))}
|
||||||
/>
|
/>
|
||||||
<span>{t('bk.zip')}</span>
|
<span>{t('bk.zip')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer pb-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={!!(backupCfg as any).every_exit}
|
||||||
|
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, every_exit: !!c } as any))}
|
||||||
|
/>
|
||||||
|
<span>{t('bk.everyExit')}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-border/60 pt-3 flex items-center gap-3">
|
<div className="border-t border-border/60 pt-3 flex items-center gap-3">
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const en: Dict = {
|
|||||||
'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.',
|
'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.',
|
||||||
'bk.hint': 'OpsLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones.',
|
'bk.hint': 'OpsLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones.',
|
||||||
'bk.auto': 'Automatic backup when closing OpsLog (max once per day)', 'bk.folder': 'Backup folder', 'bk.folderPh': 'leave empty for default', 'bk.browse': 'Browse…',
|
'bk.auto': 'Automatic backup when closing OpsLog (max once per day)', 'bk.folder': 'Backup folder', 'bk.folderPh': 'leave empty for default', 'bk.browse': 'Browse…',
|
||||||
'bk.effective': 'Effective folder:', 'bk.defaultUse': 'If empty, OpsLog uses the default:', 'bk.rotation': 'Rotation (copies to keep)', 'bk.zip': 'ZIP backup (smaller file)',
|
'bk.effective': 'Effective folder:', 'bk.defaultUse': 'If empty, OpsLog uses the default:', 'bk.rotation': 'Rotation (copies to keep)', 'bk.zip': 'ZIP backup (smaller file)', 'bk.everyExit': 'Back up on every exit (not just once a day)',
|
||||||
'bk.lastRun': 'Last run:', 'bk.never': 'never', 'bk.backupNow': 'Back up now', 'bk.backingUp': 'Backing up…', 'bk.writtenTo': 'Backup written to',
|
'bk.lastRun': 'Last run:', 'bk.never': 'never', 'bk.backupNow': 'Back up now', 'bk.backingUp': 'Backing up…', 'bk.writtenTo': 'Backup written to',
|
||||||
// Section hints (hardware/software panel headers)
|
// Section hints (hardware/software panel headers)
|
||||||
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
||||||
@@ -681,7 +681,7 @@ const fr: Dict = {
|
|||||||
'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.",
|
'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.",
|
||||||
'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.",
|
'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.",
|
||||||
'bk.auto': 'Sauvegarde auto à la fermeture d\'OpsLog (max 1×/jour)', 'bk.folder': 'Dossier de sauvegarde', 'bk.folderPh': 'vide = dossier par défaut', 'bk.browse': 'Parcourir…',
|
'bk.auto': 'Sauvegarde auto à la fermeture d\'OpsLog (max 1×/jour)', 'bk.folder': 'Dossier de sauvegarde', 'bk.folderPh': 'vide = dossier par défaut', 'bk.browse': 'Parcourir…',
|
||||||
'bk.effective': 'Dossier effectif :', 'bk.defaultUse': 'Si vide, OpsLog utilise le défaut :', 'bk.rotation': 'Rotation (copies à garder)', 'bk.zip': 'Sauvegarde ZIP (fichier plus petit)',
|
'bk.effective': 'Dossier effectif :', 'bk.defaultUse': 'Si vide, OpsLog utilise le défaut :', 'bk.rotation': 'Rotation (copies à garder)', 'bk.zip': 'Sauvegarde ZIP (fichier plus petit)', 'bk.everyExit': 'Sauvegarder à chaque fermeture (pas une fois par jour)',
|
||||||
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
||||||
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
||||||
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
||||||
|
|||||||
@@ -1894,6 +1894,7 @@ export namespace main {
|
|||||||
folder: string;
|
folder: string;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
zip: boolean;
|
zip: boolean;
|
||||||
|
every_exit: boolean;
|
||||||
last_backup_at: string;
|
last_backup_at: string;
|
||||||
default_folder: string;
|
default_folder: string;
|
||||||
|
|
||||||
@@ -1907,6 +1908,7 @@ export namespace main {
|
|||||||
this.folder = source["folder"];
|
this.folder = source["folder"];
|
||||||
this.rotation = source["rotation"];
|
this.rotation = source["rotation"];
|
||||||
this.zip = source["zip"];
|
this.zip = source["zip"];
|
||||||
|
this.every_exit = source["every_exit"];
|
||||||
this.last_backup_at = source["last_backup_at"];
|
this.last_backup_at = source["last_backup_at"];
|
||||||
this.default_folder = source["default_folder"];
|
this.default_folder = source["default_folder"];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user