syncova-backup/apps/web/src/api/useApiResource.ts
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

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 };
}