diff --git a/dist/backupper-2026.06.14.9.10-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.10-x86_64-1.txz.sha256 deleted file mode 100644 index 2d6b12f..0000000 --- a/dist/backupper-2026.06.14.9.10-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -3f91f1c5ca0df0a0af3fe109e8c27cd3a547492020c78bb977ea403f13279fec backupper-2026.06.14.9.10-x86_64-1.txz diff --git a/dist/backupper-2026.06.14.9.10-x86_64-1.txz b/dist/backupper-2026.06.14.9.11-x86_64-1.txz similarity index 55% rename from dist/backupper-2026.06.14.9.10-x86_64-1.txz rename to dist/backupper-2026.06.14.9.11-x86_64-1.txz index 17b6fa6..026c445 100644 Binary files a/dist/backupper-2026.06.14.9.10-x86_64-1.txz and b/dist/backupper-2026.06.14.9.11-x86_64-1.txz differ diff --git a/dist/backupper-2026.06.14.9.11-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.11-x86_64-1.txz.sha256 new file mode 100644 index 0000000..3a7336c --- /dev/null +++ b/dist/backupper-2026.06.14.9.11-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +e956ec19eb199b15938ab4e85c766f31614fc47e42233e3d807eb59e5c0b0e39 backupper-2026.06.14.9.11-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index cbdce67..22f459c 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,16 @@ - + - + ]> +### 2026.06.14.9.11 +- Add a global setting to show live run logs in Activity, disabled by default. + ### 2026.06.14.9.10 - Always show a clearly labeled activity history delete action, including when no completed entries exist. diff --git a/internal/model/model.go b/internal/model/model.go index e50a73c..4871844 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -35,6 +35,7 @@ type Settings struct { ResticPath string `json:"resticPath"` RestoreRoot string `json:"restoreRoot"` PersistentLogDir string `json:"persistentLogDir,omitempty"` + ShowLiveLog bool `json:"showLiveLog"` CatchUpWindowHrs int `json:"catchUpWindowHours"` CheckCron string `json:"checkCron"` PruneCron string `json:"pruneCron"` diff --git a/internal/model/validate_test.go b/internal/model/validate_test.go index 054d7b1..31ebcda 100644 --- a/internal/model/validate_test.go +++ b/internal/model/validate_test.go @@ -4,6 +4,9 @@ import "testing" func TestValidateConfig(t *testing.T) { c := DefaultConfig() + if c.Settings.ShowLiveLog { + t.Fatal("live log must be hidden by default") + } c.Repositories = append(c.Repositories, Repository{SchemaVersion: 1, ID: "repo", Name: "Local", Type: RepositoryLocal, Location: "/tmp/repo", PasswordRef: "password"}) c.Jobs = append(c.Jobs, Job{SchemaVersion: 1, ID: "job", Name: "Share", Type: JobShare, Enabled: true, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/data"}}, Schedule: Schedule{Cron: "0 2 * * *"}, Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{Daily: 7, Weekly: 4, Monthly: 12}, ShutdownSecs: 120}) if err := ValidateConfig(c); err != nil { diff --git a/plugin/backupper.plg b/plugin/backupper.plg index 2b3e250..b520240 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,16 @@ - + ]> +### 2026.06.14.9.11 +- Add a global setting to show live run logs in Activity, disabled by default. + ### 2026.06.14.9.10 - Always show a clearly labeled activity history delete action, including when no completed entries exist. diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index da55a08..fe632d4 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -42,6 +42,7 @@ resticPath: 'Pfad zur vom Plugin bereitgestellten Restic-Datei. Standard: /usr/local/libexec/backupper/restic. Normalerweise nicht ändern.', restoreRoot: 'Basisverzeichnis für sichere Staging-Restores. Beispiel: /mnt/user/backupper-restores.', persistentLogDir: 'Optionales Verzeichnis für dauerhafte Detailprotokolle. Beispiel: /mnt/user/system/backupper-logs. Leer lassen, um Flash-Schreibzugriffe zu vermeiden.', + showLiveLog: 'Zeigt bereinigte Restic-Laufmeldungen unter den Aufgaben im Activity-Bereich. Standardmäßig deaktiviert; Fortschritt und Ergebnis bleiben trotzdem sichtbar.', catchUp: 'Wie viele Stunden nach einem verpassten Termin ein Job beim Boot nachgeholt werden darf. Beispiel: 24 holt einen innerhalb des letzten Tages verpassten Lauf nach.', checkCron: 'Zeitplan für vollständige Repository-Prüfungen. Beispiel: 0 3 1 * * prüft am ersten Tag jedes Monats um 03:00 Uhr.', pruneCron: 'Zeitplan zum Freigeben nicht mehr benötigter Restic-Daten. Beispiel: 0 4 * * 0 startet sonntags um 04:00 Uhr.', @@ -477,7 +478,8 @@ } function runsTable(runs) { if(!runs.length) return '
No activity recorded.
'; - return `${runs.map(r=>`${(r.liveLog||[]).length?``:''}`).join('')}
TaskStatusCreatedProgressMessage
${esc(r.taskType)} ${esc(r.jobId||'')}${status(r.status)}${esc(new Date(r.createdAt).toLocaleString())}${progressView(r)}${esc(r.message||'')}${['queued','running'].includes(r.status)?button('Cancel',`cancel-run:${r.id}`,'danger'):''}
${liveLogView(r)}
`; + const showLiveLog=state.config?.settings?.showLiveLog===true; + return `${runs.map(r=>`${showLiveLog&&(r.liveLog||[]).length?``:''}`).join('')}
TaskStatusCreatedProgressMessage
${esc(r.taskType)} ${esc(r.jobId||'')}${status(r.status)}${esc(new Date(r.createdAt).toLocaleString())}${progressView(r)}${esc(r.message||'')}${['queued','running'].includes(r.status)?button('Cancel',`cancel-run:${r.id}`,'danger'):''}
${liveLogView(r)}
`; } function renderSettings() { @@ -485,7 +487,7 @@ const unraid = state.config.notifications.find(n=>n.type==='unraid') || {id:'unraid',enabled:true,events:['success','warning','failed']}; const gotify = state.config.notifications.find(n=>n.type==='gotify') || {id:'gotify',enabled:false,events:['success','warning','failed']}; const eventBoxes = target => [['success','Success'],['warning','Warning'],['failed','Failed']].map(([value,label])=>``).join(''); - return `

