(feature) Add cancellable backup runs and clearer dry-run logs

Add a cancel action for queued and running backup runs. Queued runs are
cancelled immediately, while running runs are marked for cancellation and
the worker terminates the active rsync process group.

Make dry-run log paths run-specific and add a defensive default dry-run
timeout so stuck dry-runs do not remain running indefinitely.

Remove rsync exit codes from run overview tables while keeping detailed
diagnostics available on the run detail payload.
This commit is contained in:
2026-05-19 20:46:10 +02:00
parent 088f43279e
commit bbb0f652f3
12 changed files with 291 additions and 47 deletions

View File

@@ -10,6 +10,7 @@ from django.contrib.admin.views.decorators import staff_member_required
from django.conf import settings
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_POST
from pobsync.errors import PobsyncError
@@ -348,12 +349,37 @@ def run_detail(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
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 {},
"result_json": _pretty_json(run.result),
}
return render(request, "pobsync_backend/run_detail.html", context)
@staff_member_required
@require_POST
def cancel_run(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host"), id=run_id)
if run.status not in {BackupRun.Status.QUEUED, BackupRun.Status.RUNNING}:
messages.warning(request, f"Run {run.id} is already {run.status}.")
return redirect("run_detail", run_id=run.id)
result = dict(run.result) if isinstance(run.result, dict) else {}
result["cancellation"] = {
"requested_at": timezone.now().isoformat(),
"previous_status": run.status,
}
update_fields = ["status", "result"]
run.status = BackupRun.Status.CANCELLED
run.result = result
if result["cancellation"]["previous_status"] == BackupRun.Status.QUEUED:
run.ended_at = timezone.now()
update_fields.append("ended_at")
run.save(update_fields=update_fields)
messages.success(request, f"Cancellation requested for run {run.id}.")
return redirect("run_detail", run_id=run.id)
@staff_member_required
def snapshot_detail(request, snapshot_id: int):
snapshot = get_object_or_404(