**Sitzung.** Die Tokens lagen nur im Arbeitsspeicher — jedes Neuladen warf den
Betreiber auf die Anmeldemaske. Das war die sicherste Variante und praktisch
unbrauchbar; mitten in einer Stoerung ist es kein Sicherheitsgewinn, sondern ein
Hindernis. Jetzt `sessionStorage` (nicht `localStorage`: stirbt mit dem Tab),
begrenzt durch zwei Uhren:
- **Harte Obergrenze** von 30 Minuten ab Anmeldung, durch keine Interaktion
verschiebbar. Sonst waere "30 Minuten" keine Zusage.
- **Untaetigkeitsgrenze** von 30 Minuten.
- Der sofortige serverseitige Widerruf bleibt die eigentliche Absicherung — die
Tokens sind opak, kein JWT, und genau dafuer wurden sie gewaehlt.
Dazu die Sitzungsuhr oben rechts neben "Abmelden", unter fuenf Minuten
auffaellig. Umgesetzt mit `useSyncExternalStore`: Die Restzeit haengt an der Uhr
und am Speicher, also an zwei Dingen ausserhalb von React. Sie beim Rendern
auszurechnen waere ein unreiner Aufruf, sie in einem Effekt zu setzen eine
zweite Renderrunde je Sekunde — beides hat der Linter gemeldet.
Sechs Tests halten die Grenzen fest, zwei davon durch Mutation als fangend
bestaetigt (Obergrenze mitverschieben schlaegt fehl).
**Fehlergrenze.** Ein Fehler in einer Komponente riss bisher den gesamten Baum
ab; uebrig blieb eine leere Seite — im dunklen Thema ein schwarzer Bildschirm
ohne jeden Hinweis. Die Grenze sitzt **um den Inhalt**: Menue und Kopfzeile
bleiben stehen, der Fehlertext ist lesbar und kopierbar.
**Umlaute.** Die Oberflaeche schrieb durchgehend ae/oe/ue/ss. Jetzt aeoeuess.
Dabei ein selbst verursachter Schaden, gefunden und behoben: Eine Regel
"ue → ü" ist falsch, weil die Buchstabenfolge nicht immer ein Umlaut ist. Sie
machte aus "Quelle" ein "Qülle", aus "neue" ein "neü", aus "aktuell" ein
"aktüll", aus "Dauer" ein "Daür". Die Abbildung laeuft jetzt ueber eine
gepruefte Wortliste mit Ausschluss englischer Bezeichner (`value`, `message`,
`session`, `queued`, `true`); die 23 zerstoerten Woerter sind einzeln
zurueckgesetzt. Ein alter Tippfehler ("geprueter") ist dabei mit aufgefallen.
82 Tests gruen, tsc sauber, eslint ohne Warnung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
6.4 KiB
TypeScript
268 lines
6.4 KiB
TypeScript
/**
|
|
* Seitenverzeichnis der Oberfläche.
|
|
*
|
|
* Der Grundsatz aus PROMPT.md §139 gilt unverändert: Ein Menü, das nur die
|
|
* fertigen Bereiche zeigt, verschweigt den Ausbaustand; eines, dessen Einträge
|
|
* in leere Masken führen, täuscht ihn vor. Jeder Eintrag erscheint, und ein
|
|
* noch nicht verfügbarer nennt, was fehlt und was stattdessen gilt.
|
|
*
|
|
* Die Gliederung folgt dem Weg durch die Anlage, nicht der Reihenfolge der
|
|
* Umsetzung: Was täglich beobachtet wird, steht oben; was einmal eingerichtet
|
|
* und dann selten angefasst wird, unten.
|
|
*/
|
|
|
|
import type { LucideIcon } from 'lucide-react';
|
|
import {
|
|
Activity,
|
|
Archive,
|
|
BadgeCheck,
|
|
Bell,
|
|
ChartLine,
|
|
Clock,
|
|
Database,
|
|
FileText,
|
|
HardDrive,
|
|
KeyRound,
|
|
LayoutDashboard,
|
|
MonitorCog,
|
|
RotateCcw,
|
|
ScrollText,
|
|
Server,
|
|
Settings,
|
|
ShieldCheck,
|
|
Users,
|
|
} from 'lucide-react';
|
|
|
|
/** Bereich der Navigation, dem eine Seite zugeordnet ist. */
|
|
export type NavigationSection =
|
|
| 'betrieb'
|
|
| 'daten'
|
|
| 'infrastruktur'
|
|
| 'analyse'
|
|
| 'verwaltung';
|
|
|
|
/** Beschriftung der Bereiche in Anzeigereihenfolge. */
|
|
export const SECTION_LABELS: ReadonlyArray<readonly [NavigationSection, string]> = [
|
|
['betrieb', 'Betrieb'],
|
|
['daten', 'Daten'],
|
|
['infrastruktur', 'Infrastruktur'],
|
|
['analyse', 'Analyse'],
|
|
['verwaltung', 'Verwaltung'],
|
|
];
|
|
|
|
/** Beschreibung einer Seite der Oberfläche. */
|
|
export interface PageDefinition {
|
|
/** Stabiler Bezeichner; er steht auch im Pfad. */
|
|
readonly id: string;
|
|
/** Beschriftung im Menü. */
|
|
readonly label: string;
|
|
/** Bereich, unter dem die Seite erscheint. */
|
|
readonly section: NavigationSection;
|
|
/** Sinnbild im Menü. */
|
|
readonly icon: LucideIcon;
|
|
/**
|
|
* Berechtigung, die der Zugriff verlangt.
|
|
*
|
|
* Rein zur Anzeige: Die Prüfung erfolgt immer serverseitig (PROMPT.md §42).
|
|
* Ein Menüpunkt, den der Server ohnehin ablehnt, führt sonst nur zu einer
|
|
* Fehlermeldung statt zu einer verständlichen Oberfläche.
|
|
*/
|
|
readonly requiredPermission: string | null;
|
|
/** Meldet, ob hinter der Seite ein Backend steht. */
|
|
readonly available: boolean;
|
|
/**
|
|
* Erklärt eine fehlende Seite.
|
|
*
|
|
* Nicht „in Arbeit", sondern: welche Funktion fehlt und wie man heute an die
|
|
* Auskunft kommt.
|
|
*/
|
|
readonly unavailableReason?: string;
|
|
}
|
|
|
|
/** Alle Seiten in Anzeigereihenfolge. */
|
|
export const ALL_PAGES: readonly PageDefinition[] = [
|
|
// --- Betrieb: was täglich beobachtet und bedient wird ---
|
|
{
|
|
id: 'dashboard',
|
|
label: 'Übersicht',
|
|
section: 'betrieb',
|
|
icon: LayoutDashboard,
|
|
requiredPermission: 'backups.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'jobs',
|
|
label: 'Sicherungsaufträge',
|
|
section: 'betrieb',
|
|
icon: Clock,
|
|
requiredPermission: 'jobs.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'restores',
|
|
label: 'Wiederherstellung',
|
|
section: 'betrieb',
|
|
icon: RotateCcw,
|
|
requiredPermission: 'restores.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'alerts',
|
|
label: 'Meldungen',
|
|
section: 'betrieb',
|
|
icon: Bell,
|
|
requiredPermission: 'alerts.read',
|
|
available: true,
|
|
},
|
|
|
|
// --- Daten: die Sicherungen selbst und ihr Schutz ---
|
|
{
|
|
id: 'recovery-points',
|
|
label: 'Wiederherstellungspunkte',
|
|
section: 'daten',
|
|
icon: Archive,
|
|
requiredPermission: 'backups.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'verification',
|
|
label: 'Prüfung',
|
|
section: 'daten',
|
|
icon: BadgeCheck,
|
|
requiredPermission: 'verification.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'repositories',
|
|
label: 'Repositories',
|
|
section: 'daten',
|
|
icon: Database,
|
|
requiredPermission: 'repositories.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'retention',
|
|
label: 'Aufbewahrung',
|
|
section: 'daten',
|
|
icon: HardDrive,
|
|
requiredPermission: 'repositories.read',
|
|
available: true,
|
|
},
|
|
|
|
// --- Infrastruktur: was gesichert wird ---
|
|
{
|
|
id: 'agents',
|
|
label: 'Agenten',
|
|
section: 'infrastruktur',
|
|
icon: MonitorCog,
|
|
requiredPermission: 'agents.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'proxmox',
|
|
label: 'Proxmox',
|
|
section: 'infrastruktur',
|
|
icon: Server,
|
|
requiredPermission: 'providers.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'protected-systems',
|
|
label: 'Geschützte Systeme',
|
|
section: 'infrastruktur',
|
|
icon: Activity,
|
|
requiredPermission: 'providers.read',
|
|
available: true,
|
|
},
|
|
|
|
// --- Analyse: Auswertung und Nachweis ---
|
|
{
|
|
id: 'metrics',
|
|
label: 'Kennzahlen',
|
|
section: 'analyse',
|
|
icon: ChartLine,
|
|
requiredPermission: 'monitoring.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'reports',
|
|
label: 'Berichte',
|
|
section: 'analyse',
|
|
icon: FileText,
|
|
requiredPermission: 'reports.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'security',
|
|
label: 'Security Center',
|
|
section: 'analyse',
|
|
icon: ShieldCheck,
|
|
requiredPermission: 'security.read',
|
|
available: true,
|
|
},
|
|
|
|
// --- Verwaltung: einmal einrichten, selten anfassen ---
|
|
{
|
|
id: 'users',
|
|
label: 'Benutzer',
|
|
section: 'verwaltung',
|
|
icon: Users,
|
|
requiredPermission: 'users.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'roles',
|
|
label: 'Rollen',
|
|
section: 'verwaltung',
|
|
icon: KeyRound,
|
|
requiredPermission: 'roles.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'events',
|
|
label: 'Ereignisprotokoll',
|
|
section: 'verwaltung',
|
|
icon: ScrollText,
|
|
requiredPermission: 'audit.read',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Einstellungen',
|
|
section: 'verwaltung',
|
|
icon: Settings,
|
|
requiredPermission: 'settings.read',
|
|
available: true,
|
|
},
|
|
];
|
|
|
|
/** Die Seite, die nach der Anmeldung erscheint. */
|
|
export const DEFAULT_PAGE_ID = 'dashboard';
|
|
|
|
/** Sucht eine Seite anhand ihres Bezeichners. */
|
|
export function findPage(pageIdentifier: string): PageDefinition | undefined {
|
|
return ALL_PAGES.find((page) => page.id === pageIdentifier);
|
|
}
|
|
|
|
/**
|
|
* Meldet, ob ein Benutzer eine Seite sehen darf.
|
|
*
|
|
* Ausschließlich zur Anzeige — sie verhindert Sackgassen im Menü. Die
|
|
* verbindliche Prüfung findet auf dem Server statt (PROMPT.md §42).
|
|
*/
|
|
export function mayViewPage(
|
|
page: PageDefinition,
|
|
grantedPermissions: readonly string[],
|
|
): boolean {
|
|
if (page.requiredPermission === null) {
|
|
return true;
|
|
}
|
|
|
|
// Der Super-Administrator trägt einen Platzhalter statt aller Einzelrechte.
|
|
if (grantedPermissions.includes('*')) {
|
|
return true;
|
|
}
|
|
|
|
return grantedPermissions.includes(page.requiredPermission);
|
|
}
|