(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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user