From 4e8e4f75fd48151e7329f6aa91c64786861bf884 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 11:52:35 +0200 Subject: [PATCH 1/3] (ui) Add dashboard-linked run and snapshot lists Add staff-only list pages for backup runs and snapshots with practical filters, then wire the dashboard summary cards and latest-runs panel to those overviews. This gives the dashboard real drill-down paths for run and snapshot counts instead of leaving the data only partially visible on the first screen. Refs #23 --- .../templates/pobsync_backend/base.html | 15 +++ .../templates/pobsync_backend/dashboard.html | 20 ++-- .../templates/pobsync_backend/runs_list.html | 106 ++++++++++++++++++ .../pobsync_backend/snapshots_list.html | 96 ++++++++++++++++ src/pobsync_backend/tests/test_views.py | 57 +++++++++- src/pobsync_backend/views.py | 58 ++++++++++ src/pobsync_server/urls.py | 2 + 7 files changed, 341 insertions(+), 13 deletions(-) create mode 100644 src/pobsync_backend/templates/pobsync_backend/runs_list.html create mode 100644 src/pobsync_backend/templates/pobsync_backend/snapshots_list.html diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index 0d321f5..d2695b3 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -168,6 +168,21 @@ .metric.warning { border-color: #e7cf8a; background: #fffaf0; } .metric.running { border-color: #e7cf8a; background: #fffaf0; } .metric.queued { border-color: #b5cdea; background: #eef6ff; } + .metric-link { + color: inherit; + display: block; + text-decoration: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; + } + .metric-link:hover { + border-color: #9eb2c8; + box-shadow: var(--shadow); + transform: translateY(-1px); + } + .metric-link:focus-visible { + outline: 3px solid #93c5fd; + outline-offset: 2px; + } .panel { margin-bottom: 18px; overflow: auto; diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index 358fd72..f78f584 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -33,17 +33,17 @@ {% endif %}
-
Hosts
{{ counts.enabled_hosts }}/{{ counts.hosts }}
-
Schedules
{{ counts.enabled_schedules }}/{{ counts.schedules }}
-
Snapshots
{{ counts.snapshots }}
-
Runs
{{ counts.runs }}
-
Queued
{{ counts.queued_runs }}
-
Running
{{ counts.running_runs }}
-
Warnings
{{ counts.warning_runs }}
-
Failed
{{ counts.failed_runs }}
+
Hosts
{{ counts.enabled_hosts }}/{{ counts.hosts }}
+
Schedules
{{ counts.enabled_schedules }}/{{ counts.schedules }}
+
Snapshots
{{ counts.snapshots }}
+
Runs
{{ counts.runs }}
+
Queued
{{ counts.queued_runs }}
+
Running
{{ counts.running_runs }}
+
Warnings
{{ counts.warning_runs }}
+
Failed
{{ counts.failed_runs }}
-
+

Operational Status

{% if counts.failed_runs or counts.warning_runs or counts.running_runs or counts.queued_runs %}
@@ -266,7 +266,7 @@
-

Latest Runs

+

Latest Runs View all

diff --git a/src/pobsync_backend/templates/pobsync_backend/runs_list.html b/src/pobsync_backend/templates/pobsync_backend/runs_list.html new file mode 100644 index 0000000..a8dc10d --- /dev/null +++ b/src/pobsync_backend/templates/pobsync_backend/runs_list.html @@ -0,0 +1,106 @@ +{% extends "pobsync_backend/base.html" %} + +{% block title %}Runs | pobsync{% endblock %} + +{% block content %} + + +
+

Filters

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

Backup Runs

+

Showing up to 200 of {{ total_count }} run{{ total_count|pluralize }}.

