feat: expand backup to all data and add automatic scheduled backups
Backend Image / build-and-push (push) Successful in 2m16s
Frontend Image / build-and-push (push) Successful in 1m56s

Backup export/import now covers Role, User, RolePermission,
Notification, AuditLog and Document (with actual file content), not
just business data - matching "wirklich alle Daten". Role/User use
natural keys (role.key, resource+action) so a bundle restores cleanly
into a different database.

Adds BackupSchedulerService (@nestjs/schedule) for cron-based automatic
backups to a local path (BACKUP_STORAGE_PATH), with retention pruning
and a file-mtime guard against duplicate runs across replicas. New
GET /backup/schedule and POST /backup/schedule/run-now endpoints, plus
a status card and manual trigger in the frontend Backup settings page.
This commit is contained in:
Nick Szaunig committed 2026-09-03 11:33:48 +02:00
1 parent d998f611e2
commit 7b23862a41
13 files changed
+853 -68

No files matched your search

@@ -2,9 +2,9 @@
import { ChangeEvent, useState } from "react";
import { Database } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { exportBackup, importBackup } from "@/lib/backup/api";
import { exportBackup, getBackupSchedule, importBackup, runBackupNow } from "@/lib/backup/api";
import { importCsv } from "@/lib/import/api";
import { ImportEntity, ImportReport, ImportRowStatus } from "@/lib/import/types";
import { Badge } from "@/components/ui/badge";
@@ -76,9 +76,9 @@ function BackupSection() {
</CardHeader>
<CardContent className="flex flex-col gap-4 text-sm">
<p className="text-muted-foreground">
Sichert Bienenstände bis Etikettenvorlagen (Betriebsdaten) in einer JSON-Datei -
für Backups und zur Übertragung auf ein anderes Deployment. Benutzer, Rollen,
Audit-Log und hochgeladene Dokumente sind nicht enthalten.
Sichert wirklich alle Daten - Bienenstände, Völker, Ernten und mehr, dazu Benutzer,
Rollen &amp; Rechte, Audit-Log und hochgeladene Dokumente (inkl. Dateiinhalt) - in
einer JSON-Datei. Für Backups und zur Übertragung auf ein anderes Deployment.
</p>
<Button
type="button"
@@ -113,6 +113,74 @@ function BackupSection() {
);
}
function formatDateTime(value: string | null): string {
if (!value) {
return "Noch nicht gelaufen";
}
return new Date(value).toLocaleString("de-DE");
}
function ScheduleSection() {
const queryClient = useQueryClient();
const scheduleQuery = useQuery({
queryKey: ["backup-schedule"],
queryFn: getBackupSchedule,
});
const runNowMutation = useMutation({
mutationFn: runBackupNow,
onSuccess: () => {
toast.success("Backup wurde erstellt");
queryClient.invalidateQueries({ queryKey: ["backup-schedule"] });
},
onError: () => toast.error("Backup konnte nicht erstellt werden"),
});
const status = scheduleQuery.data;
return (
<Card className="max-w-lg">
<CardHeader>
<CardTitle>Automatische Backups</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 text-sm">
{scheduleQuery.isLoading ? (
<p className="text-muted-foreground">Lädt…</p>
) : !status?.enabled ? (
<p className="text-muted-foreground">
Automatische Backups sind nicht konfiguriert (BACKUP_STORAGE_PATH fehlt).
Manueller Export/Import bleibt weiterhin möglich.
</p>
) : (
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5">
<dt className="text-muted-foreground">Status</dt>
<dd>
<Badge variant="default">Aktiv</Badge>
</dd>
<dt className="text-muted-foreground">Pfad</dt>
<dd className="break-all">{status.path}</dd>
<dt className="text-muted-foreground">Zeitplan</dt>
<dd>{status.cronSchedule}</dd>
<dt className="text-muted-foreground">Aufbewahrung</dt>
<dd>{status.retentionCount} neueste Backups</dd>
<dt className="text-muted-foreground">Letzter Lauf</dt>
<dd>{formatDateTime(status.lastRunAt)}</dd>
</dl>
)}
<Button
type="button"
variant="outline"
disabled={!status?.enabled || runNowMutation.isPending}
onClick={() => runNowMutation.mutate()}
>
{runNowMutation.isPending ? "Wird gesichert…" : "Jetzt sichern"}
</Button>
</CardContent>
</Card>
);
}
const ENTITY_LABELS: Record<ImportEntity, string> = {
apiaries: "Bienenstände",
colonies: "Völker",
@@ -271,6 +339,7 @@ export default function BackupPage() {
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<BackupSection />
<ScheduleSection />
<CsvImportSection />
</div>
</main>
+12 -2
View File
@@ -1,5 +1,5 @@
import { apiFetchBlob, apiFetchFormData, BlobDownload } from "@/lib/auth/api-client";
import { BackupImportSummary } from "./types";
import { apiFetch, apiFetchBlob, apiFetchFormData, BlobDownload } from "@/lib/auth/api-client";
import { BackupImportSummary, BackupScheduleStatus } from "./types";
export function exportBackup(): Promise<BlobDownload> {
return apiFetchBlob("/backup/export", "apicore-backup.json");
@@ -10,3 +10,13 @@ export function importBackup(file: File): Promise<BackupImportSummary> {
formData.append("file", file);
return apiFetchFormData<BackupImportSummary>("/backup/import", formData);
}
export function getBackupSchedule(): Promise<BackupScheduleStatus> {
return apiFetch<BackupScheduleStatus>("/backup/schedule");
}
export function runBackupNow(): Promise<{ filePath: string }> {
return apiFetch<{ filePath: string }>("/backup/schedule/run-now", {
method: "POST",
});
}
+8
View File
@@ -1,3 +1,11 @@
export interface BackupImportSummary {
imported: Record<string, number>;
}
export interface BackupScheduleStatus {
enabled: boolean;
path: string | null;
cronSchedule: string | null;
retentionCount: number | null;
lastRunAt: string | null;
}