(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:
@@ -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]:
|
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])
|
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_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 = host.snapshots.order_by("-started_at", "-discovered_at", "-id").first()
|
||||||
latest_snapshot_stats = _snapshot_summary(latest_snapshot) if latest_snapshot else {}
|
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]
|
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]
|
matched_values = [value for value in matched_values if value is not None]
|
||||||
max_literal = max(literal_values) if literal_values else 0
|
max_literal = max(literal_values) if literal_values else 0
|
||||||
max_matched = max(matched_values) if matched_values else 0
|
max_matched = max(matched_values) if matched_values else 0
|
||||||
|
|
||||||
return {
|
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_run": real_runs[0] if real_runs else {},
|
||||||
"latest_snapshot": latest_snapshot_stats,
|
"latest_snapshot": latest_snapshot_stats,
|
||||||
"avg_literal_data_bytes": _average(literal_values),
|
"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_literal_data_bytes": sum(literal_values),
|
||||||
"total_matched_data_bytes": sum(matched_values),
|
"total_matched_data_bytes": sum(matched_values),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,14 @@
|
|||||||
<span class="muted">none</span>
|
<span class="muted">none</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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.stats_summary.latest_run.rsync.literal_data_bytes|filesizeformat }}</td>
|
||||||
<td>{{ host.run_count }}</td>
|
<td>{{ host.run_count }}</td>
|
||||||
<td>d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}</td>
|
<td>d{{ host.retention_daily }} w{{ host.retention_weekly }} m{{ host.retention_monthly }} y{{ host.retention_yearly }}</td>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<div><strong>Schedule expression:</strong> {{ schedule.cron_expr }}</div>
|
<div><strong>Schedule expression:</strong> {{ schedule.cron_expr }}</div>
|
||||||
<div class="muted">Evaluated by the pobsync scheduler service.</div>
|
<div class="muted">Evaluated by the pobsync scheduler service.</div>
|
||||||
<div><strong>Enabled:</strong> {{ schedule.enabled|yesno:"yes,no" }}</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>Prune:</strong> {{ schedule.prune|yesno:"yes,no" }}</div>
|
||||||
<div><strong>Last status:</strong> {{ schedule.last_status|default:"" }}</div>
|
<div><strong>Last status:</strong> {{ schedule.last_status|default:"" }}</div>
|
||||||
<div><strong>Last started:</strong> {{ schedule.last_started_at|default:"" }}</div>
|
<div><strong>Last started:</strong> {{ schedule.last_started_at|default:"" }}</div>
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "web-01")
|
self.assertContains(response, "web-01")
|
||||||
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
||||||
self.assertContains(response, "success")
|
self.assertContains(response, "success")
|
||||||
|
self.assertContains(response, f"Run {run.id}")
|
||||||
|
self.assertContains(response, "manual")
|
||||||
|
|
||||||
def test_dashboard_renders_backup_trend_summary(self) -> None:
|
def test_dashboard_renders_backup_trend_summary(self) -> None:
|
||||||
self.client.force_login(self.staff_user)
|
self.client.force_login(self.staff_user)
|
||||||
@@ -91,6 +93,7 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "Avg Daily New")
|
self.assertContains(response, "Avg Daily New")
|
||||||
self.assertContains(response, "Days Until Full")
|
self.assertContains(response, "Days Until Full")
|
||||||
self.assertContains(response, "Next Run")
|
self.assertContains(response, "Next Run")
|
||||||
|
self.assertContains(response, "UTC")
|
||||||
self.assertContains(response, "10")
|
self.assertContains(response, "10")
|
||||||
self.assertContains(response, f"Run {run.id}")
|
self.assertContains(response, f"Run {run.id}")
|
||||||
self.assertContains(response, "manual")
|
self.assertContains(response, "manual")
|
||||||
@@ -554,6 +557,7 @@ class ViewTests(TestCase):
|
|||||||
self.assertContains(response, "Schedule expression")
|
self.assertContains(response, "Schedule expression")
|
||||||
self.assertContains(response, "Evaluated by the pobsync scheduler service.")
|
self.assertContains(response, "Evaluated by the pobsync scheduler service.")
|
||||||
self.assertContains(response, "Next run:")
|
self.assertContains(response, "Next run:")
|
||||||
|
self.assertContains(response, "UTC")
|
||||||
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
self.assertContains(response, "20260519-021500Z__ABCDEFGH")
|
||||||
self.assertContains(response, "Discover snapshots")
|
self.assertContains(response, "Discover snapshots")
|
||||||
self.assertContains(response, "Edit schedule")
|
self.assertContains(response, "Edit schedule")
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ def dashboard(request):
|
|||||||
"hosts": hosts,
|
"hosts": hosts,
|
||||||
"global_config": global_config,
|
"global_config": global_config,
|
||||||
"stats_summary": stats_summary,
|
"stats_summary": stats_summary,
|
||||||
|
"scheduler_timezone": timezone.get_current_timezone_name(),
|
||||||
"latest_runs": BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")[:10],
|
"latest_runs": BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")[:10],
|
||||||
"counts": {
|
"counts": {
|
||||||
"global_configs": GlobalConfig.objects.count(),
|
"global_configs": GlobalConfig.objects.count(),
|
||||||
@@ -271,6 +272,7 @@ def host_detail(request, host: str):
|
|||||||
"host": host_config,
|
"host": host_config,
|
||||||
"schedule": schedule,
|
"schedule": schedule,
|
||||||
"next_run_at": _next_run_for_schedule(schedule, host_config),
|
"next_run_at": _next_run_for_schedule(schedule, host_config),
|
||||||
|
"scheduler_timezone": timezone.get_current_timezone_name(),
|
||||||
"discovery": inspect_snapshot_discovery(host=host_config),
|
"discovery": inspect_snapshot_discovery(host=host_config),
|
||||||
"host_checks": host_checks,
|
"host_checks": host_checks,
|
||||||
"host_check_summary": summarize_self_checks(host_checks),
|
"host_check_summary": summarize_self_checks(host_checks),
|
||||||
|
|||||||
Reference in New Issue
Block a user