(feature) Add run completion notifications

Add email and webhook notification targets with delivery tracking, and send
notifications when backup runs reach a terminal status.

Expose notification target management in the Django UI and keep delivery
failures recorded without failing the backup worker.
This commit is contained in:
2026-05-28 21:20:38 +02:00
parent 1f5c4e0756
commit 67ffd6101b
14 changed files with 819 additions and 4 deletions

View File

@@ -32,13 +32,24 @@ from .forms import (
HostConfigForm,
IncompleteCleanupForm,
ManualBackupForm,
NotificationTargetForm,
RetentionApplyForm,
SshCredentialGenerateForm,
ScheduleConfigForm,
SshCredentialForm,
)
from .host_ops import ensure_host_directories
from .models import BackupRun, GlobalConfig, HostConfig, PurgedSnapshot, ScheduleConfig, SnapshotRecord, SshCredential
from .models import (
BackupRun,
GlobalConfig,
HostConfig,
NotificationDelivery,
NotificationTarget,
PurgedSnapshot,
ScheduleConfig,
SnapshotRecord,
SshCredential,
)
from .preflight import collect_backup_gate, effective_host_config_preview, run_remote_preflight
from .retention import run_incomplete_cleanup, run_sql_retention_apply, run_sql_retention_plan
from .self_check import collect_self_checks, summarize_self_checks
@@ -320,6 +331,65 @@ def logs(request):
return render(request, "pobsync_backend/logs.html", context)
@staff_member_required
def notification_targets(request):
targets = NotificationTarget.objects.order_by("name")
deliveries = NotificationDelivery.objects.select_related("target", "run", "run__host").order_by("-created_at")[:12]
return render(
request,
"pobsync_backend/notification_targets.html",
{
"targets": targets,
"deliveries": deliveries,
},
)
@staff_member_required
def create_notification_target(request):
if request.method == "POST":
form = NotificationTargetForm(request.POST)
if form.is_valid():
target = form.save()
messages.success(request, f"Notification target {target.name} created.")
return redirect("notification_targets")
else:
form = NotificationTargetForm()
return render(
request,
"pobsync_backend/notification_target_form.html",
{
"form": form,
"target": None,
"title": "New notification target",
"submit_label": "Create target",
},
)
@staff_member_required
def edit_notification_target(request, target_id: int):
target = get_object_or_404(NotificationTarget, id=target_id)
if request.method == "POST":
form = NotificationTargetForm(request.POST, instance=target)
if form.is_valid():
target = form.save()
messages.success(request, f"Notification target {target.name} updated.")
return redirect("notification_targets")
else:
form = NotificationTargetForm(instance=target)
return render(
request,
"pobsync_backend/notification_target_form.html",
{
"form": form,
"target": target,
"title": f"Edit notification target: {target.name}",
"submit_label": "Save target",
},
)
@staff_member_required
def runs_list(request):
status = request.GET.get("status", "").strip()