(feature) Show backup data totals by snapshot kind
Aggregate snapshot storage metadata by snapshot kind so operators can see scheduled, manual, incomplete, and total backup data. Surface the totals per host and across all hosts on the dashboard, using allocated snapshot size from recorded backup metadata without rescanning backup storage.
This commit is contained in:
@@ -190,6 +190,29 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "running")
|
||||
self.assertNotContains(response, "<html", html=False)
|
||||
|
||||
def test_dashboard_priority_live_renders_global_backup_data_totals(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
web = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
db = HostConfig.objects.create(host="db-01", address="db-01.example.test")
|
||||
scheduled = self._snapshot(web, "20260519-021500Z__SCHED01", kind=SnapshotRecord.Kind.SCHEDULED)
|
||||
manual = self._snapshot(web, "20260519-031500Z__MANUAL1", kind=SnapshotRecord.Kind.MANUAL)
|
||||
incomplete = self._snapshot(db, "20260519-041500Z__BROKEN1", kind=SnapshotRecord.Kind.INCOMPLETE)
|
||||
self._set_snapshot_storage(scheduled, allocated=100)
|
||||
self._set_snapshot_storage(manual, allocated=200)
|
||||
self._set_snapshot_storage(incomplete, allocated=300)
|
||||
|
||||
response = self.client.get(reverse("dashboard_priority_live"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Scheduled data")
|
||||
self.assertContains(response, "Manual data")
|
||||
self.assertContains(response, "Incomplete data")
|
||||
self.assertContains(response, "Total snapshot data")
|
||||
self.assertContains(response, "100 bytes", html=True)
|
||||
self.assertContains(response, "200 bytes", html=True)
|
||||
self.assertContains(response, "300 bytes", html=True)
|
||||
self.assertContains(response, "600 bytes", html=True)
|
||||
|
||||
def test_dashboard_hosts_live_returns_hosts_partial(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
@@ -203,6 +226,28 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, "Snapshot health")
|
||||
self.assertNotContains(response, "<html", html=False)
|
||||
|
||||
def test_dashboard_host_cards_render_backup_data_totals(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
scheduled = self._snapshot(host, "20260519-021500Z__SCHED01", kind=SnapshotRecord.Kind.SCHEDULED)
|
||||
manual = self._snapshot(host, "20260519-031500Z__MANUAL1", kind=SnapshotRecord.Kind.MANUAL)
|
||||
incomplete = self._snapshot(host, "20260519-041500Z__BROKEN1", kind=SnapshotRecord.Kind.INCOMPLETE)
|
||||
self._set_snapshot_storage(scheduled, allocated=100)
|
||||
self._set_snapshot_storage(manual, allocated=200)
|
||||
self._set_snapshot_storage(incomplete, allocated=300)
|
||||
|
||||
response = self.client.get(reverse("dashboard_hosts_live"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Scheduled data")
|
||||
self.assertContains(response, "Manual data")
|
||||
self.assertContains(response, "Incomplete data")
|
||||
self.assertContains(response, "Total data")
|
||||
self.assertContains(response, "100 bytes", html=True)
|
||||
self.assertContains(response, "200 bytes", html=True)
|
||||
self.assertContains(response, "300 bytes", html=True)
|
||||
self.assertContains(response, "600 bytes", html=True)
|
||||
|
||||
def test_hosts_list_renders_host_cards_and_controls(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
web = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
@@ -1075,6 +1120,7 @@ class ViewTests(TestCase):
|
||||
(backup_root / host.host / subdir).mkdir(parents=True)
|
||||
ScheduleConfig.objects.create(host=host, cron_expr="15 2 * * *", prune=True, last_status="success")
|
||||
snapshot = self._snapshot(host, "20260519-021500Z__ABCDEFGH")
|
||||
self._set_snapshot_storage(snapshot, allocated=100)
|
||||
BackupRun.objects.create(host=host, status=BackupRun.Status.SUCCESS, snapshot=snapshot)
|
||||
|
||||
response = self.client.get(reverse("host_detail", args=[host.host]))
|
||||
@@ -1099,6 +1145,8 @@ class ViewTests(TestCase):
|
||||
self.assertContains(response, reverse("prepare_host_directories", args=[host.host]))
|
||||
self.assertContains(response, "warning")
|
||||
self.assertContains(response, "Snapshot Storage")
|
||||
self.assertContains(response, "Backup Data")
|
||||
self.assertContains(response, "100 bytes", html=True)
|
||||
self.assertContains(response, reverse("queue_manual_backup", args=[host.host]))
|
||||
self.assertContains(response, reverse("run_detail", args=[BackupRun.objects.get().id]))
|
||||
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
|
||||
@@ -2606,3 +2654,16 @@ class ViewTests(TestCase):
|
||||
status="success",
|
||||
started_at=started_at,
|
||||
)
|
||||
|
||||
def _set_snapshot_storage(self, snapshot: SnapshotRecord, *, allocated: int) -> None:
|
||||
snapshot.metadata = {
|
||||
"stats": {
|
||||
"storage": {
|
||||
"snapshot": {
|
||||
"apparent_size_bytes": allocated * 2,
|
||||
"allocated_size_bytes": allocated,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.save(update_fields=["metadata"])
|
||||
|
||||
Reference in New Issue
Block a user