Add activity history clearing

This commit is contained in:
Mikei386
2026-06-14 22:17:54 +02:00
parent 5da077cdf7
commit 12bd339198
11 changed files with 62 additions and 9 deletions
-1
View File
@@ -1 +0,0 @@
91325b4f694386ba23ad5ea43e51f6f3b41ae02ce5e66cf0bb04b0473fce9231 backupper-2026.06.14.9.7-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
0442a269efe056ffdc47ec64d14bf0df898a020426a0a203efd8ac73fd0ced30 backupper-2026.06.14.9.8-x86_64-1.txz
+5 -2
View File
@@ -2,13 +2,16 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "backupper"> <!ENTITY name "backupper">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.14.9.7"> <!ENTITY version "2026.06.14.9.8">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "91325b4f694386ba23ad5ea43e51f6f3b41ae02ce5e66cf0bb04b0473fce9231"> <!ENTITY packageSHA256 "0442a269efe056ffdc47ec64d14bf0df898a020426a0a203efd8ac73fd0ced30">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
<CHANGES> <CHANGES>
### 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 ### 2026.06.14.9.7
- Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. - Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors.
- Strengthen the Initialize confirmation for existing repository destinations. - Strengthen the Initialize confirmation for existing repository destinations.
+4
View File
@@ -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("PUT /v1/secrets/{id}", s.putSecret)
mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret) mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
mux.HandleFunc("GET /v1/runs", s.runs) 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/filesystem/directories", s.directories)
mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads) mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads)
mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob) 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) w.WriteHeader(204)
} }
func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Runs()) } 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) { func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
items, err := s.service.BrowseDirectories(r.URL.Query().Get("path")) items, err := s.service.BrowseDirectories(r.URL.Query().Get("path"))
if err != nil { if err != nil {
+9
View File
@@ -151,6 +151,15 @@ func (q *Queue) UpdateActive(id string, update func(*model.Run)) bool {
return true 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 { func (q *Queue) snapshotLocked() []model.Run {
result := append([]model.Run{}, q.history...) result := append([]model.Run{}, q.history...)
if q.active != nil { if q.active != nil {
+31
View File
@@ -28,3 +28,34 @@ func TestQueueDeduplicatesJobAndRunsTask(t *testing.T) {
} }
q.Stop() 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)
}
+1
View File
@@ -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) 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) { func (s *Service) Snapshots(ctx context.Context, repoID string) ([]model.Snapshot, error) {
repo, ok := s.repository(repoID) repo, ok := s.repository(repoID)
+4 -1
View File
@@ -2,13 +2,16 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "backupper"> <!ENTITY name "backupper">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.14.9.7"> <!ENTITY version "2026.06.14.9.8">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE"> <!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
<CHANGES> <CHANGES>
### 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 ### 2026.06.14.9.7
- Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. - Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors.
- Strengthen the Initialize confirmation for existing repository destinations. - Strengthen the Initialize confirmation for existing repository destinations.
+4 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php <?php
$pluginRoot = '/plugins/backupper'; $pluginRoot = '/plugins/backupper';
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=2026061497"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=2026061498">
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php"> <div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header"> <header class="bu-header">
<div class="bu-brand"> <div class="bu-brand">
<img class="bu-logo" src="<?= $pluginRoot ?>/images/backupper.png?v=2026061497" alt="URBM logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/backupper.png?v=2026061498" alt="URBM logo">
<div><h1>URBM <span class="bu-version">2026.06.14.9.7</span></h1><p>Restic Backup Manager for Unraid</p></div> <div><h1>URBM <span class="bu-version">2026.06.14.9.8</span></h1><p>Restic Backup Manager for Unraid</p></div>
</div> </div>
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div> <div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div>
</header> </header>
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div> <div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.9.7.js"></script> <script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.9.8.js"></script>
+3 -1
View File
@@ -63,6 +63,7 @@
'submit-unraid-notify': 'Speichert native Unraid-Benachrichtigungen. Die Zustellung erfolgt über die in Unraid eingerichteten Notification Agents.', '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.', 'submit-gotify-notify': 'Speichert den Gotify-Kanal und das App-Token verschlüsselt.',
'refresh': 'Lädt Laufstatus und Historie erneut vom URBM-Daemon.', '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.', 'cancel-form': 'Verwirft ungespeicherte Eingaben und kehrt zur Übersicht zurück.',
'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.', 'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.',
'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.', 'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.',
@@ -456,7 +457,7 @@
return `<section class="bu-card"><h2>Restore</h2><form id="bu-restore-form" class="bu-form"><label>Repository<select name="repositoryId">${options}</select></label><label>Snapshot ID<input name="snapshotId" required value="${esc(state.selectedSnapshot||'')}"></label><label class="wide">Include paths, one per line<textarea name="includes">${esc((state.restoreIncludes||[]).join('\n'))}</textarea></label><label class="wide">Target<input name="target" required value="${esc(state.config.settings.restoreRoot)}/${esc(id('restore'))}"></label><label>In-place restore<input name="inPlace" type="checkbox"></label><label>Confirm overwrite risk<input name="confirmed" type="checkbox"></label><div class="wide bu-actions">${button('Queue restore','submit-restore','primary')}</div></form></section>`; return `<section class="bu-card"><h2>Restore</h2><form id="bu-restore-form" class="bu-form"><label>Repository<select name="repositoryId">${options}</select></label><label>Snapshot ID<input name="snapshotId" required value="${esc(state.selectedSnapshot||'')}"></label><label class="wide">Include paths, one per line<textarea name="includes">${esc((state.restoreIncludes||[]).join('\n'))}</textarea></label><label class="wide">Target<input name="target" required value="${esc(state.config.settings.restoreRoot)}/${esc(id('restore'))}"></label><label>In-place restore<input name="inPlace" type="checkbox"></label><label>Confirm overwrite risk<input name="confirmed" type="checkbox"></label><div class="wide bu-actions">${button('Queue restore','submit-restore','primary')}</div></form></section>`;
} }
function renderActivity() { return `<div class="bu-toolbar"><div><h2>Activity</h2><div class="bu-muted">Running tasks update automatically every two seconds.</div></div>${button('Refresh','refresh','primary')}</div><section class="bu-card">${runsTable([...state.runs].reverse())}</section>`; } function renderActivity() { const completed=state.runs.some(r=>!['queued','running'].includes(r.status)); return `<div class="bu-toolbar"><div><h2>Activity</h2><div class="bu-muted">Running tasks update automatically every two seconds.</div></div><div class="bu-actions">${button('Refresh','refresh','primary')}${completed?button('Clear history','clear-activity','danger'):''}</div></div><section class="bu-card">${runsTable([...state.runs].reverse())}</section>`; }
function progressView(run) { function progressView(run) {
if(run.status==='queued') return '<span class="bu-muted">Waiting in queue</span>'; if(run.status==='queued') return '<span class="bu-muted">Waiting in queue</span>';
if(run.taskType!=='backup'&&!run.progressPercent) return '<span class="bu-muted">No percentage available</span>'; if(run.taskType!=='backup'&&!run.progressPercent) return '<span class="bu-muted">No percentage available</span>';
@@ -506,6 +507,7 @@
if (name === 'refresh') return load(); if (name === 'refresh') return load();
if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); 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 === '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 === '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 === '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(); } if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a} queued`); return load(); }