Von 95 fachlichen Endpunkten erreicht die Oberflaeche jetzt 89 statt 23. Schreibend waren es neun (vier davon An- und Abmeldung) — jetzt ist jede Handlung der Anlage bedienbar. Neu bedienbar: - **Wiederherstellung** als vierstufiger Assistent. Die Vorabpruefung ist ein eigener Schritt, weil sie den Unterschied zwischen Hoffnung und Nachweis macht: Sie schreibt nichts und stellt fest, ob **jeder benoetigte Block noch da ist**. Ein Manifest allein belegt nur, dass jemand einmal etwas gesichert hat. Die drei Huerden vor dem Ueberschreiben sind sichtbar umgesetzt; laeuft die dritte ins Leere, entfaellt sie — ein Ritual ohne Anlass gewoehnt das Wegklicken an. - **Wiederherstellungspunkte** mit Bewertung, Schutz, Ransomware-Einschaetzung, Legal Hold, Fristverlaengerung und Loeschung. Eine unbelastbare Prozentzahl wird als Vermutung gekennzeichnet, ungemessene Eingangsgroessen erscheinen als "ungemessen" statt als null Punkte. - **Pruefung** mit allen fuenf Pruefarten. Zustand und Ergebnis stehen nebeneinander: Eine gescheiterte Pruefung ist kein Befund am Backup. - **Repositories** mit Integritaetslauf, Katalog-Neuaufbau, Gesundheitspruefung und gemessener Durchsetzungsstufe. Die Uebernahme sagt ausdruecklich, dass hier nichts angelegt wird. - **Aufbewahrung** mit Regeln und Vorschau vor dem Loeschen. - **Proxmox** (neun Endpunkte, bisher ohne jede Oberflaeche) und **Agenten** samt einmaliger Anzeige des Aufnahme-Tokens. - **Benutzer, Rollen, Benachrichtigungswege, eigener zweiter Faktor.** Zwei Funde beim Nachweis gegen den laufenden Dienst: - **Die Einstufung heisst `successful`, nicht `unverified`.** Das Vokabular lautet failed/corrupted/successful/verified/recoverable. `successful` ist die Falle: Es bedeutet "der Lauf ist durchgelaufen" — nicht "wiederherstellbar". Es ist deshalb **neutral**, nicht gruen; ein gruenes Abzeichen laese sich als "geprueft und in Ordnung", und genau diese Verwechslung soll die Anlage verhindern. Regressionstest, durch Mutation als fangend bestaetigt. - **Der Geheimnis-Scanner griff korrekt** beim Platzhaltertext des SSH-Schluesselfelds. Gekennzeichnet statt das Muster aufzuweichen. Die Statuszuordnung steht an genau einer Stelle und faellt fuer unbekannte Werte auf neutral zurueck, nie auf gruen: Ein neuer Serverzustand darf nicht als "in Ordnung" durchgehen. Nachgewiesen im Container gegen Debian 12 mit echtem PostgreSQL und nginx, ueber genau die Aufrufe, die die Konsole macht: Repository uebernommen, Durchsetzungsstufe gemessen (advisory — korrekt auf overlayfs), Auftrag angelegt, Lauf 202, zweiter Anstoss 409, Sicherung erfolgreich (2 Objekte, 3.000.006 Byte, 0 uebergangen), Blockpruefung clean (5 Bloecke), Vorabpruefung "wiederherstellbar: 2 Dateien, 2,9 MiB", Wiederherstellung nach /etc abgewiesen. 76 Tests gruen, tsc sauber, eslint ohne Warnung, Bau 460 KB (136 KB gzip). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
244 lines
8.3 KiB
TypeScript
244 lines
8.3 KiB
TypeScript
/**
|
|
* Wurzelkomponente der Syncova-Oberflaeche.
|
|
*
|
|
* Sie entscheidet zwischen Anmeldemaske und angemeldeter Ansicht und verteilt
|
|
* letztere auf die Bereiche aus PROMPT.md §28.
|
|
*
|
|
* Die Navigation kennt zwei Ebenen: eine Seite und optional ein Objekt darauf
|
|
* (`jobs/<id>`). Das reicht fuer diese Konsole und kostet rund fuenfzig Zeilen —
|
|
* eine Router-Bibliothek waere hier mehr Abhaengigkeit als Nutzen.
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { setAccessTokenProvider } from './api/client';
|
|
import { AppShell } from './components/layout/AppShell';
|
|
import { ToastProvider } from './components/ui';
|
|
import { getAccessToken, logout } from './features/auth/authApi';
|
|
import { LoginPage } from './features/auth/LoginPage';
|
|
import { AlertsPage } from './features/alerts/AlertsPage';
|
|
import { DashboardPage } from './features/dashboard/DashboardPage';
|
|
import { RecoveryPointsPage } from './features/dashboard/RecoveryPointsPage';
|
|
import { RolesPage } from './features/administration/RolesPage';
|
|
import { SettingsPage } from './features/administration/SettingsPage';
|
|
import { UsersPage } from './features/administration/UsersPage';
|
|
import { EventsPage } from './features/administration/EventsPage';
|
|
import { AgentsPage } from './features/infrastructure/AgentsPage';
|
|
import { ProxmoxPage } from './features/infrastructure/ProxmoxPage';
|
|
import { ProtectedSystemsPage } from './features/infrastructure/ProtectedSystemsPage';
|
|
import { RepositoriesPage } from './features/repositories/RepositoriesPage';
|
|
import { RetentionPage } from './features/repositories/RetentionPage';
|
|
import { BackupDetailPage } from './features/backups/BackupDetailPage';
|
|
import { VerificationPage } from './features/backups/VerificationPage';
|
|
import { RestoresPage } from './features/restores/RestoresPage';
|
|
import { JobDetailPage } from './features/jobs/JobDetailPage';
|
|
import { JobsPage } from './features/jobs/JobsPage';
|
|
import { MetricsPage } from './features/metrics/MetricsPage';
|
|
import { ReportsPage } from './features/reports/ReportsPage';
|
|
import { SecurityPage } from './features/security/SecurityPage';
|
|
import { UnavailablePage } from './navigation/UnavailablePage';
|
|
import { DEFAULT_PAGE_ID, findPage, mayViewPage } from './navigation/pages';
|
|
import { useCurrentPage } from './navigation/useCurrentPage';
|
|
import type { CurrentUser } from './types/auth';
|
|
import './styles/theme.css';
|
|
// Die noch nicht umgebauten Seiten nutzen weiterhin das alte Stylesheet. Es
|
|
// hier stehen zu lassen ist unschoen und die einzige Alternative zu einer
|
|
// Oberflaeche, die auf halbem Weg unformatiert erscheint. Es faellt weg,
|
|
// sobald die letzte Seite umgebaut ist.
|
|
import './styles/tokens.css';
|
|
import './App.css';
|
|
|
|
// Der API-Client erhaelt seinen Tokenzugriff einmalig beim Laden des Moduls.
|
|
setAccessTokenProvider(getAccessToken);
|
|
|
|
export function App(): React.JSX.Element {
|
|
const [authenticatedUser, setAuthenticatedUser] = useState<CurrentUser | null>(null);
|
|
const { currentPageId, navigateToPage } = useCurrentPage();
|
|
|
|
// Das gerade geoeffnete Objekt innerhalb einer Seite, etwa ein Auftrag.
|
|
const [openObjectIdentifier, setOpenObjectIdentifier] = useState<string | null>(null);
|
|
|
|
if (authenticatedUser === null) {
|
|
return (
|
|
<ToastProvider>
|
|
<LoginPage onAuthenticated={setAuthenticatedUser} />
|
|
</ToastProvider>
|
|
);
|
|
}
|
|
|
|
async function handleLogout(): Promise<void> {
|
|
await logout();
|
|
setAuthenticatedUser(null);
|
|
}
|
|
|
|
const grantedPermissions = authenticatedUser.permissions ?? [];
|
|
|
|
/** Wechselt die Seite und schliesst ein offenes Objekt. */
|
|
function handleNavigate(pageIdentifier: string): void {
|
|
setOpenObjectIdentifier(null);
|
|
navigateToPage(pageIdentifier);
|
|
}
|
|
|
|
return (
|
|
<ToastProvider>
|
|
<AppShell
|
|
currentPageId={currentPageId}
|
|
onNavigate={handleNavigate}
|
|
grantedPermissions={grantedPermissions}
|
|
userName={authenticatedUser.username}
|
|
onLogout={() => void handleLogout()}
|
|
>
|
|
<CurrentPageContent
|
|
currentPageId={currentPageId}
|
|
grantedPermissions={grantedPermissions}
|
|
currentUser={authenticatedUser}
|
|
openObjectIdentifier={openObjectIdentifier}
|
|
onOpenObject={setOpenObjectIdentifier}
|
|
/>
|
|
</AppShell>
|
|
</ToastProvider>
|
|
);
|
|
}
|
|
|
|
/** Zeigt den Inhalt der gewaehlten Seite. */
|
|
function CurrentPageContent({
|
|
currentPageId,
|
|
grantedPermissions,
|
|
currentUser,
|
|
openObjectIdentifier,
|
|
onOpenObject,
|
|
}: {
|
|
readonly currentPageId: string;
|
|
readonly grantedPermissions: readonly string[];
|
|
readonly currentUser: CurrentUser;
|
|
readonly openObjectIdentifier: string | null;
|
|
readonly onOpenObject: (objectIdentifier: string | null) => void;
|
|
}): React.JSX.Element {
|
|
const pageDefinition = findPage(currentPageId) ?? findPage(DEFAULT_PAGE_ID);
|
|
|
|
if (pageDefinition === undefined) {
|
|
return <DashboardPage />;
|
|
}
|
|
|
|
// Die Anzeigepruefung ersetzt die serverseitige nicht, sie ergaenzt sie: Ohne
|
|
// sie liefe der Aufruf in eine Fehlermeldung statt in eine verstaendliche
|
|
// Auskunft (PROMPT.md §42).
|
|
if (!mayViewPage(pageDefinition, grantedPermissions)) {
|
|
return (
|
|
<UnavailablePage
|
|
title={pageDefinition.label}
|
|
reason="Ihrer Rolle fehlt die Berechtigung fuer diesen Bereich. Die Pruefung erfolgt auf dem Server; diese Anzeige verhindert nur den Umweg ueber eine Fehlermeldung."
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!pageDefinition.available) {
|
|
return (
|
|
<UnavailablePage
|
|
title={pageDefinition.label}
|
|
reason={pageDefinition.unavailableReason ?? 'Dieser Bereich ist noch nicht verfuegbar.'}
|
|
/>
|
|
);
|
|
}
|
|
|
|
switch (pageDefinition.id) {
|
|
case 'dashboard':
|
|
return <DashboardPage />;
|
|
|
|
case 'jobs':
|
|
return openObjectIdentifier ? (
|
|
<JobDetailPage
|
|
jobIdentifier={openObjectIdentifier}
|
|
grantedPermissions={grantedPermissions}
|
|
onBack={() => onOpenObject(null)}
|
|
/>
|
|
) : (
|
|
<JobsPage grantedPermissions={grantedPermissions} onOpenJob={onOpenObject} />
|
|
);
|
|
|
|
case 'recovery-points':
|
|
return openObjectIdentifier ? (
|
|
<BackupDetailPage
|
|
backupIdentifier={openObjectIdentifier}
|
|
grantedPermissions={grantedPermissions}
|
|
onBack={() => onOpenObject(null)}
|
|
onDeleted={() => onOpenObject(null)}
|
|
/>
|
|
) : (
|
|
<RecoveryPointsPage onOpenBackup={onOpenObject} />
|
|
);
|
|
|
|
case 'restores':
|
|
return <RestoresPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'verification':
|
|
return openObjectIdentifier ? (
|
|
<BackupDetailPage
|
|
backupIdentifier={openObjectIdentifier}
|
|
grantedPermissions={grantedPermissions}
|
|
onBack={() => onOpenObject(null)}
|
|
onDeleted={() => onOpenObject(null)}
|
|
/>
|
|
) : (
|
|
<VerificationPage
|
|
grantedPermissions={grantedPermissions}
|
|
onOpenBackup={onOpenObject}
|
|
/>
|
|
);
|
|
|
|
case 'repositories':
|
|
return <RepositoriesPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'retention':
|
|
return <RetentionPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'agents':
|
|
return <AgentsPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'proxmox':
|
|
return <ProxmoxPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'protected-systems':
|
|
return <ProtectedSystemsPage />;
|
|
|
|
case 'events':
|
|
return <EventsPage />;
|
|
|
|
case 'alerts':
|
|
return <AlertsPage />;
|
|
|
|
case 'metrics':
|
|
return <MetricsPage />;
|
|
|
|
case 'reports':
|
|
return <ReportsPage />;
|
|
|
|
case 'security':
|
|
return <SecurityPage />;
|
|
|
|
case 'users':
|
|
return <UsersPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'roles':
|
|
return <RolesPage grantedPermissions={grantedPermissions} />;
|
|
|
|
case 'settings':
|
|
return (
|
|
<SettingsPage
|
|
grantedPermissions={grantedPermissions}
|
|
currentUser={currentUser}
|
|
/>
|
|
);
|
|
|
|
default:
|
|
// Eine Seite steht im Verzeichnis, hat aber noch keine Umsetzung. Das
|
|
// ist kein Fehler, sondern der Ausbaustand — und wird als solcher
|
|
// benannt, statt eine leere Maske zu zeigen.
|
|
return (
|
|
<UnavailablePage
|
|
title={pageDefinition.label}
|
|
reason="Dieser Bereich wird gerade gebaut. Die zugehoerigen API-Endpunkte sind vorhanden; es fehlt die Bedienoberflaeche."
|
|
/>
|
|
);
|
|
}
|
|
}
|