## Summary

- Add a dedicated `/hosts/` page with host cards and enabled/disabled filtering.
- Link the dashboard Hosts metric and top navigation to the new page.
- Add host enable/disable plus schedule and scheduled-retention pause/resume actions.

## Tests
- `.venv/bin/python manage.py test src.pobsync_backend.tests.test_views.ViewTests.test_base_navigation_groups_primary_and_system_links src.pobsync_backend.tests.test_views.ViewTests.test_dashboard_renders_hosts_and_latest_runs src.pobsync_backend.tests.test_views.ViewTests.test_dashboard_hosts_live_returns_hosts_partial src.pobsync_backend.tests.test_views.ViewTests.test_hosts_list_renders_host_cards_and_controls src.pobsync_backend.tests.test_views.ViewTests.test_hosts_list_filters_by_enabled_state src.pobsync_backend.tests.test_views.ViewTests.test_update_host_state_toggles_host_schedule_and_retention --verbosity 2`
- `.venv/bin/python manage.py check`
- `.venv/bin/python manage.py test src.pobsync_backend --verbosity 2`

Closes #48
Closes #49
This commit is contained in:
2026-05-23 01:13:32 +02:00
parent 8e83fee7b5
commit ce1cb9d157
7 changed files with 245 additions and 4 deletions

View File

@@ -48,6 +48,7 @@ class ViewTests(TestCase):
self.assertContains(response, 'aria-label="Primary navigation"', html=False)
self.assertContains(response, 'aria-label="System navigation"', html=False)
self.assertContains(response, reverse("dashboard"))
self.assertContains(response, reverse("hosts_list"))
self.assertContains(response, reverse("ssh_credentials"))
self.assertContains(response, reverse("logs"))
self.assertContains(response, reverse("purged_snapshots"))
@@ -202,6 +203,72 @@ class ViewTests(TestCase):
self.assertContains(response, "Snapshot health")
self.assertNotContains(response, "<html", html=False)
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")
db = HostConfig.objects.create(host="db-01", address="db-01.example.test", enabled=False)
ScheduleConfig.objects.create(host=web, cron_expr="15 2 * * *", enabled=True, prune=True)
BackupRun.objects.create(host=web, status=BackupRun.Status.RUNNING)
response = self.client.get(reverse("hosts_list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Inventory")
self.assertContains(response, "Configured backup targets")
self.assertContains(response, "web-01")
self.assertContains(response, "db-01")
self.assertContains(response, "running 1")
self.assertContains(response, "schedule on")
self.assertContains(response, "retention on")
self.assertContains(response, "Disable host")
self.assertContains(response, "Enable host")
self.assertContains(response, "Pause schedule")
self.assertContains(response, "Pause retention")
self.assertContains(response, reverse("update_host_state", args=[web.host]))
self.assertContains(response, reverse("edit_host_config", args=[web.host]))
self.assertContains(response, reverse("edit_host_schedule", args=[web.host]))
def test_hosts_list_filters_by_enabled_state(self) -> None:
self.client.force_login(self.staff_user)
HostConfig.objects.create(host="web-01", address="web-01.example.test")
HostConfig.objects.create(host="db-01", address="db-01.example.test", enabled=False)
response = self.client.get(reverse("hosts_list"), {"enabled": "no"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "db-01")
self.assertNotContains(response, "web-01")
def test_update_host_state_toggles_host_schedule_and_retention(self) -> None:
self.client.force_login(self.staff_user)
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
schedule = ScheduleConfig.objects.create(host=host, cron_expr="15 2 * * *", enabled=True, prune=True)
response = self.client.post(
reverse("update_host_state", args=[host.host]),
{"action": "disable_host", "next": reverse("hosts_list")},
follow=True,
)
self.assertRedirects(response, reverse("hosts_list"))
host.refresh_from_db()
self.assertFalse(host.enabled)
self.assertContains(response, "Disabled host web-01.")
self.client.post(reverse("update_host_state", args=[host.host]), {"action": "disable_schedule"})
self.client.post(reverse("update_host_state", args=[host.host]), {"action": "disable_prune"})
schedule.refresh_from_db()
self.assertFalse(schedule.enabled)
self.assertFalse(schedule.prune)
self.client.post(reverse("update_host_state", args=[host.host]), {"action": "enable_host"})
self.client.post(reverse("update_host_state", args=[host.host]), {"action": "enable_schedule"})
self.client.post(reverse("update_host_state", args=[host.host]), {"action": "enable_prune"})
host.refresh_from_db()
schedule.refresh_from_db()
self.assertTrue(host.enabled)
self.assertTrue(schedule.enabled)
self.assertTrue(schedule.prune)
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")