Settings

${button('Save settings','submit-settings','primary')}
+ return `

Settings

${button('Save settings','submit-settings','primary')}

Unraid notifications

Uses Unraid's notification system and all agents configured under Settings > Notification Settings.

Notify on
${eventBoxes(unraid)}
${button('Save Unraid notifications','submit-unraid-notify','primary')}

Gotify notification

Create an application in Gotify and enter its application token here.

Notify on
${eventBoxes(gotify)}
${button('Save Gotify notification','submit-gotify-notify','primary')}
`; } @@ -563,7 +565,7 @@ render(); notify(`Repository saved: ${saved.location}`); } if (kind === 'submit-restore') { const f=new FormData(document.getElementById('bu-restore-form')); await api('/v1/restores',{method:'POST',body:JSON.stringify({schemaVersion:1,id:'',repositoryId:f.get('repositoryId'),snapshotId:f.get('snapshotId'),includes:lines(f.get('includes')),target:f.get('target'),inPlace:f.get('inPlace')==='on',confirmed:f.get('confirmed')==='on'})}); notify('Restore queued'); state.view='activity'; render(); } - if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); } + if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),showLiveLog:f.get('showLiveLog')==='on',catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); render(); } if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Unraid notifications'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); } if (kind === 'submit-gotify-notify') { const f=new FormData(document.getElementById('bu-gotify-notify-form')); const existing=state.config.notifications.find(n=>n.type==='gotify'); const target={schemaVersion:1,id:f.get('id')||'gotify',name:'Gotify',type:'gotify',enabled:f.get('enabled')==='on',url:String(f.get('url')||'').trim(),tokenRef:String(f.get('tokenRef')||'').trim(),events:f.getAll('events')}; if(target.enabled&&!target.url) throw new Error('Gotify server URL is required'); if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Gotify notifications'); if(target.enabled&&!existing&&!f.get('token')) throw new Error('A Gotify application token is required'); if(f.get('token')) await api(`/v1/secrets/${encodeURIComponent(target.tokenRef)}`,{method:'PUT',body:JSON.stringify({type:'gotify-token',value:f.get('token')})}); state.config.notifications=state.config.notifications.filter(n=>n.type!=='gotify'&&n.type!=='ntfy'); if(target.enabled||target.url) state.config.notifications.push(target); await saveConfig(); render(); } }