from __future__ import annotations from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_POST from pobsync.errors import ConfigError from .forms import ScheduleConfigForm from .models import BackupRun, HostConfig, ScheduleConfig, SnapshotRecord from .retention import run_sql_retention_plan from .snapshot_discovery import discover_snapshots @staff_member_required def dashboard(request): host_qs = ( HostConfig.objects.annotate(snapshot_count=Count("snapshots", distinct=True), run_count=Count("runs", distinct=True)) .order_by("host") ) context = { "hosts": host_qs, "latest_runs": BackupRun.objects.select_related("host", "snapshot").order_by("-created_at")[:10], "counts": { "hosts": HostConfig.objects.count(), "enabled_hosts": HostConfig.objects.filter(enabled=True).count(), "schedules": ScheduleConfig.objects.count(), "enabled_schedules": ScheduleConfig.objects.filter(enabled=True).count(), "snapshots": SnapshotRecord.objects.count(), "runs": BackupRun.objects.count(), "running_runs": BackupRun.objects.filter(status=BackupRun.Status.RUNNING).count(), "failed_runs": BackupRun.objects.filter(status=BackupRun.Status.FAILED).count(), }, } return render(request, "pobsync_backend/dashboard.html", context) @staff_member_required def host_detail(request, host: str): host_config = get_object_or_404(HostConfig, host=host) context = { "host": host_config, "schedule": _schedule_for_host(host_config), "latest_runs": host_config.runs.select_related("snapshot").order_by("-created_at")[:10], "snapshots": host_config.snapshots.select_related("base").order_by("-started_at", "dirname")[:20], "counts": { "snapshots": host_config.snapshots.count(), "runs": host_config.runs.count(), "failed_runs": host_config.runs.filter(status=BackupRun.Status.FAILED).count(), "incomplete_snapshots": host_config.snapshots.filter(kind=SnapshotRecord.Kind.INCOMPLETE).count(), }, } return render(request, "pobsync_backend/host_detail.html", context) @staff_member_required @require_POST def discover_host_snapshots(request, host: str): host_config = get_object_or_404(HostConfig, host=host) try: result = discover_snapshots(host=host_config) except Exception as exc: messages.error(request, f"Snapshot discovery failed for {host_config.host}: {exc}") else: messages.success( request, ( f"Snapshot discovery scanned {result['scanned']} items for {host_config.host}: " f"{result['created']} created, {result['updated']} updated." ), ) return redirect("host_detail", host=host_config.host) @staff_member_required def host_retention_plan(request, host: str): host_config = get_object_or_404(HostConfig, host=host) kind = request.GET.get("kind", "scheduled") if kind not in {"scheduled", "manual", "all"}: messages.error(request, "Retention kind must be scheduled, manual, or all.") return redirect("host_detail", host=host_config.host) protect_bases = request.GET.get("protect_bases") in {"1", "true", "on", "yes"} try: plan = run_sql_retention_plan(host=host_config.host, kind=kind, protect_bases=protect_bases) except ConfigError as exc: messages.error(request, str(exc)) return redirect("host_detail", host=host_config.host) context = { "host": host_config, "kind": kind, "protect_bases": protect_bases, "plan": plan, } return render(request, "pobsync_backend/retention_plan.html", context) @staff_member_required def edit_host_schedule(request, host: str): host_config = get_object_or_404(HostConfig, host=host) schedule = _schedule_for_host(host_config) if request.method == "POST": form = ScheduleConfigForm(request.POST, instance=schedule) if form.is_valid(): saved_schedule = form.save(commit=False) saved_schedule.host = host_config saved_schedule.save() messages.success(request, f"Schedule saved for {host_config.host}.") return redirect("host_detail", host=host_config.host) else: form = ScheduleConfigForm(instance=schedule, initial=_default_schedule_initial()) return render( request, "pobsync_backend/schedule_form.html", { "host": host_config, "schedule": schedule, "form": form, }, ) def _schedule_for_host(host_config: HostConfig) -> ScheduleConfig | None: try: return host_config.schedule except ScheduleConfig.DoesNotExist: return None def _default_schedule_initial() -> dict[str, object]: return { "cron_expr": "15 2 * * *", "user": "root", "enabled": True, "prune_max_delete": 10, }