+
+ + + + + + + + + + + + + + + {% for run in runs %} + + + + + + + + + + + + {% empty %} + + {% endfor %} + +
RunHostStatusTypeCreatedStartedEndedSnapshotReview
Run {{ run.id }}{{ run.host.host }}{{ run.status }}{{ run.run_type }}{{ run.created_at }}{{ run.started_at|default:"" }}{{ run.ended_at|default:"" }} + {% if run.snapshot %} + {{ run.snapshot.dirname }} + {% elif run.snapshot_path %} + {{ run.snapshot_path }} + {% else %} + none + {% endif %} + {% if run.reviewed_at %}reviewed{% elif run.status == "failed" or run.status == "warning" %}needed{% else %}none{% endif %}
No runs matched the current filter.
+
+{% endblock %} diff --git a/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html b/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html new file mode 100644 index 0000000..a6d8e7c --- /dev/null +++ b/src/pobsync_backend/templates/pobsync_backend/snapshots_list.html @@ -0,0 +1,96 @@ +{% extends "pobsync_backend/base.html" %} + +{% block title %}Snapshots | pobsync{% endblock %} + +{% block content %} + + +
+

Filters

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

Snapshot Records

+

Showing up to 200 of {{ total_count }} snapshot{{ total_count|pluralize }}.

