(ui) Surface run debugging details in Django

Restructure the run detail page into clearer sections for summary, failure
classification, requested options, rsync command, rsync log output, stats,
retention, and raw result data.

Show recent rsync log output inline with a link to the full log, and promote
failure and retention warning details out of the JSON payload so failed or slow
runs are easier to debug from the control panel.
This commit is contained in:
2026-05-21 00:17:39 +02:00
parent d8c0ee5d1e
commit 98695f9888
4 changed files with 136 additions and 6 deletions

View File

@@ -857,6 +857,11 @@ class ViewTests(TestCase):
result={
"ok": True,
"snapshot": snapshot.path,
"rsync": {
"command": ["rsync", "--archive", "root@web-01:/", snapshot.path],
"exit_code": 0,
"log_tail": ["sending incremental file list", "sent 500 bytes"],
},
"requested": {
"dry_run": True,
"verbose_output": True,
@@ -889,6 +894,10 @@ class ViewTests(TestCase):
self.assertContains(response, "Requested Options")
self.assertContains(response, "Dry run:</strong> yes")
self.assertContains(response, "Verbose rsync output:</strong> yes")
self.assertContains(response, "Rsync Command")
self.assertContains(response, "--archive")
self.assertContains(response, "Rsync Log")
self.assertContains(response, "sending incremental file list")
self.assertContains(response, "Stats")
self.assertContains(response, "Files seen:</strong> 10")
self.assertContains(response, "Estimated link-dest saving")
@@ -901,7 +910,7 @@ class ViewTests(TestCase):
with TemporaryDirectory() as tmp:
log_path = Path(tmp) / "snapshot" / "meta" / "rsync.log"
log_path.parent.mkdir(parents=True)
log_path.write_text("rsync log line\n", encoding="utf-8")
log_path.write_text("old line\nrsync log line\n", encoding="utf-8")
run = BackupRun.objects.create(
host=host,
status=BackupRun.Status.SUCCESS,
@@ -915,8 +924,40 @@ class ViewTests(TestCase):
self.assertContains(response, reverse("run_rsync_log", args=[run.id]))
self.assertContains(response, str(log_path))
self.assertContains(response, "rsync log line")
self.assertEqual(log_response.status_code, 200)
self.assertEqual(log_body, b"rsync log line\n")
self.assertEqual(log_body, b"old line\nrsync log line\n")
def test_run_detail_surfaces_failure_and_retention_warning(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.WARNING,
rsync_exit_code=0,
result={
"ok": True,
"failure": {
"category": "transport",
"summary": "SSH connection dropped.",
"hint": "Check network connectivity.",
},
"prune": {
"ok": False,
"type": "ConfigError",
"error": "Deletion blocked by --max-delete=0",
},
},
)
response = self.client.get(reverse("run_detail", args=[run.id]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Failure")
self.assertContains(response, "transport")
self.assertContains(response, "Check network connectivity.")
self.assertContains(response, "Retention")
self.assertContains(response, "Deletion blocked by --max-delete=0")
def test_run_detail_infers_rsync_log_from_snapshot_path(self) -> None:
self.client.force_login(self.staff_user)