Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
/**
|
|
* Allgemeiner Lade-Hook fuer API-Ressourcen.
|
|
*
|
|
* Er folgt demselben Muster wie useSystemHealth: Solange kein echtes Ergebnis
|
|
* vorliegt, bleibt der Zustand ausdruecklich „laedt" oder „Fehler" — niemals ein
|
|
* leeres Ergebnis, das sich von einem echten leeren nicht unterscheiden liesse
|
|
* (PROMPT.md §139).
|
|
*
|
|
* Der Hook ersetzt die Wiederholung derselben dreissig Zeilen in jeder Seite.
|
|
* Genau deshalb steht er hier und nicht in einer der Seiten: Ein zweiter Ort mit
|
|
* eigener Fehlerbehandlung waere ein zweiter Ort, an dem sie fehlen kann.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { ApiError } from './client';
|
|
|
|
/** Ladezustand einer Ressource. */
|
|
export type ResourceLoadState = 'loading' | 'loaded' | 'failed';
|
|
|
|
/** Ergebnis des Lade-Hooks. */
|
|
export interface UseApiResourceResult<TPayload> {
|
|
/** Aktueller Ladezustand. */
|
|
readonly loadState: ResourceLoadState;
|
|
/** Geladene Daten; null, solange keine vorliegen. */
|
|
readonly data: TPayload | null;
|
|
/** Aufgetretener Fehler; null, wenn keiner vorliegt. */
|
|
readonly loadError: ApiError | null;
|
|
/** Laedt die Ressource erneut. */
|
|
readonly reload: () => void;
|
|
}
|
|
|
|
/**
|
|
* Laedt eine Ressource und haelt ihren Zustand.
|
|
*
|
|
* @param loadResource Ladefunktion; sie erhaelt ein Abbruchsignal.
|
|
* @param dependencyKey Aendert sich dieser Wert, wird neu geladen. Ein einzelner
|
|
* Schluessel statt eines Abhaengigkeitsarrays: Ein Array mit wechselnder Laenge
|
|
* ist in React ein Fehler, und ein Objekt als Abhaengigkeit laedt bei jedem
|
|
* Rendern neu.
|
|
*/
|
|
export function useApiResource<TPayload>(
|
|
loadResource: (abortSignal: AbortSignal) => Promise<TPayload>,
|
|
dependencyKey = '',
|
|
): UseApiResourceResult<TPayload> {
|
|
// reloadCounter erzwingt einen erneuten Lauf des Effekts bei manuellem Neuladen.
|
|
const [reloadCounter, setReloadCounter] = useState(0);
|
|
|
|
// Das Ergebnis traegt den Schluessel, unter dem es entstanden ist. Daraus
|
|
// laesst sich der Ladezustand **ableiten**, statt ihn im Effekt zu setzen:
|
|
// Passt der Schluessel nicht zum aktuellen, laeuft die Anfrage noch. Ein
|
|
// setState im Effektkoerper loeste dagegen eine zweite Renderrunde aus,
|
|
// bevor ueberhaupt etwas geladen wurde.
|
|
const [loadResult, setLoadResult] = useState<{
|
|
key: string;
|
|
data: TPayload | null;
|
|
error: ApiError | null;
|
|
} | null>(null);
|
|
|
|
const effectiveKey = `${dependencyKey}#${String(reloadCounter)}`;
|
|
|
|
const reload = useCallback(() => {
|
|
setReloadCounter((previousCounter) => previousCounter + 1);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const abortController = new AbortController();
|
|
|
|
async function loadFromApi(): Promise<void> {
|
|
try {
|
|
const loadedPayload = await loadResource(abortController.signal);
|
|
|
|
setLoadResult({ key: effectiveKey, data: loadedPayload, error: null });
|
|
} catch (caughtError) {
|
|
// Ein Abbruch ist kein Fehler, sondern Folge des Aufraeumens.
|
|
if (caughtError instanceof DOMException && caughtError.name === 'AbortError') {
|
|
return;
|
|
}
|
|
|
|
setLoadResult({
|
|
key: effectiveKey,
|
|
data: null,
|
|
error:
|
|
caughtError instanceof ApiError
|
|
? caughtError
|
|
: new ApiError({
|
|
code: 'UNEXPECTED_ERROR',
|
|
message: 'Die Anfrage ist unerwartet fehlgeschlagen.',
|
|
statusCode: 0,
|
|
requestId: '',
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
|
|
void loadFromApi();
|
|
|
|
return () => abortController.abort();
|
|
// loadResource bewusst nicht in den Abhaengigkeiten: Eine bei jedem Rendern
|
|
// neu gebildete Funktion loeste sonst eine Endlosschleife aus. Der
|
|
// effectiveKey steuert das Neuladen ausdruecklich.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [effectiveKey]);
|
|
|
|
// Solange kein Ergebnis zum aktuellen Schluessel vorliegt, wird geladen. Die
|
|
// vorherigen Daten bleiben dabei sichtbar — ein Filterwechsel laesst die
|
|
// Tabelle also nicht aufblitzen.
|
|
if (loadResult === null || loadResult.key !== effectiveKey) {
|
|
return {
|
|
loadState: 'loading',
|
|
data: loadResult?.data ?? null,
|
|
loadError: null,
|
|
reload,
|
|
};
|
|
}
|
|
|
|
if (loadResult.error !== null) {
|
|
return { loadState: 'failed', data: null, loadError: loadResult.error, reload };
|
|
}
|
|
|
|
return { loadState: 'loaded', data: loadResult.data, loadError: null, reload };
|
|
}
|