From 90e293facda04a4d03479a65707872315be53882 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 01:13:44 +0200 Subject: [PATCH] (ui) Surface retention warnings on run detail Show host-level retention warnings on run detail pages so successful or warning runs still expose scheduled prune limit issues and incomplete snapshots that need operator attention. --- .../templates/pobsync_backend/run_detail.html | 23 ++++++++++++++ src/pobsync_backend/tests/test_views.py | 31 +++++++++++++++++++ src/pobsync_backend/views.py | 1 + 3 files changed, 55 insertions(+) diff --git a/src/pobsync_backend/templates/pobsync_backend/run_detail.html b/src/pobsync_backend/templates/pobsync_backend/run_detail.html index e66cc28..da7ffff 100644 --- a/src/pobsync_backend/templates/pobsync_backend/run_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/run_detail.html @@ -179,6 +179,29 @@ {% endif %} + {% if retention_warning.has_warning %} +
+

Retention Warnings

+
+ {% if retention_warning.prune_exceeded %} +
+ Scheduled pruning for this host would delete {{ retention_warning.delete_count }} snapshot(s), above max + delete {{ retention_warning.max_delete }}. +
+ {% endif %} + {% if retention_warning.incomplete_count %} +
+ {{ retention_warning.incomplete_count }} incomplete snapshot(s) exist for this host and are excluded from + retention cleanup. +
+ {% endif %} + {% if retention_warning.error %} +
{{ retention_warning.error }}
+ {% endif %} +
+
+ {% endif %} +

Raw Result

{{ result_json }}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 69f9dae..2dbdf40 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -1213,6 +1213,37 @@ class ViewTests(TestCase): self.assertContains(response, "Retention") self.assertContains(response, "Deletion blocked by --max-delete=0") + def test_run_detail_surfaces_host_retention_warnings(self) -> None: + self.client.force_login(self.staff_user) + host = HostConfig.objects.create( + host="web-01", + address="web-01.example.test", + retention_daily=0, + retention_weekly=0, + retention_monthly=0, + retention_yearly=0, + ) + ScheduleConfig.objects.create(host=host, cron_expr="15 2 * * *", enabled=True, prune=True, prune_max_delete=1) + self._snapshot(host, "20260517-021500Z__OLDSNP1") + self._snapshot(host, "20260518-021500Z__OLDSNP2") + self._snapshot(host, "20260519-021500Z__NEWSNAP") + SnapshotRecord.objects.create( + host=host, + kind=SnapshotRecord.Kind.INCOMPLETE, + dirname="20260519-031500Z__BROKEN01", + path=f"/backups/{host.host}/.incomplete/20260519-031500Z__BROKEN01", + status="failed", + started_at=datetime(2026, 5, 19, 3, 15, tzinfo=timezone.utc), + ) + run = BackupRun.objects.create(host=host, status=BackupRun.Status.SUCCESS, result={"ok": True}) + + response = self.client.get(reverse("run_detail", args=[run.id])) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Retention Warnings") + self.assertContains(response, "Scheduled pruning for this host would delete 2 snapshot(s)") + self.assertContains(response, "1 incomplete snapshot(s) exist for this host") + def test_run_detail_infers_rsync_log_from_snapshot_path(self) -> None: self.client.force_login(self.staff_user) host = HostConfig.objects.create(host="web-01", address="web-01.example.test") diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 4ad50ad..44d7715 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -429,6 +429,7 @@ def run_detail(request, run_id: int): "failure_summary": failure.get("message") or failure.get("summary") or "", "prune_result": prune_result, "has_prune_result": bool(prune_result), + "retention_warning": _retention_warning_for_host(run.host, _schedule_for_host(run.host)), "rsync_log_path": str(rsync_log_path) if rsync_log_path is not None else "", "rsync_log_exists": bool(rsync_log_path and rsync_log_path.exists()), "rsync_log_tail": rsync_log_tail,