syncova-backup/apps/web/src/features/metrics/MetricsPage.tsx
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

140 lines
4.9 KiB
TypeScript

/**
* Kennzahlen und Diagramme.
*
* Alle zwoelf Diagramme aus dem Plan erscheinen. Das eine ohne Datengrundlage
* — die Ressourcenlast der Agenten — steht mit dabei und sagt, was fehlt.
* Dieselbe Regel wie in der Uebersicht: Ein weggelassenes Diagramm sieht aus wie
* ein vergessenes, ein leeres wie eine Anlage ohne Betrieb.
*/
import { useCallback, useState } from 'react';
import { useApiResource } from '../../api/useApiResource';
import { ErrorState, LoadingState } from '../../components/PageState';
import { LineChart } from './LineChart';
import { fetchChart, fetchChartCatalog } from './metricsApi';
import type { Chart, ChartCatalog, ChartDefinition } from './metricsApi';
/** Auswaehlbare Zeitraeume mit ihrer Beschriftung. */
const RANGE_OPTIONS: readonly { readonly value: string; readonly label: string }[] = [
{ value: '1h', label: 'Letzte Stunde' },
{ value: '24h', label: 'Letzte 24 Stunden' },
{ value: '7d', label: 'Letzte 7 Tage' },
{ value: '30d', label: 'Letzte 30 Tage' },
{ value: '90d', label: 'Letzte 90 Tage' },
{ value: '1y', label: 'Letztes Jahr' },
];
/** Zeigt die Kennzahlen. */
export function MetricsPage(): React.JSX.Element {
const [selectedRange, setSelectedRange] = useState('7d');
const loadCatalog = useCallback((abortSignal: AbortSignal) => fetchChartCatalog(abortSignal), []);
const { loadState, data, loadError, reload } = useApiResource<ChartCatalog>(loadCatalog);
return (
<section className="page">
<header className="page__header">
<h1 className="page__title">Kennzahlen</h1>
{data !== null && (
<span className="page__meta">
{data.available_count} von {data.charts.length} Diagrammen haben eine Datengrundlage
</span>
)}
</header>
<div className="filter-bar">
<label className="filter-bar__field">
<span className="filter-bar__label">Zeitraum</span>
<select
className="filter-bar__select"
value={selectedRange}
onChange={(changeEvent) => setSelectedRange(changeEvent.target.value)}
>
{RANGE_OPTIONS.map((rangeOption) => (
<option key={rangeOption.value} value={rangeOption.value}>
{rangeOption.label}
</option>
))}
</select>
</label>
</div>
{loadState === 'loading' && <LoadingState what="Die Kennzahlen" />}
{loadState === 'failed' && loadError !== null && (
<ErrorState error={loadError} onRetry={reload} />
)}
{loadState === 'loaded' && data !== null && (
<div className="chart-list">
{data.charts.map((chartDefinition) => (
<ChartCard
key={chartDefinition.metric}
definition={chartDefinition}
timeRange={selectedRange}
/>
))}
</div>
)}
</section>
);
}
/** Eigenschaften einer Diagrammkachel. */
interface ChartCardProperties {
/** Das darzustellende Diagramm. */
readonly definition: ChartDefinition;
/** Der gewaehlte Zeitraum. */
readonly timeRange: string;
}
/** Zeigt ein einzelnes Diagramm. */
function ChartCard({ definition, timeRange }: ChartCardProperties): React.JSX.Element {
const loadChart = useCallback(
(abortSignal: AbortSignal) => fetchChart(definition.metric, timeRange, abortSignal),
[definition.metric, timeRange],
);
// Ein Diagramm ohne Datengrundlage wird gar nicht erst abgerufen: Der Server
// antwortete mit 501, und ein Fehler in der Oberflaeche saehe aus wie eine
// Stoerung. Es ist keine — die Funktion gibt es nur noch nicht.
const { loadState, data, loadError, reload } = useApiResource<Chart>(
loadChart,
definition.available ? `${definition.metric}|${timeRange}` : 'unavailable',
);
if (!definition.available) {
return (
<article className="chart-card chart-card--unavailable">
<h2 className="chart-card__title">{definition.title}</h2>
<p className="chart-card__badge">noch nicht verfuegbar</p>
<p className="chart-card__description">{definition.unavailable_reason}</p>
</article>
);
}
return (
<article className="chart-card">
<h2 className="chart-card__title">{definition.title}</h2>
<p className="chart-card__description">{definition.description}</p>
{loadState === 'loading' && <LoadingState what="Die Reihe" />}
{loadState === 'failed' && loadError !== null && (
<ErrorState error={loadError} onRetry={reload} />
)}
{loadState === 'loaded' && data !== null && (
<>
<LineChart series={data.series} unit={definition.unit} />
{/* Der Hinweis ordnet ein, was die Kurve wert ist: Zwei Punkte sehen
aus wie ein Trend und sind keiner. */}
{data.note !== undefined && data.note !== '' && (
<p className="chart-card__note">{data.note}</p>
)}
</>
)}
</article>
);
}