(ui) Add readable dry-run summaries
Surface dry-run status, transfer estimates, file counts, warnings, and the full rsync log link directly on the run detail page. Keep raw rsync output and JSON available, but make the common review path easier to scan before starting a real backup.
This commit is contained in:
@@ -415,19 +415,29 @@ def run_detail(request, run_id: int):
|
||||
prune_result = result.get("prune") if isinstance(result.get("prune"), dict) else {}
|
||||
rsync_log_path = _run_rsync_log_path(run)
|
||||
rsync_log_tail = _run_rsync_log_tail(rsync_result, rsync_log_path)
|
||||
requested = result.get("requested") if isinstance(result.get("requested"), dict) else {}
|
||||
context = {
|
||||
"run": run,
|
||||
"can_cancel": run.status in {BackupRun.Status.QUEUED, BackupRun.Status.RUNNING},
|
||||
"requested": result.get("requested") if isinstance(result.get("requested"), dict) else {},
|
||||
"requested": requested,
|
||||
"stats": run_stats if isinstance(run_stats, dict) else {},
|
||||
"rsync": rsync_result,
|
||||
"rsync_command": _run_rsync_command(rsync_result),
|
||||
"failure": failure,
|
||||
"failure_summary": failure.get("message") or failure.get("summary") or "",
|
||||
"prune_result": prune_result,
|
||||
"has_prune_result": bool(prune_result),
|
||||
"rsync_log_path": str(rsync_log_path) if rsync_log_path is not None else "",
|
||||
"rsync_log_exists": bool(rsync_log_path and rsync_log_path.exists()),
|
||||
"rsync_log_tail": rsync_log_tail,
|
||||
"dry_run_summary": _dry_run_summary(
|
||||
result=result,
|
||||
requested=requested,
|
||||
stats=run_stats if isinstance(run_stats, dict) else {},
|
||||
failure=failure,
|
||||
rsync_log_tail=rsync_log_tail,
|
||||
rsync_log_exists=bool(rsync_log_path and rsync_log_path.exists()),
|
||||
),
|
||||
"result_json": _pretty_json(run.result),
|
||||
}
|
||||
return render(request, "pobsync_backend/run_detail.html", context)
|
||||
@@ -739,6 +749,48 @@ def _run_rsync_log_tail(rsync_result: dict, log_path: Path | None, *, max_lines:
|
||||
return []
|
||||
|
||||
|
||||
def _dry_run_summary(
|
||||
*,
|
||||
result: dict,
|
||||
requested: dict,
|
||||
stats: dict,
|
||||
failure: dict,
|
||||
rsync_log_tail: list[str],
|
||||
rsync_log_exists: bool,
|
||||
) -> dict[str, object]:
|
||||
if not (result.get("dry_run") or requested.get("dry_run")):
|
||||
return {}
|
||||
|
||||
rsync_stats = stats.get("rsync") if isinstance(stats.get("rsync"), dict) else {}
|
||||
warnings = []
|
||||
if failure:
|
||||
message = failure.get("message") or failure.get("summary")
|
||||
hint = failure.get("hint")
|
||||
if message:
|
||||
warnings.append(str(message))
|
||||
if hint:
|
||||
warnings.append(str(hint))
|
||||
for line in rsync_log_tail:
|
||||
lowered = line.lower()
|
||||
if "warning" in lowered or "permission denied" in lowered or "failed" in lowered:
|
||||
warnings.append(line)
|
||||
|
||||
return {
|
||||
"ok": result.get("ok"),
|
||||
"status": "passed" if result.get("ok") else ("failed" if result.get("ok") is False else "running"),
|
||||
"highlight_class": "success" if result.get("ok") else ("failed" if result.get("ok") is False else "warning"),
|
||||
"files_seen": rsync_stats.get("files_total"),
|
||||
"files_would_transfer": rsync_stats.get("files_transferred"),
|
||||
"total_file_size_bytes": rsync_stats.get("total_file_size_bytes"),
|
||||
"transfer_estimate_bytes": rsync_stats.get("total_transferred_file_size_bytes")
|
||||
or rsync_stats.get("literal_data_bytes"),
|
||||
"link_dest_estimated_savings_bytes": rsync_stats.get("link_dest_estimated_savings_bytes"),
|
||||
"duration_seconds": stats.get("duration_seconds"),
|
||||
"log_available": rsync_log_exists,
|
||||
"warnings": list(dict.fromkeys(warnings)),
|
||||
}
|
||||
|
||||
|
||||
def _log_context(request) -> dict[str, object]:
|
||||
units = ("pobsync-web.service", "pobsync-worker.service", "pobsync-scheduler.service")
|
||||
priorities = {
|
||||
|
||||
Reference in New Issue
Block a user