Add activity history clearing
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
91325b4f694386ba23ad5ea43e51f6f3b41ae02ce5e66cf0bb04b0473fce9231 backupper-2026.06.14.9.7-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
0442a269efe056ffdc47ec64d14bf0df898a020426a0a203efd8ac73fd0ced30 backupper-2026.06.14.9.8-x86_64-1.txz
|
||||||
Vendored
+5
-2
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user