(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.
This commit is contained in:
2026-05-19 23:05:22 +02:00
parent 86eee0f916
commit 124421b85c
5 changed files with 20 additions and 7 deletions

View File

@@ -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),
}

View File

@@ -90,7 +90,14 @@
<span class="muted">none</span>
{% endif %}
</td>
<td>{% if host.next_run_at %}{{ host.next_run_at }}{% else %}<span class="muted">none</span>{% endif %}</td>
<td>
{% if host.next_run_at %}
{{ host.next_run_at|date:"Y-m-d H:i T" }}
<div class="muted">{{ scheduler_timezone }}</div>
{% else %}
<span class="muted">none</span>
{% endif %}
</td>
<td>{{ host.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}</td>
<td>{{ host.run_count }}</td>
<td>d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}</td>

View File

@@ -52,7 +52,7 @@
<div><strong>Schedule expression:</strong> {{ schedule.cron_expr }}</div>
<div class="muted">Evaluated by the pobsync scheduler service.</div>
<div><strong>Enabled:</strong> {{ schedule.enabled|yesno:"yes,no" }}</div>
<div><strong>Next run:</strong> {{ next_run_at|default:"" }}</div>
<div><strong>Next run:</strong> {% if next_run_at %}{{ next_run_at|date:"Y-m-d H:i T" }} <span class="muted">{{ scheduler_timezone }}</span>{% endif %}</div>
<div><strong>Prune:</strong> {{ schedule.prune|yesno:"yes,no" }}</div>
<div><strong>Last status:</strong> {{ schedule.last_status|default:"" }}</div>
<div><strong>Last started:</strong> {{ schedule.last_started_at|default:"" }}</div>

View File

@@ -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")

View File

@@ -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),