feat(C): Phase C — Core UI prüfen, vervollständigen, testen

C-ERR-BOUNDARY: ErrorBoundary erweitert (trace_id, Fallback-UI, PluginErrorBoundary)
C-NOTIF: NotificationDropdown mit System-Channel-Link
C-DOCS: ApiDocs.tsx Seite (Swagger UI iframe)
C-A11Y: aria-label, min-h-touch, focus-visible ergänzt
C-FE-TEST: 29 neue Tests (ErrorBoundary, ApiDocs, PrintButton, themeStore, NotificationDropdown)
C-DOC: ui-design-guidelines.md aktualisiert

Verifiziert (keine Änderungen nötig):
- C-TAGS, C-CF, C-FILTER, C-DEDUP, C-ONBOARD, C-THEME, C-PWA, C-PRINT

TSC: 0 errors, Build: erfolgreich, Vitest: 29/29 passed
This commit is contained in:
Agent Zero
2026-08-13 21:57:59 +02:00
parent 0e72d4624d
commit 25b2581653
15 changed files with 713 additions and 62 deletions
@@ -3,13 +3,19 @@ import { logError } from '@/utils/errorLogger';
interface ErrorBoundaryProps {
children: ReactNode;
/** Optional fallback render function; receives the error and a retry callback */
fallback?: (error: Error, retry: () => void) => ReactNode;
/** Optional fallback render function; receives the error, a retry callback, and a trace_id */
fallback?: (error: Error, retry: () => void, traceId: string) => ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
traceId: string | null;
}
/** Generate a short unique trace ID for error tracking */
function generateTraceId(): string {
return 'err-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}
/**
@@ -21,79 +27,78 @@ interface ErrorBoundaryState {
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
this.state = { hasError: false, error: null, traceId: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
return { hasError: true, error, traceId: generateTraceId() };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log to console + backend + sessionStorage buffer
logError(error, { componentStack: errorInfo.componentStack });
// Log to console + backend + sessionStorage buffer with trace_id
logError(error, { componentStack: errorInfo.componentStack, trace_id: this.state.traceId });
}
handleRetry = (): void => {
this.setState({ hasError: false, error: null });
this.setState({ hasError: false, error: null, traceId: null });
};
render(): ReactNode {
if (this.state.hasError && this.state.error) {
const traceId = this.state.traceId || '';
// Custom fallback if provided
if (this.props.fallback) {
return this.props.fallback(this.state.error, this.handleRetry);
return this.props.fallback(this.state.error, this.handleRetry, traceId);
}
// Default fallback UI
// Default fallback UI — Tailwind classes, trace_id, retry + reload
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '300px',
padding: '2rem',
textAlign: 'center',
}}
className="flex flex-col items-center justify-center min-h-[300px] p-8 text-center"
role="alert"
aria-live="assertive"
>
<h2 style={{ marginBottom: '0.5rem', color: '#dc2626' }}>
<div
className="text-danger-500 text-5xl mb-4"
aria-hidden="true"
>
</div>
<h2 className="text-xl font-bold text-secondary-900 mb-2">
Etwas ist schiefgelaufen
</h2>
<p style={{ marginBottom: '1rem', color: '#6b7280', maxWidth: '400px' }}>
<p className="text-sm text-secondary-600 mb-4 max-w-md">
Ein unerwarteter Fehler ist aufgetreten. Sie können es erneut versuchen oder die Seite aktualisieren.
</p>
<details style={{ marginBottom: '1rem', maxWidth: '600px', color: '#9ca3af' }}>
<summary style={{ cursor: 'pointer', fontSize: '0.875rem' }}>
{traceId && (
<p className="text-xs text-secondary-400 mb-4 font-mono" data-testid="error-trace-id">
Trace-ID: {traceId}
</p>
)}
<details className="mb-4 max-w-lg text-left text-secondary-500">
<summary className="cursor-pointer text-sm hover:text-secondary-700">
Fehlerdetails
</summary>
<pre
style={{
marginTop: '0.5rem',
fontSize: '0.75rem',
overflow: 'auto',
textAlign: 'left',
}}
>
<pre className="mt-2 p-3 bg-secondary-50 rounded text-xs overflow-auto max-h-32">
{this.state.error.message}
{this.state.error.stack ? `\n\n${this.state.error.stack}` : ''}
</pre>
</details>
<button
onClick={this.handleRetry}
style={{
padding: '0.5rem 1.5rem',
backgroundColor: '#2563eb',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
fontSize: '0.875rem',
fontWeight: 500,
}}
>
Erneut versuchen
</button>
<div className="flex gap-3">
<button
onClick={this.handleRetry}
className="inline-flex items-center px-5 py-2.5 bg-primary-600 text-white font-medium rounded-md hover:bg-primary-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 transition-colors min-h-touch"
>
Erneut versuchen
</button>
<button
onClick={() => window.location.reload()}
className="inline-flex items-center px-5 py-2.5 bg-secondary-100 text-secondary-700 font-medium rounded-md hover:bg-secondary-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-secondary-400 focus-visible:ring-offset-2 transition-colors min-h-touch"
>
Neu laden
</button>
</div>
</div>
);
}