syncova-backup/apps/web/src/features/dashboard/DashboardPage.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

125 lines
4.4 KiB
TypeScript

/**
* Uebersicht der Anlage.
*
* Der Plan (§14) nennt zehn Kennzahlen. Sieben haben eine Datengrundlage, drei
* nicht — und die drei erscheinen trotzdem, mit der Angabe, was fehlt. Ein
* weggelassenes Feld sieht aus wie ein vergessenes; ein mit einer Null
* gefuelltes waere eine erfundene Statistik (PROMPT.md §139).
*/
import { useCallback } from 'react';
import { useApiResource } from '../../api/useApiResource';
import { ErrorState, LoadingState } from '../../components/PageState';
import { fetchDashboard } from './dashboardApi';
import type { Dashboard, DashboardWidget } from './dashboardApi';
/** Zeigt die Uebersicht. */
export function DashboardPage(): React.JSX.Element {
const loadDashboard = useCallback((abortSignal: AbortSignal) => fetchDashboard(abortSignal), []);
const { loadState, data, loadError, reload } = useApiResource<Dashboard>(loadDashboard);
return (
<section className="page">
<header className="page__header">
<h1 className="page__title">Uebersicht</h1>
{data !== null && (
<span className="page__meta">
{data.available_count} von {data.widgets.length} Kennzahlen haben eine Datengrundlage
</span>
)}
</header>
{loadState === 'loading' && <LoadingState what="Die Kennzahlen" />}
{loadState === 'failed' && loadError !== null && (
<ErrorState error={loadError} onRetry={reload} />
)}
{loadState === 'loaded' && data !== null && (
<div className="widget-grid">
{data.widgets.map((dashboardWidget) => (
<WidgetCard key={dashboardWidget.key} widget={dashboardWidget} />
))}
</div>
)}
</section>
);
}
/** Eigenschaften einer Kennzahlkachel. */
interface WidgetCardProperties {
/** Die dargestellte Kennzahl. */
readonly widget: DashboardWidget;
}
/** Zeigt eine einzelne Kennzahl. */
function WidgetCard({ widget }: WidgetCardProperties): React.JSX.Element {
if (!widget.available) {
return (
<article className="widget widget--unavailable">
<h2 className="widget__title">{widget.title}</h2>
<p className="widget__unavailable">noch nicht verfuegbar</p>
<p className="widget__detail">{widget.unavailable_reason}</p>
</article>
);
}
return (
<article className={`widget widget--${widget.severity}`}>
<h2 className="widget__title">{widget.title}</h2>
<p className="widget__value">
{widget.value === undefined ? (
// Kein Wert heisst nicht null. Eine Kennzahl, die sich nicht bilden
// laesst, sagt das — der Grund steht im Detailtext darunter.
<span className="widget__value-missing">nicht bezifferbar</span>
) : (
<>
{formatWidgetValue(widget.value)}
{widget.unit !== undefined && <span className="widget__unit"> {widget.unit}</span>}
</>
)}
</p>
{widget.detail !== undefined && <p className="widget__detail">{widget.detail}</p>}
{widget.breakdown !== undefined && (
<dl className="widget__breakdown">
{Object.entries(widget.breakdown).map(([breakdownKey, breakdownValue]) => (
<div className="widget__breakdown-item" key={breakdownKey}>
<dt>{formatBreakdownLabel(breakdownKey)}</dt>
<dd>{breakdownValue.toLocaleString('de-DE')}</dd>
</div>
))}
</dl>
)}
</article>
);
}
/** Formatiert die Hauptzahl einer Kennzahl. */
function formatWidgetValue(widgetValue: number): string {
// Ein kleiner Wert wird nicht auf null gerundet: „0 %" belegter Speicher liest
// sich wie „nichts abgelegt", obwohl Daten da sind. Dieselbe Ueberlegung wie
// bei der Recovery Assurance — eine Zahl darf nicht mehr behaupten, als sie
// weiss, und auch nicht weniger.
if (widgetValue > 0 && widgetValue < 0.1) {
return '< 0,1';
}
// Ganze Zahlen ohne Nachkommastellen, gebrochene mit einer: „99,7 %" sagt
// mehr als „100 %", wenn drei von tausend Laeufen scheiterten.
if (Number.isInteger(widgetValue)) {
return widgetValue.toLocaleString('de-DE');
}
return widgetValue.toLocaleString('de-DE', { maximumFractionDigits: 1 });
}
/** Macht aus einem Schluessel der Aufschluesselung eine Beschriftung. */
function formatBreakdownLabel(breakdownKey: string): string {
const readableLabel = breakdownKey.replace(/_/g, ' ');
return readableLabel.charAt(0).toUpperCase() + readableLabel.slice(1);
}