**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>
43 lines
1.3 KiB
TypeScript
43 lines
1.3 KiB
TypeScript
/** Kopfbereich einer Seite: Titel, Erläuterung, Handlungen. */
|
|
|
|
import { ChevronLeft } from 'lucide-react';
|
|
import type { ReactNode } from 'react';
|
|
import { Button } from '@/components/ui';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export function PageHeader({
|
|
title,
|
|
description,
|
|
actions,
|
|
onBack,
|
|
backLabel = 'Zurück',
|
|
className,
|
|
}: {
|
|
readonly title: ReactNode;
|
|
readonly description?: ReactNode | undefined;
|
|
readonly actions?: ReactNode | undefined;
|
|
readonly onBack?: (() => void) | undefined;
|
|
readonly backLabel?: string | undefined;
|
|
readonly className?: string | undefined;
|
|
}) {
|
|
return (
|
|
<div className={cn('mb-5', className)}>
|
|
{onBack ? (
|
|
<Button variant="ghost" size="sm" className="-ml-2 mb-2" onClick={onBack}>
|
|
<ChevronLeft />
|
|
{backLabel}
|
|
</Button>
|
|
) : null}
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<h2 className="text-xl font-semibold tracking-tight text-fg">{title}</h2>
|
|
{description ? (
|
|
<p className="mt-1 max-w-3xl text-sm text-fg-muted">{description}</p>
|
|
) : null}
|
|
</div>
|
|
{actions ? <div className="flex shrink-0 flex-wrap gap-2">{actions}</div> : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|