diff --git a/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 deleted file mode 100644 index 0dfffd4..0000000 --- a/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -91325b4f694386ba23ad5ea43e51f6f3b41ae02ce5e66cf0bb04b0473fce9231 backupper-2026.06.14.9.7-x86_64-1.txz diff --git a/dist/backupper-2026.06.14.9.7-x86_64-1.txz b/dist/backupper-2026.06.14.9.8-x86_64-1.txz similarity index 55% rename from dist/backupper-2026.06.14.9.7-x86_64-1.txz rename to dist/backupper-2026.06.14.9.8-x86_64-1.txz index d96d6c1..5da2451 100644 Binary files a/dist/backupper-2026.06.14.9.7-x86_64-1.txz and b/dist/backupper-2026.06.14.9.8-x86_64-1.txz differ diff --git a/dist/backupper-2026.06.14.9.8-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.8-x86_64-1.txz.sha256 new file mode 100644 index 0000000..8354be5 --- /dev/null +++ b/dist/backupper-2026.06.14.9.8-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +0442a269efe056ffdc47ec64d14bf0df898a020426a0a203efd8ac73fd0ced30 backupper-2026.06.14.9.8-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index 4a6d6a0..a23833f 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,16 @@ - + - + ]> +### 2026.06.14.9.8 +- Add a Clear history action that removes completed activity entries and saved run logs while preserving active and queued tasks. + ### 2026.06.14.9.7 - Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. - Strengthen the Initialize confirmation for existing repository destinations. diff --git a/internal/api/server.go b/internal/api/server.go index fcad597..1705220 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -36,6 +36,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server { mux.HandleFunc("PUT /v1/secrets/{id}", s.putSecret) mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret) mux.HandleFunc("GET /v1/runs", s.runs) + mux.HandleFunc("DELETE /v1/runs", s.clearRuns) mux.HandleFunc("GET /v1/filesystem/directories", s.directories) mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads) mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob) @@ -118,6 +119,9 @@ func (s *Server) deleteSecret(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Runs()) } +func (s *Server) clearRuns(w http.ResponseWriter, _ *http.Request) { + writeJSON(w, 200, map[string]int{"cleared": s.service.ClearRunHistory()}) +} func (s *Server) directories(w http.ResponseWriter, r *http.Request) { items, err := s.service.BrowseDirectories(r.URL.Query().Get("path")) if err != nil { diff --git a/internal/queue/queue.go b/internal/queue/queue.go index f8be99d..d9377c6 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -151,6 +151,15 @@ func (q *Queue) UpdateActive(id string, update func(*model.Run)) bool { return true } +func (q *Queue) ClearHistory() int { + q.mu.Lock() + defer q.mu.Unlock() + count := len(q.history) + q.history = nil + q.emitLocked() + return count +} + func (q *Queue) snapshotLocked() []model.Run { result := append([]model.Run{}, q.history...) if q.active != nil { diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index 55bfa13..4ed2dac 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -28,3 +28,34 @@ func TestQueueDeduplicatesJobAndRunsTask(t *testing.T) { } q.Stop() } + +func TestClearHistoryKeepsActiveAndPendingRuns(t *testing.T) { + started := make(chan struct{}) + release := make(chan struct{}) + q := New(func(_ context.Context, run model.Run) model.Run { + close(started) + <-release + run.Status = "success" + return run + }, nil) + q.RestoreHistory([]model.Run{{ID: "old", Status: "success"}}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go q.Run(ctx) + if err := q.Enqueue(model.Run{ID: "active", JobID: "active-job"}); err != nil { + t.Fatal(err) + } + <-started + if err := q.Enqueue(model.Run{ID: "pending", JobID: "pending-job"}); err != nil { + t.Fatal(err) + } + if cleared := q.ClearHistory(); cleared != 1 { + t.Fatalf("cleared = %d", cleared) + } + runs := q.Snapshot() + if len(runs) != 2 || runs[0].ID != "active" || runs[1].ID != "pending" { + t.Fatalf("runs = %#v", runs) + } + q.Stop() + close(release) +} diff --git a/internal/service/service.go b/internal/service/service.go index a0f57c7..f980412 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -166,6 +166,7 @@ func (s *Service) EnqueueRestore(task model.RestoreTask) (model.Run, error) { } func (s *Service) Cancel(runID string) bool { return s.queue.Cancel(runID) } +func (s *Service) ClearRunHistory() int { return s.queue.ClearHistory() } func (s *Service) Snapshots(ctx context.Context, repoID string) ([]model.Snapshot, error) { repo, ok := s.repository(repoID) diff --git a/plugin/backupper.plg b/plugin/backupper.plg index a4535ff..d4c3855 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,16 @@ - + ]> +### 2026.06.14.9.8 +- Add a Clear history action that removes completed activity entries and saved run logs while preserving active and queued tasks. + ### 2026.06.14.9.7 - Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. - Strengthen the Initialize confirmation for existing repository destinations. diff --git a/webgui/Backupper.page b/webgui/Backupper.page index 8be0785..463c2f4 100644 --- a/webgui/Backupper.page +++ b/webgui/Backupper.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.14.9.7

Restic Backup Manager for Unraid

+ +

URBM 2026.06.14.9.8

Restic Backup Manager for Unraid

Connecting...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
- + diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index fd93ed1..df3233d 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -63,6 +63,7 @@ 'submit-unraid-notify': 'Speichert native Unraid-Benachrichtigungen. Die Zustellung erfolgt über die in Unraid eingerichteten Notification Agents.', 'submit-gotify-notify': 'Speichert den Gotify-Kanal und das App-Token verschlüsselt.', 'refresh': 'Lädt Laufstatus und Historie erneut vom URBM-Daemon.', + 'clear-activity': 'Löscht alle abgeschlossenen Activity-Einträge und deren gespeicherte Laufprotokolle. Laufende und wartende Aufgaben bleiben erhalten.', 'cancel-form': 'Verwirft ungespeicherte Eingaben und kehrt zur Übersicht zurück.', 'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.', 'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.', @@ -456,7 +457,7 @@ return `

Restore

${button('Queue restore','submit-restore','primary')}
`; } - function renderActivity() { return `

Activity

Running tasks update automatically every two seconds.
${button('Refresh','refresh','primary')}
${runsTable([...state.runs].reverse())}
`; } + function renderActivity() { const completed=state.runs.some(r=>!['queued','running'].includes(r.status)); return `

Activity

Running tasks update automatically every two seconds.
${button('Refresh','refresh','primary')}${completed?button('Clear history','clear-activity','danger'):''}
${runsTable([...state.runs].reverse())}
`; } function progressView(run) { if(run.status==='queued') return 'Waiting in queue'; if(run.taskType!=='backup'&&!run.progressPercent) return 'No percentage available'; @@ -506,6 +507,7 @@ if (name === 'refresh') return load(); if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); return load(); } if (name === 'cancel-run') { await api(`/v1/runs/${a}/cancel`,{method:'POST'}); return load(); } + if (name === 'clear-activity') { if(confirm('Delete all completed activity entries and saved run logs? Running and queued tasks will remain.')) { const result=await api('/v1/runs',{method:'DELETE'}); notify(`${result.cleared||0} activity entries cleared`); return load(); } return; } if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository connection succeeded'); } if (name === 'init-repo') { if(confirm('Create a NEW Restic repository here?\n\nOnly continue if this destination does not already contain a Restic repository. For an existing repository, enter its original password under Edit and use Test.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository initialized successfully'); } return; } if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a} queued`); return load(); }