/** * 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/`). 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(null); const { currentPageId, navigateToPage } = useCurrentPage(); // Das gerade geoeffnete Objekt innerhalb einer Seite, etwa ein Auftrag. const [openObjectIdentifier, setOpenObjectIdentifier] = useState(null); if (authenticatedUser === null) { return ( ); } async function handleLogout(): Promise { 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 ( void handleLogout()} > ); } /** 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 ; } // 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 ( ); } if (!pageDefinition.available) { return ( ); } switch (pageDefinition.id) { case 'dashboard': return ; case 'jobs': return openObjectIdentifier ? ( onOpenObject(null)} /> ) : ( ); case 'recovery-points': return openObjectIdentifier ? ( onOpenObject(null)} onDeleted={() => onOpenObject(null)} /> ) : ( ); case 'restores': return ; case 'verification': return openObjectIdentifier ? ( onOpenObject(null)} onDeleted={() => onOpenObject(null)} /> ) : ( ); case 'repositories': return ; case 'retention': return ; case 'agents': return ; case 'proxmox': return ; case 'protected-systems': return ; case 'events': return ; case 'alerts': return ; case 'metrics': return ; case 'reports': return ; case 'security': return ; case 'users': return ; case 'roles': return ; case 'settings': return ( ); 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 ( ); } }