Enrich backup notifications

This commit is contained in:
Mikei386
2026-06-21 21:34:05 +02:00
parent cc6d370c05
commit 64412c6597
8 changed files with 77 additions and 24 deletions
-1
View File
@@ -1 +0,0 @@
7331dd7ade82aa68351b48f2555cd6dd9141620e6ba4ebce14e722fb0e6e5a46 urbm-2026.06.21.r009-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
6223df8fefd872c4e102a9dfadb8103bce4de364d2836b85d773591c92025c4b urbm-2026.06.21.r010-x86_64-1.txz
+6 -2
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r009"> <!ENTITY version "2026.06.21.r010">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "7331dd7ade82aa68351b48f2555cd6dd9141620e6ba4ebce14e722fb0e6e5a46"> <!ENTITY packageSHA256 "6223df8fefd872c4e102a9dfadb8103bce4de364d2836b85d773591c92025c4b">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.06.21.r010
- Enrich Unraid and Gotify notifications with structured job, repository, duration, snapshot, file, and data statistics.
- Use status-specific notification titles with success, warning, and failure indicators.
### 2026.06.21.r009 ### 2026.06.21.r009
- Allow normal staging restores to explicitly selected safe targets under /mnt/user, /mnt/disks, and /mnt/remotes. - Allow normal staging restores to explicitly selected safe targets under /mnt/user, /mnt/disks, and /mnt/remotes.
- Keep protected system paths blocked and continue rejecting symlink traversal in restore targets. - Keep protected system paths blocked and continue rejecting symlink traversal in restore targets.
+10 -2
View File
@@ -12,10 +12,18 @@ func TestNotificationMessageIncludesBackupStatistics(t *testing.T) {
started := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC) started := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
finished := started.Add(2*time.Minute + 8*time.Second) finished := started.Add(2*time.Minute + 8*time.Second)
run := model.Run{TaskType: "backup", Status: "success", StartedAt: &started, FinishedAt: &finished, SnapshotID: "abc123", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120} run := model.Run{TaskType: "backup", Status: "success", StartedAt: &started, FinishedAt: &finished, SnapshotID: "abc123", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120}
message := notificationMessage(run, finished) message := notificationMessage("Freigaben", "LV-426", run, finished)
for _, expected := range []string{"Status: Erfolgreich", "Dauer: 2 Min. 8 Sek.", "Neu gespeichert: 1.5 KiB", "Neue Dateien: 7", "Geänderte Dateien: 2", "Gesamtgröße dieses Backups: 5.0 MiB", "Dateien insgesamt: 120", "Snapshot: abc123"} { for _, expected := range []string{"📊 URBM-Zusammenfassung", "Status: Erfolgreich", "Job: Freigaben", "Repository: LV-426", "Dauer: 2 Min. 8 Sek.", "Snapshot: abc123", "Verarbeitete Daten: 5.0 MiB", "Verarbeitete Dateien: 120", "Neu gespeichert: 1.5 KiB", "Neue Dateien: 7", "Geänderte Dateien: 2"} {
if !strings.Contains(message, expected) { if !strings.Contains(message, expected) {
t.Errorf("message %q does not contain %q", message, expected) t.Errorf("message %q does not contain %q", message, expected)
} }
} }
} }
func TestNotificationTitleIncludesRepositoryAndStatus(t *testing.T) {
run := model.Run{TaskType: "backup", Status: "failed"}
title := notificationTitle("Freigaben", "LV-426", run)
if title != "❌ URBM Backup: LV-426" {
t.Fatalf("title = %q", title)
}
}
+51 -14
View File
@@ -746,17 +746,24 @@ func (s *Service) executeRestore(ctx context.Context, run model.Run) model.Run {
func (s *Service) finishJob(run model.Run, job model.Job, result model.Run) model.Run { func (s *Service) finishJob(run model.Run, job model.Job, result model.Run) model.Run {
if result.Status == "success" || result.Status == "warning" || result.Status == "failed" { if result.Status == "success" || result.Status == "warning" || result.Status == "failed" {
go s.notify(job.Name, result) repoName := ""
if job.RepositoryID != "" {
if repo, ok := s.repository(job.RepositoryID); ok {
repoName = repo.Name
}
}
go s.notify(job.Name, repoName, result)
} }
return result return result
} }
func (s *Service) notify(name string, run model.Run) { func (s *Service) notify(name, repository string, run model.Run) {
message := notificationMessage(run, time.Now().UTC()) title := notificationTitle(name, repository, run)
message := notificationMessage(name, repository, run, time.Now().UTC())
for _, target := range s.Config().Notifications { for _, target := range s.Config().Notifications {
for _, event := range target.Events { for _, event := range target.Events {
if event == run.Status { if event == run.Status {
if err := s.notifier.Send(context.Background(), target, "URBM: "+name, message, run.Status); err != nil { if err := s.notifier.Send(context.Background(), target, title, message, run.Status); err != nil {
s.log.Warn("send notification failed", "target", target.ID, "type", target.Type, "error", err) s.log.Warn("send notification failed", "target", target.ID, "type", target.Type, "error", err)
} }
break break
@@ -765,12 +772,36 @@ func (s *Service) notify(name string, run model.Run) {
} }
} }
func notificationMessage(run model.Run, now time.Time) string { func notificationTitle(name, repository string, run model.Run) string {
icon := map[string]string{"success": "✅", "warning": "⚠️", "failed": "❌"}[run.Status]
if icon == "" {
icon = "️"
}
task := map[string]string{"backup": "Backup", "rsync": "Rsync", "restore": "Restore", "check": "Prüfung", "prune": "Bereinigung"}[run.TaskType]
if task == "" {
task = run.TaskType
if task != "" {
task = strings.ToUpper(task[:1]) + task[1:]
}
}
if repository != "" {
return fmt.Sprintf("%s URBM %s: %s", icon, task, repository)
}
return fmt.Sprintf("%s URBM %s: %s", icon, task, name)
}
func notificationMessage(name, repository string, run model.Run, now time.Time) string {
status := map[string]string{"success": "Erfolgreich", "warning": "Mit Warnung", "failed": "Fehlgeschlagen"}[run.Status] status := map[string]string{"success": "Erfolgreich", "warning": "Mit Warnung", "failed": "Fehlgeschlagen"}[run.Status]
if status == "" { if status == "" {
status = run.Status status = run.Status
} }
lines := []string{"Status: " + status} lines := []string{"📊 URBM-Zusammenfassung", "", "Status: " + status}
if name != "" {
lines = append(lines, "Job: "+name)
}
if repository != "" {
lines = append(lines, "Repository: "+repository)
}
if run.StartedAt != nil { if run.StartedAt != nil {
finished := now finished := now
if run.FinishedAt != nil { if run.FinishedAt != nil {
@@ -781,16 +812,22 @@ func notificationMessage(run model.Run, now time.Time) string {
} }
} }
if (run.TaskType == "backup" || run.TaskType == "rsync") && (run.Status == "success" || run.Status == "warning") { if (run.TaskType == "backup" || run.TaskType == "rsync") && (run.Status == "success" || run.Status == "warning") {
lines = append(lines, if run.TaskType == "backup" && run.SnapshotID != "" {
"Neu gespeichert: "+formatBytes(run.BytesAdded),
fmt.Sprintf("Neue Dateien: %d", run.FilesNew),
fmt.Sprintf("Geänderte Dateien: %d", run.FilesChanged),
"Gesamtgröße dieses Backups: "+formatBytes(run.BytesProcessed),
fmt.Sprintf("Dateien insgesamt: %d", run.FilesProcessed),
)
if run.SnapshotID != "" {
lines = append(lines, "Snapshot: "+run.SnapshotID) lines = append(lines, "Snapshot: "+run.SnapshotID)
} }
lines = append(lines,
"Verarbeitete Daten: "+formatBytes(run.BytesProcessed),
fmt.Sprintf("Verarbeitete Dateien: %d", run.FilesProcessed),
)
if run.TaskType == "backup" {
lines = append(lines,
"Neu gespeichert: "+formatBytes(run.BytesAdded),
fmt.Sprintf("Neue Dateien: %d", run.FilesNew),
fmt.Sprintf("Geänderte Dateien: %d", run.FilesChanged),
)
} else {
lines = append(lines, "Kopierte Daten: "+formatBytes(run.BytesAdded))
}
} }
if run.Message != "" && run.Message != "backup completed" && run.Message != "rsync completed" { if run.Message != "" && run.Message != "backup completed" && run.Message != "rsync completed" {
lines = append(lines, "Details: "+run.Message) lines = append(lines, "Details: "+run.Message)
+5 -1
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r009"> <!ENTITY version "2026.06.21.r010">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&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/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.06.21.r010
- Enrich Unraid and Gotify notifications with structured job, repository, duration, snapshot, file, and data statistics.
- Use status-specific notification titles with success, warning, and failure indicators.
### 2026.06.21.r009 ### 2026.06.21.r009
- Allow normal staging restores to explicitly selected safe targets under /mnt/user, /mnt/disks, and /mnt/remotes. - Allow normal staging restores to explicitly selected safe targets under /mnt/user, /mnt/disks, and /mnt/remotes.
- Keep protected system paths blocked and continue rejecting symlink traversal in restore targets. - Keep protected system paths blocked and continue rejecting symlink traversal in restore targets.
+4 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php <?php
$pluginRoot = '/plugins/urbm'; $pluginRoot = '/plugins/urbm';
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r009"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r010">
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php"> <div id="urbm-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/urbm.png?v=20260621r009" alt="URBM-Logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r010" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.21.r009</span></h1><p>Restic Backup Manager für Unraid</p></div> <div><h1>URBM <span class="bu-version">2026.06.21.r010</span></h1><p>Restic Backup Manager für 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.">Verbindung wird hergestellt...</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.">Verbindung wird hergestellt...</div>
</header> </header>
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div> <div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r009.js"></script> <script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r010.js"></script>