/** * Prüfungen — Übersicht. * * Die wichtigste Unterscheidung dieser Seite steht in zwei Spalten * nebeneinander und wird gern verwechselt: * * - **Zustand** (`status`): Ist die Prüfung durchgelaufen? * - **Ergebnis** (`result`): Was hat sie gefunden? * * Eine gescheiterte Prüfung ist **kein Befund am Backup**. Repository nicht * erreichbar heißt: Auftrag `failed`, Einstufung unberührt. Wer beides * vermengt, baut ein Prüfwerkzeug, das grundlos Alarm schlägt — und das wird * bald nicht mehr ernst genommen. */ import { Ban, RefreshCw } from 'lucide-react'; import { useCallback } from 'react'; import { useApiResource } from '@/api/useApiResource'; import { describeApiError, useMutation } from '@/api/useMutation'; import { PageHeader } from '@/components/layout/PageHeader'; import { Button, Callout, Card, DataTable, ErrorState, StatusBadge, useToast, type TableColumn, } from '@/components/ui'; import { formatBytes, formatDuration, formatRelativeTime } from '@/lib/utils'; import { VERIFICATION_TYPES, cancelVerification, listVerifications, type VerificationJob, } from './backupsApi'; export function VerificationPage({ grantedPermissions, onOpenBackup, }: { readonly grantedPermissions: readonly string[]; readonly onOpenBackup: (backupIdentifier: string) => void; }) { const toast = useToast(); const verificationsResource = useApiResource( useCallback((abortSignal) => listVerifications(abortSignal), []), ); const mayWrite = grantedPermissions.includes('*') || grantedPermissions.includes('verification.write'); const cancelMutation = useMutation(cancelVerification, { onSuccess: () => { toast.showSuccess('Prüfung abgebrochen'); verificationsResource.reload(); }, onError: (apiError) => toast.showError('Der Abbruch schlug fehl', describeApiError(apiError), apiError.requestId), }); const jobs = verificationsResource.data ?? []; const tableColumns: ReadonlyArray> = [ { key: 'type', header: 'Prüfart', render: (job) => (

{VERIFICATION_TYPES.find((type) => type.value === job.verification_type)?.label ?? job.verification_type}

{job.verification_type === 'restore_test' ? (

Der einzige echte Nachweis

) : null}
), }, { key: 'status', header: 'Zustand', render: (job) => , }, { key: 'result', header: 'Ergebnis', render: (job) => job.result ? ( ) : ( // Kein Ergebnis heißt: Die Prüfung lief nicht durch. Das ist etwas // anderes als "ohne Befund" und darf nicht danach aussehen. — ), }, { key: 'chunks', header: 'Blöcke', numeric: true, render: (job) => ( {job.chunks_checked.toLocaleString('de-DE')} {job.chunks_missing > 0 || job.chunks_corrupted > 0 ? ( ({job.chunks_missing} fehlend, {job.chunks_corrupted} beschädigt) ) : null} ), }, { key: 'bytes_read', header: 'Gelesen', numeric: true, render: (job) => formatBytes(job.bytes_read), }, { key: 'duration', header: 'Dauer', numeric: true, render: (job) => formatDuration(job.duration_seconds), }, { key: 'started', header: 'Begonnen', render: (job) => ( {formatRelativeTime(job.started_at ?? job.created_at)} ), }, { key: 'actions', header: '', className: 'w-px', render: (job) => mayWrite && (job.status === 'running' || job.status === 'queued') ? ( ) : null, }, ]; if (verificationsResource.loadState === 'failed' && verificationsResource.loadError) { return ( <> ); } return ( <> Aktualisieren } /> Eine gescheiterte Prüfung ist kein Befund am Backup. job.id} onRowClick={(job) => onOpenBackup(job.backup_id)} isLoading={verificationsResource.loadState === 'loading'} emptyTitle="Noch keine Prüfung" emptyDescription="Prüfungen starten Sie bei einem Wiederherstellungspunkt." /> ); }