From 124421b85c841583e3bac0aabef5913e38d835a4 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Tue, 19 May 2026 23:05:22 +0200 Subject: [PATCH] (bugfix) Show latest successful runs without requiring stats Separate operational latest-run display from trend-stat collection so successful backups without parsed stats still appear in dashboard host rows. Keep trend summaries limited to runs with stats, but use all successful real runs for the host latest-run indicator. Render next scheduled run times with an explicit timezone label to avoid ambiguity between UTC and local scheduler time. --- src/pobsync_backend/stats_summary.py | 10 +++++----- .../templates/pobsync_backend/dashboard.html | 9 ++++++++- .../templates/pobsync_backend/host_detail.html | 2 +- src/pobsync_backend/tests/test_views.py | 4 ++++ src/pobsync_backend/views.py | 2 ++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/pobsync_backend/stats_summary.py b/src/pobsync_backend/stats_summary.py index b6802f7..ab13149 100644 --- a/src/pobsync_backend/stats_summary.py +++ b/src/pobsync_backend/stats_summary.py @@ -54,23 +54,23 @@ def collect_dashboard_stats(*, hosts: Iterable[HostConfig], global_config: Globa def collect_host_stats(*, host: HostConfig, limit: int = 8) -> dict[str, Any]: runs = list(host.runs.select_related("snapshot").filter(status=BackupRun.Status.SUCCESS).order_by("-started_at", "-created_at")[:50]) real_runs = [_run_summary(run) for run in runs if _is_real_run(run)] - real_runs = [run for run in real_runs if run["has_stats"]][:limit] + trend_runs = [run for run in real_runs if run["has_stats"]][:limit] latest_snapshot = host.snapshots.order_by("-started_at", "-discovered_at", "-id").first() latest_snapshot_stats = _snapshot_summary(latest_snapshot) if latest_snapshot else {} - literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in real_runs] + literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in trend_runs] literal_values = [value for value in literal_values if value is not None] - matched_values = [_int_at(run, "rsync", "matched_data_bytes") for run in real_runs] + matched_values = [_int_at(run, "rsync", "matched_data_bytes") for run in trend_runs] matched_values = [value for value in matched_values if value is not None] max_literal = max(literal_values) if literal_values else 0 max_matched = max(matched_values) if matched_values else 0 return { - "runs": [_with_bar_percentages(run, max_literal=max_literal, max_matched=max_matched) for run in real_runs], + "runs": [_with_bar_percentages(run, max_literal=max_literal, max_matched=max_matched) for run in trend_runs], "latest_run": real_runs[0] if real_runs else {}, "latest_snapshot": latest_snapshot_stats, "avg_literal_data_bytes": _average(literal_values), - "avg_daily_literal_data_bytes": _average_daily_literal(real_runs), + "avg_daily_literal_data_bytes": _average_daily_literal(trend_runs), "total_literal_data_bytes": sum(literal_values), "total_matched_data_bytes": sum(matched_values), } diff --git a/src/pobsync_backend/templates/pobsync_backend/dashboard.html b/src/pobsync_backend/templates/pobsync_backend/dashboard.html index e12fcfe..5daff1c 100644 --- a/src/pobsync_backend/templates/pobsync_backend/dashboard.html +++ b/src/pobsync_backend/templates/pobsync_backend/dashboard.html @@ -90,7 +90,14 @@ none {% endif %} - {% if host.next_run_at %}{{ host.next_run_at }}{% else %}none{% endif %} + + {% if host.next_run_at %} + {{ host.next_run_at|date:"Y-m-d H:i T" }} +
{{ scheduler_timezone }}
+ {% else %} + none + {% endif %} + {{ host.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }} {{ host.run_count }} d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }} diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index b176998..6fc1932 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -52,7 +52,7 @@
Schedule expression: {{ schedule.cron_expr }}
Evaluated by the pobsync scheduler service.
Enabled: {{ schedule.enabled|yesno:"yes,no" }}
-
Next run: {{ next_run_at|default:"" }}
+
Next run: {% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }} {{ scheduler_timezone }}{% endif %}
Prune: {{ schedule.prune|yesno:"yes,no" }}
Last status: {{ schedule.last_status|default:"" }}
Last started: {{ schedule.last_started_at|default:"" }}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index e5bc391..76569ee 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -50,6 +50,8 @@ class ViewTests(TestCase): self.assertContains(response, "web-01") self.assertContains(response, "20260519-021500Z__ABCDEFGH") self.assertContains(response, "success") + self.assertContains(response, f"Run {run.id}") + self.assertContains(response, "manual") def test_dashboard_renders_backup_trend_summary(self) -> None: self.client.force_login(self.staff_user) @@ -91,6 +93,7 @@ class ViewTests(TestCase): self.assertContains(response, "Avg Daily New") self.assertContains(response, "Days Until Full") self.assertContains(response, "Next Run") + self.assertContains(response, "UTC") self.assertContains(response, "10") self.assertContains(response, f"Run {run.id}") self.assertContains(response, "manual") @@ -554,6 +557,7 @@ class ViewTests(TestCase): self.assertContains(response, "Schedule expression") self.assertContains(response, "Evaluated by the pobsync scheduler service.") self.assertContains(response, "Next run:") + self.assertContains(response, "UTC") self.assertContains(response, "20260519-021500Z__ABCDEFGH") self.assertContains(response, "Discover snapshots") self.assertContains(response, "Edit schedule") diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index aae4720..8bbce78 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -58,6 +58,7 @@ def dashboard(request): "hosts": hosts, "global_config": global_config, "stats_summary": stats_summary, + "scheduler_timezone": timezone.get_current_timezone_name(), "latest_runs": BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")[:10], "counts": { "global_configs": GlobalConfig.objects.count(), @@ -271,6 +272,7 @@ def host_detail(request, host: str): "host": host_config, "schedule": schedule, "next_run_at": _next_run_for_schedule(schedule, host_config), + "scheduler_timezone": timezone.get_current_timezone_name(), "discovery": inspect_snapshot_discovery(host=host_config), "host_checks": host_checks, "host_check_summary": summarize_self_checks(host_checks),