+ + + + + + + + + + + + + + + {% for snapshot in snapshots %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
SnapshotHostKindStatusStartedEndedBasePath
{{ snapshot.dirname }}{{ snapshot.host.host }}{{ snapshot.kind }}{% if snapshot.status %}{{ snapshot.status }}{% else %}unknown{% endif %}{{ snapshot.started_at|default:"" }}{{ snapshot.ended_at|default:"" }} + {% if snapshot.base %} + {{ snapshot.base.dirname }} + {% elif snapshot.base_dirname %} + {{ snapshot.base_dirname }} + {% else %} + none + {% endif %} + {{ snapshot.path }}
No snapshots matched the current filter.
+
+{% endblock %} diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 510f77f..08c7040 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -125,6 +125,12 @@ class ViewTests(TestCase): self.assertContains(response, "1 run completed with warnings.") self.assertContains(response, "1 backup run in progress.") self.assertContains(response, "1 backup run waiting for the worker.") + self.assertContains(response, f'href="{reverse("runs_list")}"', html=False) + self.assertContains(response, f'href="{reverse("runs_list")}?status=queued"', html=False) + self.assertContains(response, f'href="{reverse("runs_list")}?status=running"', html=False) + 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) def test_dashboard_renders_backup_trend_summary(self) -> None: self.client.force_login(self.staff_user) @@ -200,6 +206,45 @@ class ViewTests(TestCase): self.assertContains(response, "Operational Status") self.assertContains(response, "No queued, running, or unreviewed warning/failed runs.") + def test_runs_list_filters_by_status_and_review(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") + failed = BackupRun.objects.create(host=web, status=BackupRun.Status.FAILED, run_type=BackupRun.RunType.MANUAL) + success = BackupRun.objects.create(host=db, status=BackupRun.Status.SUCCESS, run_type=BackupRun.RunType.SCHEDULED) + BackupRun.objects.create( + host=web, + status=BackupRun.Status.WARNING, + reviewed_at=datetime(2026, 5, 19, 4, 15, tzinfo=timezone.utc), + reviewed_by="admin", + ) + + response = self.client.get(reverse("runs_list"), {"status": "failed", "review": "needed"}) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Runs") + self.assertContains(response, "Review queued, running, completed") + self.assertContains(response, f"Run {failed.id}") + self.assertContains(response, "web-01") + self.assertContains(response, "needed") + self.assertNotContains(response, f"Run {success.id}") + + def test_snapshots_list_filters_by_host_and_kind(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") + manual = self._snapshot(web, "20260519-021500Z__MANUAL01", kind=SnapshotRecord.Kind.MANUAL) + scheduled = self._snapshot(db, "20260519-021500Z__SCHED01", kind=SnapshotRecord.Kind.SCHEDULED) + + response = self.client.get(reverse("snapshots_list"), {"host": web.host, "kind": SnapshotRecord.Kind.MANUAL}) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Snapshots") + self.assertContains(response, "Browse discovered scheduled, manual, and incomplete snapshots") + self.assertContains(response, manual.dirname) + self.assertContains(response, "web-01") + self.assertNotContains(response, scheduled.dirname) + def test_dashboard_surfaces_retention_warnings(self) -> None: self.client.force_login(self.staff_user) host = HostConfig.objects.create( @@ -2223,13 +2268,19 @@ class ViewTests(TestCase): self.assertEqual(host.excludes_add, []) self.assertEqual(host.excludes_replace, ["*.cache", "node_modules/"]) - def _snapshot(self, host: HostConfig, dirname: str) -> SnapshotRecord: + def _snapshot( + self, + host: HostConfig, + dirname: str, + *, + kind: str = SnapshotRecord.Kind.SCHEDULED, + ) -> SnapshotRecord: started_at = datetime.strptime(dirname.split("__", 1)[0], "%Y%m%d-%H%M%SZ").replace(tzinfo=timezone.utc) return SnapshotRecord.objects.create( host=host, - kind=SnapshotRecord.Kind.SCHEDULED, + kind=kind, dirname=dirname, - path=f"/backups/{host.host}/scheduled/{dirname}", + path=f"/backups/{host.host}/{kind}/{dirname}", status="success", started_at=started_at, ) diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index bca4d2d..70fb456 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -145,6 +145,64 @@ def logs(request): return render(request, "pobsync_backend/logs.html", context) +@staff_member_required +def runs_list(request): + status = request.GET.get("status", "").strip() + run_type = request.GET.get("type", "").strip() + host = request.GET.get("host", "").strip() + review = request.GET.get("review", "").strip() + runs = BackupRun.objects.select_related("host", "snapshot").order_by("-created_at", "-id") + if status: + runs = runs.filter(status=status) + if run_type: + runs = runs.filter(run_type=run_type) + if host: + runs = runs.filter(host__host=host) + if review == "needed": + runs = runs.filter(status__in=[BackupRun.Status.FAILED, BackupRun.Status.WARNING], reviewed_at__isnull=True) + elif review == "reviewed": + runs = runs.filter(reviewed_at__isnull=False) + + context = { + "runs": runs[:200], + "total_count": runs.count(), + "hosts": HostConfig.objects.order_by("host"), + "statuses": BackupRun.Status.choices, + "run_types": BackupRun.RunType.choices, + "selected_status": status, + "selected_type": run_type, + "selected_host": host, + "selected_review": review, + } + return render(request, "pobsync_backend/runs_list.html", context) + + +@staff_member_required +def snapshots_list(request): + kind = request.GET.get("kind", "").strip() + status = request.GET.get("status", "").strip() + host = request.GET.get("host", "").strip() + snapshots = SnapshotRecord.objects.select_related("host", "base").order_by("-started_at", "-discovered_at", "-id") + if kind: + snapshots = snapshots.filter(kind=kind) + if status: + snapshots = snapshots.filter(status=status) + if host: + snapshots = snapshots.filter(host__host=host) + + context = { + "snapshots": snapshots[:200], + "total_count": snapshots.count(), + "hosts": HostConfig.objects.order_by("host"), + "kinds": SnapshotRecord.Kind.choices, + "statuses": SnapshotRecord.objects.exclude(status="").order_by("status").values_list("status", flat=True).distinct(), + "selected_kind": kind, + "selected_status": status, + "selected_host": host, + } + return render(request, "pobsync_backend/snapshots_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 6f77f1c..934a1b2 100644 --- a/src/pobsync_server/urls.py +++ b/src/pobsync_server/urls.py @@ -34,6 +34,7 @@ urlpatterns = [ name="cleanup_host_incomplete_snapshots", ), path("hosts//schedule/", views.edit_host_schedule, name="edit_host_schedule"), + path("runs/", views.runs_list, name="runs_list"), path("runs//", views.run_detail, name="run_detail"), path("runs//rsync-log/", views.run_rsync_log, name="run_rsync_log"), path("runs//cancel/", views.cancel_run, name="cancel_run"), @@ -43,6 +44,7 @@ urlpatterns = [ views.resolve_host_incomplete_reviews, name="resolve_host_incomplete_reviews", ), + path("snapshots/", views.snapshots_list, name="snapshots_list"), path("snapshots//", views.snapshot_detail, name="snapshot_detail"), path("api/", api.api_index), path("api/status/", api.status), From 67d1af0baadc938da48bb9d760bbd057c692e84d Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 12:00:06 +0200 Subject: [PATCH 2/3] (ui) Make dashboard operational status actionable Turn the dashboard operational status rows into direct links to filtered run lists, so failed, warning, running, and queued states can be investigated from the first screen. Also move the hosts anchor back to the actual Hosts section. Refs #23 --- .../templates/pobsync_backend/base.html | 18 +++++++++++++- .../templates/pobsync_backend/dashboard.html | 24 +++++++++++-------- src/pobsync_backend/tests/test_views.py | 4 ++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index d2695b3..4e8c6a7 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -296,7 +296,23 @@ .status-summary.failed { border-color: #e8b4b4; background: #fff7f7; color: var(--failed); } .status-summary.warning, .status-summary.running { border-color: #e7cf8a; background: #fffaf0; color: var(--running); } - .status-summary.queued { border-color: #b5cdea; background: #eef6ff; color: var(--link); } + .status-summary.queued { border-color: #b5cdea; background: #eef6ff; color: var(--link); } + a.status-summary { + color: inherit; + text-decoration: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; + } + a.status-summary:hover { + border-color: var(--border-strong); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); + } + .status-summary .summary-action { + color: var(--muted-strong); + font-size: 12px; + font-weight: 650; + margin-left: auto; + } .operator-state { align-items: center; display: flex; diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index f78f584..e748d4d 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -43,33 +43,37 @@
Failed
{{ counts.failed_runs }}
-
+

Operational Status

{% if counts.failed_runs or counts.warning_runs or counts.running_runs or counts.queued_runs %} {% elif counts.hosts %} @@ -79,7 +83,7 @@ {% endif %}
-
+

Backup Trends

{% if stats_summary.runs_sampled %}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 08c7040..cef4230 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -125,6 +125,10 @@ class ViewTests(TestCase): self.assertContains(response, "1 run completed with warnings.") self.assertContains(response, "1 backup run in progress.") self.assertContains(response, "1 backup run waiting for the worker.") + self.assertContains(response, "Review failed runs") + self.assertContains(response, "Review warnings") + self.assertContains(response, "View running runs") + self.assertContains(response, "View queued runs") self.assertContains(response, f'href="{reverse("runs_list")}"', html=False) self.assertContains(response, f'href="{reverse("runs_list")}?status=queued"', html=False) self.assertContains(response, f'href="{reverse("runs_list")}?status=running"', html=False) From 01b779c8629ca3678ac299dde50a37359621f868 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 12:39:57 +0200 Subject: [PATCH 3/3] (ui) Add schedule overview for dashboard drill-down Add a staff-only schedules page with filters for host, enabled state, and prune state, including next run and last scheduler state. Wire the dashboard Schedules metric to the new overview so all primary dashboard count cards have useful destinations. Refs #23 --- .../templates/pobsync_backend/dashboard.html | 2 +- .../pobsync_backend/schedules_list.html | 101 ++++++++++++++++++ src/pobsync_backend/tests/test_views.py | 19 ++++ src/pobsync_backend/views.py | 38 +++++++ src/pobsync_server/urls.py | 1 + 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/pobsync_backend/templates/pobsync_backend/schedules_list.html 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"),