diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html
index 4e8c6a7..ed8e326 100644
--- a/src/pobsync_backend/templates/pobsync_backend/base.html
+++ b/src/pobsync_backend/templates/pobsync_backend/base.html
@@ -273,6 +273,11 @@
}
button.secondary:hover,
.button-link.secondary:hover { background: #eef3f8; }
+ button.compact,
+ .button-link.compact {
+ font-size: 12px;
+ padding: 5px 8px;
+ }
button:disabled {
background: #d8dee6;
border-color: #d8dee6;
diff --git a/src/pobsync_backend/templates/pobsync_backend/runs_list.html b/src/pobsync_backend/templates/pobsync_backend/runs_list.html
index a8dc10d..9be3a57 100644
--- a/src/pobsync_backend/templates/pobsync_backend/runs_list.html
+++ b/src/pobsync_backend/templates/pobsync_backend/runs_list.html
@@ -95,7 +95,22 @@
none
{% endif %}
-
{% if run.reviewed_at %}reviewed{% elif run.status == "failed" or run.status == "warning" %}needed{% else %}none{% endif %} |
+
+ {% if run.reviewed_at %}
+ reviewed
+ {% elif run.status == "failed" or run.status == "warning" %}
+
+ needed
+
+
+ {% else %}
+ none
+ {% endif %}
+ |
{% empty %}
| No runs matched the current filter. |
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py
index 3437007..aa929b8 100644
--- a/src/pobsync_backend/tests/test_views.py
+++ b/src/pobsync_backend/tests/test_views.py
@@ -234,6 +234,31 @@ class ViewTests(TestCase):
self.assertContains(response, "needed")
self.assertNotContains(response, f"Run {success.id}")
+ def test_runs_list_can_mark_problem_run_reviewed(self) -> None:
+ self.client.force_login(self.staff_user)
+ host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
+ run = BackupRun.objects.create(host=host, status=BackupRun.Status.FAILED, run_type=BackupRun.RunType.MANUAL)
+ list_url = f'{reverse("runs_list")}?status=failed&review=needed'
+
+ response = self.client.get(list_url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "Mark reviewed")
+ self.assertContains(response, 'value="/runs/?status=failed&review=needed"', html=False)
+
+ response = self.client.post(
+ reverse("resolve_run_review", args=[run.id]),
+ {"next": list_url},
+ follow=True,
+ )
+
+ run.refresh_from_db()
+ self.assertIsNotNone(run.reviewed_at)
+ self.assertEqual(run.reviewed_by, self.staff_user.username)
+ self.assertRedirects(response, list_url)
+ self.assertContains(response, f"Run {run.id} marked reviewed.")
+ self.assertNotContains(response, f"Run {run.id}", html=False)
+
def test_snapshots_list_filters_by_host_and_kind(self) -> None:
self.client.force_login(self.staff_user)
web = 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 e319485..5e2b85d 100644
--- a/src/pobsync_backend/views.py
+++ b/src/pobsync_backend/views.py
@@ -661,13 +661,13 @@ def resolve_run_review(request, run_id: int):
return redirect("run_detail", run_id=run.id)
if run.reviewed_at:
messages.info(request, f"Run {run.id} was already marked reviewed.")
- return redirect("run_detail", run_id=run.id)
+ return _redirect_after_run_review(request, run)
run.reviewed_at = timezone.now()
run.reviewed_by = request.user.get_username()
run.save(update_fields=["reviewed_at", "reviewed_by"])
messages.success(request, f"Run {run.id} marked reviewed.")
- return redirect("run_detail", run_id=run.id)
+ return _redirect_after_run_review(request, run)
@staff_member_required
@@ -937,6 +937,13 @@ def _next_run_for_schedule(schedule: ScheduleConfig | None, host_config: HostCon
return None
+def _redirect_after_run_review(request, run: BackupRun):
+ next_url = request.POST.get("next", "").strip()
+ if next_url.startswith("/"):
+ return redirect(next_url)
+ return redirect("run_detail", run_id=run.id)
+
+
def _retention_warning_for_host(host_config: HostConfig, schedule: ScheduleConfig | None) -> dict[str, object]:
incomplete_count = host_config.snapshots.filter(
kind=SnapshotRecord.Kind.INCOMPLETE,