/** * Wiederherstellungspunkte. * * Die zentrale Auskunft der Anlage. Sie beantwortet nicht „welche Backups gibt * es", sondern „auf welche kann ich mich verlassen" — deshalb steht die * Einstufung in jeder Zeile und nicht in einem Detailfenster. */ import { useCallback, useState } from 'react'; import { useApiResource } from '../../api/useApiResource'; import { EmptyState, ErrorState, LoadingState } from '../../components/PageState'; import { fetchRecoveryPoints } from './dashboardApi'; import type { RecoveryPoint } from './dashboardApi'; /** Auswahlmoeglichkeiten des Einstufungsfilters. */ const CLASSIFICATION_OPTIONS: readonly { readonly value: string; readonly label: string }[] = [ { value: '', label: 'Alle Einstufungen' }, { value: 'recoverable', label: 'Nachweislich wiederherstellbar' }, { value: 'verified', label: 'Geprueft' }, { value: 'successful', label: 'Ungeprueft' }, { value: 'corrupted', label: 'Beschaedigt' }, ]; /** Zeigt die Wiederherstellungspunkte. */ export function RecoveryPointsPage(): React.JSX.Element { const [classificationFilter, setClassificationFilter] = useState(''); const [includeDeleted, setIncludeDeleted] = useState(false); const [onlyProtected, setOnlyProtected] = useState(false); // Der Schluessel bildet die Filter ab: Aendert er sich, wird neu geladen. const filterKey = `${classificationFilter}|${String(includeDeleted)}|${String(onlyProtected)}`; const loadRecoveryPoints = useCallback( (abortSignal: AbortSignal) => fetchRecoveryPoints({ classification: classificationFilter, includeDeleted, onlyProtected }, abortSignal), [classificationFilter, includeDeleted, onlyProtected], ); const { loadState, data, loadError, reload } = useApiResource( loadRecoveryPoints, filterKey, ); return (

Wiederherstellungspunkte

{data !== null && {data.length} Punkte}
{/* Geloeschte Punkte erscheinen nur auf Nachfrage — die Frage „warum ist das Backup von vorletzter Woche weg?" muss aber beantwortbar sein. */}
{loadState === 'loading' && } {loadState === 'failed' && loadError !== null && ( )} {loadState === 'loaded' && data !== null && data.length === 0 && ( )} {loadState === 'loaded' && data !== null && data.length > 0 && (
{data.map((recoveryPoint) => ( ))}
Zeitpunkt Auftrag Art Groesse Einstufung Bewertung Schutz
)}
); } /** Eigenschaften einer Tabellenzeile. */ interface RecoveryPointRowProperties { /** Der dargestellte Punkt. */ readonly recoveryPoint: RecoveryPoint; } /** Zeigt einen Wiederherstellungspunkt als Tabellenzeile. */ function RecoveryPointRow({ recoveryPoint }: RecoveryPointRowProperties): React.JSX.Element { const isDeleted = recoveryPoint.deleted_at !== undefined; return ( {formatTimestamp(recoveryPoint.completed_at)} {recoveryPoint.repository_name} {recoveryPoint.job_name ?? '—'} {recoveryPoint.backup_type === 'full' ? 'Voll' : 'Zusatz'} {formatBytes(recoveryPoint.logical_bytes)} {/* Keine Bewertung heisst nicht null Prozent. Die Unterscheidung ist der Kern der Recovery Assurance (Phase 10). */} {recoveryPoint.assurance_score === undefined ? ( nicht berechnet ) : ( `${recoveryPoint.assurance_score} %` )} {isDeleted ? ( geloescht ) : recoveryPoint.legal_hold ? ( Legal Hold ) : recoveryPoint.is_protected ? ( geschuetzt ) : ( frei )} ); } /** Eigenschaften der Einstufungsanzeige. */ interface ClassificationBadgeProperties { /** Die Einstufung; fehlt bei ungeprueften Punkten. */ readonly classification: string | undefined; } /** Zeigt die Einstufung eines Punktes. */ function ClassificationBadge({ classification }: ClassificationBadgeProperties): React.JSX.Element { switch (classification) { case 'recoverable': return wiederherstellbar; case 'verified': return geprueft; case 'corrupted': return beschaedigt; case 'failed': return gescheitert; default: // „Ungeprueft" ist eine Aussage, keine Luecke: Der Punkt wurde nie // zurueckgeschrieben, und das soll man sehen. return ungeprueft; } } /** Schreibt einen Zeitstempel in deutscher Schreibweise. */ function formatTimestamp(isoTimestamp: string | undefined): string { if (isoTimestamp === undefined) { return '—'; } return new Date(isoTimestamp).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short', }); } /** Faktor zwischen zwei Groesseneinheiten. */ const BYTE_UNIT_STEP = 1024; /** Schreibt eine Datenmenge lesbar. */ function formatBytes(byteCount: number): string { if (byteCount < BYTE_UNIT_STEP) { return `${byteCount} B`; } const unitNames = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let remainingValue = byteCount; let chosenUnit = unitNames[0]; for (const unitName of unitNames) { remainingValue /= BYTE_UNIT_STEP; chosenUnit = unitName; if (remainingValue < BYTE_UNIT_STEP) { break; } } return `${remainingValue.toLocaleString('de-DE', { maximumFractionDigits: 1 })} ${chosenUnit}`; }