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')}
-