(feature) Add backup trend forecasting to the Django UI

Extend derived backup statistics with average daily new data and an
estimated days-until-full forecast based on recent successful real runs.

Show the new forecast metrics on the dashboard and add compact per-run
trend bars on the host detail page so new data and matched link-dest data
are easier to compare at a glance.

Keep the implementation migration-free by deriving everything from the
existing BackupRun result stats payload.
This commit is contained in:
2026-05-19 22:39:46 +02:00
parent fc22842fc4
commit e8169eae42
5 changed files with 103 additions and 1 deletions

View File

@@ -85,6 +85,8 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Backup Root Used")
self.assertContains(response, "Runs Until Full")
self.assertContains(response, "Avg Daily New")
self.assertContains(response, "Days Until Full")
self.assertContains(response, "10")
self.assertContains(response, f"Run {run.id}")
self.assertContains(response, "1000")
@@ -582,16 +584,38 @@ class ViewTests(TestCase):
},
},
)
BackupRun.objects.create(
host=host,
status=BackupRun.Status.SUCCESS,
snapshot=snapshot,
started_at=datetime(2026, 5, 18, 2, 15, tzinfo=timezone.utc),
result={
"ok": True,
"dry_run": False,
"stats": {
"duration_seconds": 35,
"rsync": {
"files_total": 150,
"literal_data_bytes": 1024,
"matched_data_bytes": 4096,
},
},
},
)
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, "Avg Daily New")
self.assertContains(response, "45s")
self.assertContains(response, "250")
self.assertContains(response, "2.0")
self.assertContains(response, "KB")
self.assertContains(response, "Run data trend")
self.assertContains(response, "width: 100%")
self.assertContains(response, "width: 50%")
def test_prepare_host_directories_action_creates_missing_directories(self) -> None:
self.client.force_login(self.staff_user)