"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AlertTriangle, Archive, Download, RefreshCcw, RotateCcw, ShieldAlert, Trash2 } from "lucide-react"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import SummaryCard from "@/components/common/SummaryCard"; import { useToast } from "@/components/common/ToastProvider"; import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { api } from "@/lib/api"; import { hasPermission } from "@/lib/permissions"; import type { ApiSuccess } from "@/types/audit"; import type { BackupListResponse, BackupSummary, BackupValidationResponse } from "@/types/backup"; import type { CurrentUser } from "@/types/rbac"; const RESTORE_CONFIRM_TEXT = "ICH VERSTEHE DAS RISIKO"; function getErrorMessage(error: unknown, fallback: string) { if (typeof error === "object" && error !== null && "response" in error) { const response = (error as { response?: { data?: { detail?: string; message?: string } } }).response; return response?.data?.detail ?? response?.data?.message ?? fallback; } return fallback; } function formatBytes(bytes: number) { if (bytes < 1024) { return `${bytes} B`; } const units = ["KB", "MB", "GB", "TB"]; let value = bytes / 1024; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`; } function formatDate(value: string | null) { if (!value) { return "Unbekannt"; } return new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short", }).format(new Date(value)); } export default function BackupsPage() { const { showToast } = useToast(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [error, setError] = useState(""); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [validationByFile, setValidationByFile] = useState>({}); const [validatingFile, setValidatingFile] = useState(""); const [restoreTarget, setRestoreTarget] = useState(null); const [restoreConfirm, setRestoreConfirm] = useState(""); const [restoring, setRestoring] = useState(false); const [currentUser, setCurrentUser] = useState(null); const loadBackups = useCallback(async () => { setError(""); try { const response = await api.get>("/backups"); setData(response.data.data); } catch (err) { setError(getErrorMessage(err, "Backups konnten nicht geladen werden.")); } finally { setLoading(false); } }, []); useEffect(() => { queueMicrotask(() => { void loadBackups(); }); }, [loadBackups]); useEffect(() => { queueMicrotask(async () => { try { const response = await api.get("/me"); setCurrentUser(response.data); } catch { setCurrentUser(null); } }); }, []); const cards = useMemo(() => { if (!data) { return []; } return [ { label: "Backups", value: data.total_count }, { label: "Gesamtgroesse", value: formatBytes(data.total_size_bytes) }, { label: "Letztes Backup", value: data.latest_backup_at ? formatDate(data.latest_backup_at) : "Noch keines" }, ]; }, [data]); const canCreate = hasPermission(currentUser, "backup.create"); const canDownload = hasPermission(currentUser, "backup.download"); const canDelete = hasPermission(currentUser, "backup.delete"); const canRestore = hasPermission(currentUser, "backup.restore"); async function createBackup() { setCreating(true); try { await api.post>("/backups/create"); await loadBackups(); showToast({ type: "success", title: "Backup erstellt" }); } catch (err) { showToast({ type: "error", title: "Backup konnte nicht erstellt werden", description: getErrorMessage(err, "Bitte pruefe die Hermes- und Datenbank-Konfiguration."), }); } finally { setCreating(false); } } async function validateBackup(filename: string) { setValidatingFile(filename); try { const response = await api.post>(`/backups/${encodeURIComponent(filename)}/validate`); setValidationByFile((current) => ({ ...current, [filename]: response.data.data })); showToast({ type: response.data.data.valid ? "success" : "error", title: response.data.data.valid ? "Backup ist gueltig" : "Backup-Pruefung fehlgeschlagen", description: response.data.data.valid ? response.data.data.message : response.data.data.issues.join(" | "), }); } catch (err) { showToast({ type: "error", title: "Backup konnte nicht validiert werden", description: getErrorMessage(err, "Bitte pruefe die Backup-Datei."), }); } finally { setValidatingFile(""); } } async function deleteBackup() { if (!deleteTarget) { return; } setDeleting(true); try { await api.delete(`/backups/${encodeURIComponent(deleteTarget.filename)}/delete`); setDeleteTarget(null); await loadBackups(); showToast({ type: "success", title: "Backup geloescht" }); } catch (err) { showToast({ type: "error", title: "Backup konnte nicht geloescht werden", description: getErrorMessage(err, "Bitte versuche es erneut."), }); } finally { setDeleting(false); } } async function restoreBackup() { if (!restoreTarget) { return; } setRestoring(true); try { await api.post(`/backups/${encodeURIComponent(restoreTarget.filename)}/restore`, { confirm_text: restoreConfirm, }); showToast({ type: "success", title: "Restore abgeschlossen" }); } catch (err) { showToast({ type: "info", title: "CLI-Restore erforderlich", description: getErrorMessage(err, "Automatischer Restore ist derzeit deaktiviert."), }); } finally { setRestoring(false); } } if (loading) { return
Backups werden geladen...
; } if (error || !data) { return
{error || "Keine Backups verfuegbar"}
; } return (

Backups

PostgreSQL-Dumps, Storage-Dateien und Restore-Validierung zentral verwalten.

{canCreate && ( )}
{cards.map((card) => ( ))}

Verfuegbare Backup-Dateien

{data.items.length === 0 ? (
Noch keine Backup-Dateien vorhanden.
) : (
{data.items.map((backup) => { const validation = validationByFile[backup.filename]; return (

{backup.filename}

{backup.validation_status === "valid" ? "Manifest lesbar" : "Pruefung empfohlen"}

Erstellt

{formatDate(backup.created_at)}

Groesse

{formatBytes(backup.size_bytes)}

Version

{backup.app_version || "Unbekannt"}

Ersteller

{backup.created_by_username || "System"}

Datenbank

{backup.database_name || "Unbekannt"}

Dateien

{backup.file_count}

Storage-Pfad

{backup.storage_base_path || "-"}

{validation && (
{validation.valid ? : }

{validation.message}

Checksumme: {validation.checksum_valid ? "gueltig" : "ungueltig"}

{!validation.valid && validation.issues.length > 0 && (

{validation.issues.join(" | ")}

)} {validation.requires_cli_restore && (

Restore ist vorbereitet, muss aktuell aber ueber das CLI-Script ausgefuehrt werden.

)}
)}
{canDownload && ( Download )} {canRestore && ( <> )} {canDelete && ( )}
); })}
)}
{ if (!open) { setDeleteTarget(null); } }} onConfirm={() => void deleteBackup()} > {deleteTarget && (

{deleteTarget.filename}

{formatBytes(deleteTarget.size_bytes)}

)}
{ if (!open) { setRestoreTarget(null); setRestoreConfirm(""); } }} onConfirm={() => void restoreBackup()} >

Automatischer Restore ist aus Sicherheitsgruenden deaktiviert. Die Validierung und Auditierung laufen trotzdem ueber Olympus.

Bestaetigungstext

setRestoreConfirm(event.target.value)} placeholder={RESTORE_CONFIRM_TEXT} />
{restoreTarget && (

Ziel-Backup: {restoreTarget.filename}

)}
); }