(ui) Surface run debugging details in Django
Restructure the run detail page into clearer sections for summary, failure classification, requested options, rsync command, rsync log output, stats, retention, and raw result data. Show recent rsync log output inline with a link to the full log, and promote failure and retention warning details out of the JSON payload so failed or slow runs are easier to debug from the control panel.
This commit is contained in:
@@ -362,15 +362,26 @@ def queue_manual_backup(request, host: str):
|
||||
@staff_member_required
|
||||
def run_detail(request, run_id: int):
|
||||
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
|
||||
run_stats = run.result.get("stats") if isinstance(run.result, dict) else {}
|
||||
result = run.result if isinstance(run.result, dict) else {}
|
||||
run_stats = result.get("stats") if isinstance(result.get("stats"), dict) else {}
|
||||
rsync_result = result.get("rsync") if isinstance(result.get("rsync"), dict) else {}
|
||||
failure = result.get("failure") if isinstance(result.get("failure"), dict) else {}
|
||||
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)
|
||||
context = {
|
||||
"run": run,
|
||||
"can_cancel": run.status in {BackupRun.Status.QUEUED, BackupRun.Status.RUNNING},
|
||||
"requested": run.result.get("requested") if isinstance(run.result, dict) else {},
|
||||
"requested": result.get("requested") if isinstance(result.get("requested"), dict) else {},
|
||||
"stats": run_stats if isinstance(run_stats, dict) else {},
|
||||
"rsync": rsync_result,
|
||||
"rsync_command": _run_rsync_command(rsync_result),
|
||||
"failure": failure,
|
||||
"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,
|
||||
"result_json": _pretty_json(run.result),
|
||||
}
|
||||
return render(request, "pobsync_backend/run_detail.html", context)
|
||||
@@ -662,6 +673,26 @@ def _run_rsync_log_path(run: BackupRun) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def _run_rsync_command(rsync_result: dict) -> list[str]:
|
||||
command = rsync_result.get("command")
|
||||
if not isinstance(command, list):
|
||||
return []
|
||||
return [str(part) for part in command]
|
||||
|
||||
|
||||
def _run_rsync_log_tail(rsync_result: dict, log_path: Path | None, *, max_lines: int = 30) -> list[str]:
|
||||
log_tail = rsync_result.get("log_tail")
|
||||
if isinstance(log_tail, list):
|
||||
return [str(line) for line in log_tail[-max_lines:]]
|
||||
if log_path is None or not log_path.is_file():
|
||||
return []
|
||||
try:
|
||||
with log_path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
return handle.read().splitlines()[-max_lines:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
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