Create the default global config before queueing backups.
+ {% elif not host.enabled %} +Enable this host before queueing backups.
+ {% endif %} + {% endif %} + +{{ result_json }}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py
index d2bd253..e82595c 100644
--- a/src/pobsync_backend/tests/test_views.py
+++ b/src/pobsync_backend/tests/test_views.py
@@ -211,6 +211,7 @@ class ViewTests(TestCase):
def test_host_detail_renders_config_schedule_runs_and_snapshots(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",
@@ -231,12 +232,38 @@ class ViewTests(TestCase):
self.assertContains(response, "Discover snapshots")
self.assertContains(response, "Edit schedule")
self.assertContains(response, "Edit config")
- self.assertContains(response, "Queue Manual Backup")
+ self.assertContains(response, "Backup Control")
+ self.assertContains(response, "Queue dry-run")
+ self.assertContains(response, "Queue backup")
+ self.assertContains(response, "ready")
self.assertContains(response, "Snapshot Discovery")
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]))
+ def test_host_detail_surfaces_active_backup_run(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")
+ run = BackupRun.objects.create(host=host, status=BackupRun.Status.QUEUED)
+
+ response = self.client.get(reverse("host_detail", args=[host.host]))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "queued")
+ self.assertContains(response, f"Run {run.id}")
+ self.assertContains(response, reverse("run_detail", args=[run.id]))
+
+ def test_host_detail_disables_backup_controls_without_global_config(self) -> None:
+ self.client.force_login(self.staff_user)
+ host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
+
+ response = self.client.get(reverse("host_detail", args=[host.host]))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "missing global config")
+ self.assertContains(response, "Create the default global config before queueing backups.")
+
def test_host_detail_renders_discovery_status_for_existing_snapshot_dirs(self) -> None:
self.client.force_login(self.staff_user)
with TemporaryDirectory() as tmp:
@@ -292,6 +319,29 @@ class ViewTests(TestCase):
},
)
+ def test_queue_manual_backup_quick_action_can_queue_real_backup(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")
+
+ response = self.client.post(
+ reverse("queue_manual_backup", args=[host.host]),
+ {"prune_max_delete": "10"},
+ follow=True,
+ )
+
+ run = BackupRun.objects.get(host=host)
+ self.assertRedirects(response, reverse("run_detail", args=[run.id]))
+ self.assertEqual(
+ run.result["requested"],
+ {
+ "dry_run": False,
+ "prune": False,
+ "prune_max_delete": 10,
+ "prune_protect_bases": False,
+ },
+ )
+
def test_queue_manual_backup_requires_default_global_config(self) -> None:
self.client.force_login(self.staff_user)
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
@@ -332,7 +382,16 @@ class ViewTests(TestCase):
snapshot_path=snapshot.path,
base_path="/backups/web-01/scheduled/base",
rsync_exit_code=0,
- result={"ok": True, "snapshot": snapshot.path},
+ result={
+ "ok": True,
+ "snapshot": snapshot.path,
+ "requested": {
+ "dry_run": True,
+ "prune": False,
+ "prune_max_delete": 10,
+ "prune_protect_bases": False,
+ },
+ },
)
response = self.client.get(reverse("run_detail", args=[run.id]))
@@ -342,6 +401,8 @@ class ViewTests(TestCase):
self.assertContains(response, "web-01")
self.assertContains(response, "success")
self.assertContains(response, "ABCDEFGH")
+ self.assertContains(response, "Requested Options")
+ self.assertContains(response, "Dry run: yes")
self.assertContains(response, ""ok": true")
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py
index a838456..a1fffad 100644
--- a/src/pobsync_backend/views.py
+++ b/src/pobsync_backend/views.py
@@ -104,16 +104,27 @@ def create_host_config(request):
@staff_member_required
def host_detail(request, host: str):
host_config = get_object_or_404(HostConfig, host=host)
+ queued_runs = host_config.runs.filter(status=BackupRun.Status.QUEUED)
+ running_runs = host_config.runs.filter(status=BackupRun.Status.RUNNING)
+ active_run = host_config.runs.filter(
+ status__in=[BackupRun.Status.QUEUED, BackupRun.Status.RUNNING]
+ ).order_by("created_at", "id").first()
+ has_global_config = GlobalConfig.objects.filter(name="default").exists()
context = {
"host": host_config,
"schedule": _schedule_for_host(host_config),
"discovery": inspect_snapshot_discovery(host=host_config),
"manual_backup_form": ManualBackupForm(initial=_default_manual_backup_initial(host_config)),
+ "can_queue_backup": host_config.enabled and has_global_config,
+ "has_global_config": has_global_config,
+ "active_run": active_run,
"latest_runs": host_config.runs.select_related("snapshot").order_by("-created_at")[:10],
"snapshots": host_config.snapshots.select_related("base").order_by("-started_at", "dirname")[:20],
"counts": {
"snapshots": host_config.snapshots.count(),
"runs": host_config.runs.count(),
+ "queued_runs": queued_runs.count(),
+ "running_runs": running_runs.count(),
"failed_runs": host_config.runs.filter(status=BackupRun.Status.FAILED).count(),
"incomplete_snapshots": host_config.snapshots.filter(kind=SnapshotRecord.Kind.INCOMPLETE).count(),
},
@@ -153,6 +164,7 @@ def run_detail(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
context = {
"run": run,
+ "requested": run.result.get("requested") if isinstance(run.result, dict) else {},
"result_json": _pretty_json(run.result),
}
return render(request, "pobsync_backend/run_detail.html", context)