**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>
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
/**
|
|
* Statusanzeige — die wichtigste Komponente der Konsole.
|
|
*
|
|
* Sie bildet die Fachbegriffe der API auf die fünf Bedeutungen aus PROMPT.md
|
|
* §106 ab. Dass diese Abbildung **an einer einzigen Stelle** steht, ist der
|
|
* eigentliche Punkt: Wäre sie über die Seiten verteilt, würde früher oder
|
|
* später irgendwo "partial_failure" grün erscheinen — und ein Betreiber hält
|
|
* einen Teilfehler dann für einen Erfolg.
|
|
*
|
|
* Die Zuordnungen sind keine Geschmacksfrage, sondern folgen den
|
|
* Entscheidungen der Phasen:
|
|
*
|
|
* - "partial_failure" ist **niemals** grün (Entwicklungsregel 1).
|
|
* - "unverified" ist neutral, nicht grün: Ohne Wiederherstellungstest ist die
|
|
* Wiederherstellbarkeit unbekannt, und Unbekanntes zählt nie als gut
|
|
* (Phase 10).
|
|
* - "corrupted" ist kritisch, nicht nur eine Warnung: Ein beschädigtes Backup
|
|
* ist nicht zu 70 % wiederherstellbar, sondern gar nicht.
|
|
* - "advisory" beim Löschschutz ist eine Warnung: Der Schutz ist eine
|
|
* Software-Regel, kein Schutz des Dateisystems (Phase 11).
|
|
*/
|
|
|
|
import type { ReactNode } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/** Die fünf Bedeutungen. Mehr gibt es nicht. */
|
|
export type StatusTone = 'healthy' | 'warning' | 'high' | 'critical' | 'info' | 'neutral';
|
|
|
|
interface StatusDefinition {
|
|
readonly tone: StatusTone;
|
|
readonly label: string;
|
|
}
|
|
|
|
/**
|
|
* Abbildung der Fachbegriffe auf Ton und deutsche Beschriftung.
|
|
*
|
|
* Ein unbekannter Schlüssel wird bewusst **neutral** dargestellt und mit dem
|
|
* Rohwert beschriftet — nicht grün. Ein neuer Serverzustand, den diese Tabelle
|
|
* noch nicht kennt, darf nicht als "in Ordnung" durchgehen.
|
|
*/
|
|
const statusDefinitions: Readonly<Record<string, StatusDefinition>> = {
|
|
// --- Läufe ---
|
|
succeeded: { tone: 'healthy', label: 'Erfolgreich' },
|
|
running: { tone: 'info', label: 'Läuft' },
|
|
queued: { tone: 'info', label: 'Eingereiht' },
|
|
pending: { tone: 'neutral', label: 'Ausstehend' },
|
|
partial_failure: { tone: 'warning', label: 'Teilfehler' },
|
|
failed: { tone: 'critical', label: 'Fehlgeschlagen' },
|
|
cancelled: { tone: 'neutral', label: 'Abgebrochen' },
|
|
|
|
// --- Aufträge ---
|
|
enabled: { tone: 'healthy', label: 'Aktiv' },
|
|
paused: { tone: 'warning', label: 'Angehalten' },
|
|
disabled: { tone: 'neutral', label: 'Deaktiviert' },
|
|
|
|
// --- Einstufung der Wiederherstellungspunkte (Phase 10) ---
|
|
//
|
|
// Das Vokabular lautet: failed, successful, verified, recoverable, corrupted.
|
|
//
|
|
// `successful` ist die Falle dieser Tabelle und deshalb **neutral**, nicht
|
|
// grün: Es bedeutet "der Sicherungslauf ist durchgelaufen" — nicht, dass
|
|
// sich daraus etwas wiederherstellen lässt. Ein grünes Abzeichen läse sich
|
|
// hier als "geprüft und in Ordnung", und genau diese Verwechslung soll die
|
|
// ganze Anlage verhindern. Erst ein Wiederherstellungstest hebt auf
|
|
// `recoverable`; alles davor ist ein Indiz.
|
|
recoverable: { tone: 'healthy', label: 'Wiederherstellbar' },
|
|
verified: { tone: 'info', label: 'Geprüft' },
|
|
successful: { tone: 'neutral', label: 'Gesichert, ungeprüft' },
|
|
corrupted: { tone: 'critical', label: 'Beschädigt' },
|
|
|
|
// --- Prüfergebnisse ---
|
|
clean: { tone: 'healthy', label: 'Ohne Befund' },
|
|
findings: { tone: 'critical', label: 'Mit Befund' },
|
|
|
|
// --- Repositories ---
|
|
active: { tone: 'healthy', label: 'Aktiv' },
|
|
read_only: { tone: 'warning', label: 'Nur lesend' },
|
|
unavailable: { tone: 'critical', label: 'Nicht erreichbar' },
|
|
maintenance: { tone: 'warning', label: 'Wartung' },
|
|
|
|
// --- Durchsetzungsstufe des Löschschutzes (Phase 11) ---
|
|
filesystem: { tone: 'healthy', label: 'Dateisystem' },
|
|
storage: { tone: 'healthy', label: 'Speichersystem' },
|
|
advisory: { tone: 'warning', label: 'Nur Software' },
|
|
none: { tone: 'critical', label: 'Kein Schutz' },
|
|
|
|
// --- Gesundheit ---
|
|
healthy: { tone: 'healthy', label: 'Fehlerfrei' },
|
|
degraded: { tone: 'warning', label: 'Eingeschränkt' },
|
|
critical: { tone: 'critical', label: 'Kritisch' },
|
|
unknown: { tone: 'neutral', label: 'Unbekannt' },
|
|
|
|
// --- Meldungen ---
|
|
open: { tone: 'critical', label: 'Offen' },
|
|
acknowledged: { tone: 'warning', label: 'Bestätigt' },
|
|
resolved: { tone: 'healthy', label: 'Erledigt' },
|
|
suppressed: { tone: 'neutral', label: 'Unterdrückt' },
|
|
|
|
// --- Schweregrade ---
|
|
info: { tone: 'info', label: 'Information' },
|
|
low: { tone: 'info', label: 'Gering' },
|
|
medium: { tone: 'warning', label: 'Mittel' },
|
|
warning: { tone: 'warning', label: 'Warnung' },
|
|
high: { tone: 'high', label: 'Hoch' },
|
|
|
|
// --- Agenten ---
|
|
online: { tone: 'healthy', label: 'Verbunden' },
|
|
offline: { tone: 'critical', label: 'Nicht erreichbar' },
|
|
stale: { tone: 'warning', label: 'Veraltet' },
|
|
revoked: { tone: 'neutral', label: 'Gesperrt' },
|
|
|
|
// --- Ransomware-Einstufung (Phase 16) ---
|
|
elevated: { tone: 'warning', label: 'Auffällig' },
|
|
// "unknown" oben deckt den Fall unter fünf Vergleichsläufen mit ab.
|
|
};
|
|
|
|
const toneClasses: Readonly<Record<StatusTone, string>> = {
|
|
healthy: 'bg-healthy-bg text-healthy',
|
|
warning: 'bg-warning-bg text-warning',
|
|
high: 'bg-high-bg text-high',
|
|
critical: 'bg-critical-bg text-critical',
|
|
info: 'bg-info-bg text-info',
|
|
neutral: 'bg-neutral-bg text-neutral',
|
|
};
|
|
|
|
/** Liefert Ton und Beschriftung zu einem Fachbegriff. */
|
|
export function resolveStatus(rawStatus: string | null | undefined): StatusDefinition {
|
|
if (!rawStatus) {
|
|
return { tone: 'neutral', label: 'Unbekannt' };
|
|
}
|
|
|
|
const normalizedStatus = rawStatus.toLowerCase();
|
|
|
|
return (
|
|
statusDefinitions[normalizedStatus] ?? {
|
|
// Unbekannt bleibt neutral. Grün wäre hier der bequeme und gefährliche
|
|
// Standard: Ein neuer Fehlerzustand des Servers sähe aus wie Erfolg.
|
|
tone: 'neutral',
|
|
label: rawStatus,
|
|
}
|
|
);
|
|
}
|
|
|
|
export function StatusBadge({
|
|
status,
|
|
label,
|
|
className,
|
|
showDot = true,
|
|
}: {
|
|
readonly status: string | null | undefined;
|
|
/** Überschreibt die Beschriftung, der Ton bleibt. */
|
|
readonly label?: ReactNode | undefined;
|
|
readonly className?: string | undefined;
|
|
readonly showDot?: boolean | undefined;
|
|
}) {
|
|
const resolved = resolveStatus(status);
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium',
|
|
toneClasses[resolved.tone],
|
|
className,
|
|
)}
|
|
>
|
|
{showDot ? (
|
|
<span className="size-1.5 rounded-full bg-current" aria-hidden />
|
|
) : null}
|
|
{label ?? resolved.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/** Nur der farbige Punkt — für dichte Tabellen. */
|
|
export function StatusDot({
|
|
status,
|
|
className,
|
|
}: {
|
|
readonly status: string | null | undefined;
|
|
readonly className?: string | undefined;
|
|
}) {
|
|
const resolved = resolveStatus(status);
|
|
|
|
return (
|
|
<span
|
|
className={cn('inline-flex items-center gap-2', className)}
|
|
title={resolved.label}
|
|
>
|
|
<span
|
|
className={cn('size-2 rounded-full', toneClasses[resolved.tone].split(' ')[1])}
|
|
style={{ backgroundColor: 'currentColor' }}
|
|
aria-hidden
|
|
/>
|
|
<span className="sr-only">{resolved.label}</span>
|
|
</span>
|
|
);
|
|
}
|