(feature) Summarize backup trends in the Django UI

Add a stats summary layer that aggregates recent successful real backup runs
into dashboard and host-level trend metrics.

Show backup-root usage, available space, average new data, average duration,
estimated runs until full, and link-dest savings on the dashboard. Add a host
trend table with recent run duration, file count, new data, matched data, and
snapshot links.

Keep the implementation based on existing run and snapshot stats JSON so the
UI gains useful trend visibility without introducing a schema migration yet.
This commit is contained in:
2026-05-19 22:31:24 +02:00
parent 6940dc55b7
commit fc22842fc4
5 changed files with 298 additions and 3 deletions

View File

@@ -35,7 +35,7 @@ class ViewTests(TestCase):
self.client.force_login(self.staff_user)
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
snapshot = self._snapshot(host, "20260519-021500Z__ABCDEFGH")
BackupRun.objects.create(
run = BackupRun.objects.create(
host=host,
status=BackupRun.Status.SUCCESS,
snapshot=snapshot,
@@ -50,6 +50,45 @@ class ViewTests(TestCase):
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
self.assertContains(response, "success")
def test_dashboard_renders_backup_trend_summary(self) -> None:
self.client.force_login(self.staff_user)
GlobalConfig.objects.create(name="default", backup_root="/missing-backup-root")
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
snapshot = self._snapshot(host, "20260519-021500Z__ABCDEFGH")
run = BackupRun.objects.create(
host=host,
status=BackupRun.Status.SUCCESS,
snapshot=snapshot,
started_at=datetime(2026, 5, 19, 2, 15, tzinfo=timezone.utc),
result={
"ok": True,
"dry_run": False,
"stats": {
"duration_seconds": 30,
"rsync": {
"files_total": 100,
"literal_data_bytes": 1000,
"matched_data_bytes": 4000,
},
"storage": {
"capacity": {
"available_bytes": 10_000,
"used_percent": 25.0,
}
},
},
},
)
response = self.client.get(reverse("dashboard"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Backup Root Used")
self.assertContains(response, "Runs Until Full")
self.assertContains(response, "10")
self.assertContains(response, f"Run {run.id}")
self.assertContains(response, "1000")
def test_dashboard_links_latest_snapshot_for_each_host(self) -> None:
self.client.force_login(self.staff_user)
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
@@ -520,6 +559,40 @@ class ViewTests(TestCase):
self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id]))
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
def test_host_detail_renders_backup_trends(self) -> None:
self.client.force_login(self.staff_user)
GlobalConfig.objects.create(name="default", backup_root="/backups")
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
snapshot = self._snapshot(host, "20260519-021500Z__ABCDEFGH")
BackupRun.objects.create(
host=host,
status=BackupRun.Status.SUCCESS,
snapshot=snapshot,
started_at=datetime(2026, 5, 19, 2, 15, tzinfo=timezone.utc),
result={
"ok": True,
"dry_run": False,
"stats": {
"duration_seconds": 45,
"rsync": {
"files_total": 250,
"literal_data_bytes": 2048,
"matched_data_bytes": 8192,
},
},
},
)
response = self.client.get(reverse("host_detail", args=[host.host]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Backup Trends")
self.assertContains(response, "Avg New Data")
self.assertContains(response, "45s")
self.assertContains(response, "250")
self.assertContains(response, "2.0")
self.assertContains(response, "KB")
def test_prepare_host_directories_action_creates_missing_directories(self) -> None:
self.client.force_login(self.staff_user)
with TemporaryDirectory() as tmp: