feat: expand backup to all data and add automatic scheduled backups
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:
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 & 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>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user