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>
125 lines
4.4 KiB
TypeScript
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);
|
|
}
|