diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index e748d4d..32cfc37 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -34,7 +34,7 @@
Hosts
{{ counts.enabled_hosts }}/{{ counts.hosts }}
-
Schedules
{{ counts.enabled_schedules }}/{{ counts.schedules }}
+
Schedules
{{ counts.enabled_schedules }}/{{ counts.schedules }}
Snapshots
{{ counts.snapshots }}
Runs
{{ counts.runs }}
Queued
{{ counts.queued_runs }}
diff --git a/src/pobsync_backend/templates/pobsync_backend/schedules_list.html b/src/pobsync_backend/templates/pobsync_backend/schedules_list.html new file mode 100644 index 0000000..ada648b --- /dev/null +++ b/src/pobsync_backend/templates/pobsync_backend/schedules_list.html @@ -0,0 +1,101 @@ +{% extends "pobsync_backend/base.html" %} + +{% block title %}Schedules | pobsync{% endblock %} + +{% block content %} + + +
+

Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + Clear +
+
+
+ +
+

Configured Schedules

+

Showing up to 200 of {{ total_count }} schedule{{ total_count|pluralize }}. Times use {{ scheduler_timezone }}.

+ + + + + + + + + + + + + + + + {% for row in schedule_rows %} + {% with schedule=row.schedule %} + + + + + + + + + + + + {% endwith %} + {% empty %} + + {% endfor %} + +
HostExpressionEnabledNext RunPruneLast StatusLast StartedLast FinishedActions
{{ schedule.host.host }}{{ schedule.cron_expr }}{{ schedule.enabled|yesno:"enabled,disabled" }} + {% if row.next_run_at %} + {{ row.next_run_at|date:"Y-m-d H:i T" }} + {% else %} + none + {% endif %} + + {{ schedule.prune|yesno:"enabled,disabled" }} + {% if schedule.prune %} +
max {{ schedule.prune_max_delete }}{% if schedule.prune_protect_bases %}, protects bases{% endif %}
+ {% endif %} +
{% if schedule.last_status %}{{ schedule.last_status }}{% else %}none{% endif %}{{ schedule.last_started_at|default:"" }}{{ schedule.last_finished_at|default:"" }}Edit
No schedules matched the current filter.
+
+{% endblock %} diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index cef4230..3437007 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -135,6 +135,7 @@ class ViewTests(TestCase): self.assertContains(response, f'href="{reverse("runs_list")}?status=warning&review=needed"', html=False) self.assertContains(response, f'href="{reverse("runs_list")}?status=failed&review=needed"', html=False) self.assertContains(response, f'href="{reverse("snapshots_list")}"', html=False) + self.assertContains(response, f'href="{reverse("schedules_list")}"', html=False) def test_dashboard_renders_backup_trend_summary(self) -> None: self.client.force_login(self.staff_user) @@ -249,6 +250,24 @@ class ViewTests(TestCase): self.assertContains(response, "web-01") self.assertNotContains(response, scheduled.dirname) + def test_schedules_list_filters_by_enabled_and_prune(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") + ScheduleConfig.objects.create(host=web, cron_expr="15 2 * * *", enabled=True, prune=True, last_status="success") + ScheduleConfig.objects.create(host=db, cron_expr="30 3 * * *", enabled=False, prune=False, last_status="failed") + + response = self.client.get(reverse("schedules_list"), {"enabled": "yes", "prune": "yes"}) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Schedules") + self.assertContains(response, "Review configured backup schedules") + self.assertContains(response, "web-01") + self.assertContains(response, "15 2 * * *") + self.assertContains(response, "success") + self.assertContains(response, "UTC") + self.assertNotContains(response, "30 3 * * *") + def test_dashboard_surfaces_retention_warnings(self) -> None: self.client.force_login(self.staff_user) host = HostConfig.objects.create( diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 70fb456..e319485 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -203,6 +203,44 @@ def snapshots_list(request): return render(request, "pobsync_backend/snapshots_list.html", context) +@staff_member_required +def schedules_list(request): + enabled = request.GET.get("enabled", "").strip() + prune = request.GET.get("prune", "").strip() + host = request.GET.get("host", "").strip() + schedules = ScheduleConfig.objects.select_related("host").order_by("host__host") + if enabled == "yes": + schedules = schedules.filter(enabled=True) + elif enabled == "no": + schedules = schedules.filter(enabled=False) + if prune == "yes": + schedules = schedules.filter(prune=True) + elif prune == "no": + schedules = schedules.filter(prune=False) + if host: + schedules = schedules.filter(host__host=host) + + schedule_rows = [] + for schedule in schedules[:200]: + schedule_rows.append( + { + "schedule": schedule, + "next_run_at": _next_run_for_schedule(schedule, schedule.host), + } + ) + + context = { + "schedule_rows": schedule_rows, + "total_count": schedules.count(), + "hosts": HostConfig.objects.order_by("host"), + "selected_enabled": enabled, + "selected_prune": prune, + "selected_host": host, + "scheduler_timezone": timezone.get_current_timezone_name(), + } + return render(request, "pobsync_backend/schedules_list.html", context) + + @staff_member_required def purged_snapshots(request): host = request.GET.get("host", "").strip() diff --git a/src/pobsync_server/urls.py b/src/pobsync_server/urls.py index 934a1b2..da215fe 100644 --- a/src/pobsync_server/urls.py +++ b/src/pobsync_server/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path("self-check/", views.self_check, name="self_check"), path("logs/", views.logs, name="logs"), path("purged-snapshots/", views.purged_snapshots, name="purged_snapshots"), + path("schedules/", views.schedules_list, name="schedules_list"), path("config/global/", views.edit_global_config, name="edit_global_config"), path("ssh-credentials/", views.ssh_credentials, name="ssh_credentials"), path("ssh-credentials/new/", views.create_ssh_credential, name="create_ssh_credential"),