diff --git a/app.go b/app.go index 7f1c308..b48f29b 100644 --- a/app.go +++ b/app.go @@ -296,6 +296,7 @@ const ( keyBackupRotation = "backup.rotation" keyBackupZip = "backup.zip" keyBackupEveryExit = "backup.every_exit" // "1" → back up on every quit, not just once/day + keyBackupKeepAll = "backup.keep_all" // "1" → each backup is a distinct time-stamped file (no same-day overwrite) keyBackupLast = "backup.last_at" keyQSLDefaultQSLSent = "qsl.qsl_sent" @@ -1535,7 +1536,7 @@ func (a *App) runBackupForShutdown() error { 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 { + if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll); err != nil { return err } return a.settings.Set(a.ctx, keyBackupLast, time.Now().UTC().Format(time.RFC3339)) @@ -11784,6 +11785,7 @@ type BackupSettings struct { 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 + KeepAll bool `json:"keep_all"` // time-stamped file per backup instead of one-per-day (kept up to Rotation) LastBackupAt string `json:"last_backup_at"` DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint } @@ -11798,7 +11800,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) { return out, nil } m, err := a.settings.GetMany(a.ctx, - keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupLast) + keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupKeepAll, keyBackupLast) if err != nil { return out, err } @@ -11809,6 +11811,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) { } out.Zip = m[keyBackupZip] == "1" out.EveryExit = m[keyBackupEveryExit] == "1" + out.KeepAll = m[keyBackupKeepAll] == "1" out.LastBackupAt = m[keyBackupLast] return out, nil } @@ -11836,6 +11839,7 @@ func (a *App) SaveBackupSettings(s BackupSettings) error { keyBackupRotation: strconv.Itoa(s.Rotation), keyBackupZip: doZip, keyBackupEveryExit: boolStr(s.EveryExit), + keyBackupKeepAll: boolStr(s.KeepAll), } { if err := a.settings.Set(a.ctx, k, v); err != nil { return err @@ -11852,15 +11856,15 @@ func (a *App) SaveBackupSettings(s BackupSettings) error { // backing up the QSOs once the logbook was split out. On MySQL the contacts // aren't in a local file, so they're exported to ADIF instead. Returns the path // of the contacts backup. -func (a *App) runConfiguredBackup(folder string, rotation int, zip bool) (string, error) { +func (a *App) runConfiguredBackup(folder string, rotation int, zip, unique bool) (string, error) { // Config snapshot (profiles, hardware, awards). Best-effort — a config-backup // failure must never stop the contacts from being protected. - if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, rotation, zip, "opslogcfg"); err != nil { + if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, rotation, zip, "opslogcfg", unique); err != nil { applog.Printf("backup: config snapshot failed: %v", err) } if a.dbBackend == "mysql" { // The live log is on MySQL; VACUUM INTO can't reach it — export to ADIF. - return a.backupLogADIF(folder, rotation, zip) + return a.backupLogADIF(folder, rotation, zip, unique) } // SQLite: snapshot the logbook file that holds the contacts. Fall back to the // settings db only when it doubles as the logbook (rare split-failure case). @@ -11868,7 +11872,7 @@ func (a *App) runConfiguredBackup(folder string, rotation int, zip bool) (string if conn == nil || path == "" { conn, path = a.db, a.dbPath } - return backup.Run(a.ctx, conn, path, folder, rotation, zip, "opslog") + return backup.Run(a.ctx, conn, path, folder, rotation, zip, "opslog", unique) } // RunBackupNow forces an immediate backup using the persisted settings. @@ -11882,7 +11886,7 @@ func (a *App) RunBackupNow() (string, error) { if folder == "" { folder = s.DefaultFolder } - path, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip) + path, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll) if err != nil { return path, err } @@ -11893,11 +11897,11 @@ func (a *App) RunBackupNow() (string, error) { // 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) { +func (a *App) backupLogADIF(folder string, rotation int, zip, unique bool) (string, error) { if a.qso == nil { return "", fmt.Errorf("logbook not initialized") } - return backup.RunADIF(folder, rotation, zip, func(p string) error { + return backup.RunADIF(folder, rotation, zip, unique, 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 @@ -11931,7 +11935,7 @@ func (a *App) maybeShutdownBackup() { 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 { + if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll); err != nil { fmt.Println("OpsLog: shutdown backup failed:", err) return } diff --git a/changelog.json b/changelog.json index bbb6e78..e33ac80 100644 --- a/changelog.json +++ b/changelog.json @@ -7,14 +7,16 @@ "Cluster details (My station): removed the example placeholders in My rig / My antenna (and the satellite name/mode) — they looked like real values, so an empty field read as already filled.", "QSL: new \"OpsLog QSL received\" marker on the QSO edit window (next to QSL Message), with a live PSE QSL / TNX indicator. When a QSL was received the card prints TNX (thanks), otherwise PSE QSL (please send one) — automatic. The stamp uses the {qso.pse_tnx} token (in the default card footer; placeable anywhere in the designer). A \"QSL received\" column was added to Recent QSOs.", "QSL: a default QSL message (Settings → E-mail, under the QSL card e-mail) now prints on the card when a QSO has no QSL Message of its own — a QSO's own QSL Message always takes precedence.", - "UDP outbound: a QSO logged from WSJT-X / JTDX / MSHV (via the inbound UDP listener) is now forwarded to the outbound ADIF integrations too — previously only QSOs typed directly into OpsLog were re-emitted." + "UDP outbound: a QSO logged from WSJT-X / JTDX / MSHV (via the inbound UDP listener) is now forwarded to the outbound ADIF integrations too — previously only QSOs typed directly into OpsLog were re-emitted.", + "Backup: with \"back up on every exit\", a new \"Keep every backup\" sub-option writes a distinct time-stamped file per exit (opslog-YYYY-MM-DD-HHMMSS.db) instead of overwriting the day's file. Rotation still keeps the newest N — raise it for more history." ], "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.", "Détails cluster (Ma station) : suppression des exemples en filigrane dans Mon rig / Mon antenne (et nom/mode satellite) — ils ressemblaient à de vraies valeurs, faisant croire qu'un champ vide était déjà rempli.", "QSL : nouveau marqueur \"QSL OpsLog reçue\" dans la fenêtre d'édition du QSO (à côté du Message QSL), avec un indicateur PSE QSL / TNX en direct. Si une QSL a été reçue, la carte imprime TNX (merci), sinon PSE QSL (envoie-moi une carte) — automatique. Le tampon utilise le token {qso.pse_tnx} (dans le pied de carte par défaut ; plaçable où tu veux dans le designer). Une colonne \"QSL reçue\" a été ajoutée aux QSO récents.", "QSL : un message QSL par défaut (Réglages → E-mail, sous l'e-mail de carte QSL) s'imprime désormais sur la carte quand un QSO n'a pas son propre Message QSL — le Message QSL du QSO l'emporte toujours.", - "UDP sortant : un QSO enregistré depuis WSJT-X / JTDX / MSHV (via l'écouteur UDP entrant) est désormais aussi transmis aux intégrations ADIF sortantes — avant, seuls les QSO saisis directement dans OpsLog étaient réémis." + "UDP sortant : un QSO enregistré depuis WSJT-X / JTDX / MSHV (via l'écouteur UDP entrant) est désormais aussi transmis aux intégrations ADIF sortantes — avant, seuls les QSO saisis directement dans OpsLog étaient réémis.", + "Sauvegarde : avec \"sauvegarder à chaque fermeture\", une nouvelle sous-option \"Garder chaque sauvegarde\" écrit un fichier horodaté distinct par fermeture (opslog-AAAA-MM-JJ-HHMMSS.db) au lieu d'écraser le fichier du jour. La rotation garde les N plus récents — augmente-la pour plus d'historique." ] }, { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 65d1b2a..237981a 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -4368,13 +4368,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan /> {t('bk.zip')} -