perf: Fix all 7 code analysis issues
HIGH (Performance):
- Replace 8 sync file operations with aiofiles in async context (storage, mail,
report_generator, dms_bridge, ai_assistant)
- Frontend bundle splitting: manualChunks for react-vendor, ui-components, tanstack,
markdown, icons, utils, i18n (ui chunk 936K → ~19K)
MEDIUM (Architecture):
- Worker circular deps: Replace direct plugin imports with job_registry.py pattern
(register_job/get_all_jobs, importlib-based lazy loading)
- App-wide ErrorBoundary: New ErrorBoundary.tsx component, wrapped in AppShell
and all standalone routes
LOW (Code Quality):
- N+1 query fix: selectinload(Contact.contact_persons) in list_contacts()
- O(n²) dedup fix: SQL GROUP BY for email/phone duplicates, Dict-based name grouping
- Response format standardization: 7 routes converted from plain arrays to
{items: [...], total: N} format
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* App-wide ErrorBoundary that catches React rendering errors
|
||||
* and displays a fallback UI with a reload button.
|
||||
*/
|
||||
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
console.error('[ErrorBoundary] Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReload = (): void => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center min-h-screen bg-secondary-50 p-8"
|
||||
role="alert"
|
||||
>
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
||||
<div className="text-red-500 text-6xl mb-4" aria-hidden="true">
|
||||
⚠️
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-2">
|
||||
Ein Fehler ist aufgetreten
|
||||
</h1>
|
||||
<p className="text-secondary-600 mb-6">
|
||||
Die Anwendung konnte nicht geladen werden. Bitte versuchen Sie es erneut.
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<details className="mb-6 text-left">
|
||||
<summary className="cursor-pointer text-sm text-secondary-500 hover:text-secondary-700">
|
||||
Fehlerdetails
|
||||
</summary>
|
||||
<pre className="mt-2 p-3 bg-secondary-100 rounded text-xs text-secondary-700 overflow-auto max-h-32">
|
||||
{this.state.error.message}
|
||||
{this.state.error.stack && `\n\n${this.state.error.stack}`}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
className="inline-flex items-center px-6 py-3 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Neu laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -10,6 +10,7 @@ import { useAIUIControl } from '@/hooks/useAIUIControl';
|
||||
import { PluginRegistry } from '@/components/plugins/PluginRegistry';
|
||||
import { AIUIControlIndicator } from '@/components/ai-ui-control/AIUIControlIndicator';
|
||||
import { WindowContainer } from '@/components/window/WindowContainer';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
|
||||
export function AppShell() {
|
||||
const location = useLocation();
|
||||
@@ -38,7 +39,9 @@ export function AppShell() {
|
||||
id="main-content"
|
||||
data-testid="content-area"
|
||||
>
|
||||
<Outlet />
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PasswordResetRequestPage } from '@/pages/PasswordResetRequest';
|
||||
import { PasswordResetConfirmPage } from '@/pages/PasswordResetConfirm';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { PluginRouteRenderer } from '@/components/plugins/PluginRouteRenderer';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
|
||||
// Lazy-loaded pages (code-splitting)
|
||||
const DashboardPage = React.lazy(() => import('@/pages/Dashboard').then(m => ({ default: m.DashboardPage })));
|
||||
@@ -73,23 +74,23 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/ai-assistant-standalone',
|
||||
element: withSuspense(<AIAssistantStandalonePage />),
|
||||
element: <ErrorBoundary>{withSuspense(<AIAssistantStandalonePage />)}</ErrorBoundary>,
|
||||
},
|
||||
{
|
||||
path: '/dms-standalone',
|
||||
element: withSuspense(<DmsStandalonePage />),
|
||||
element: <ErrorBoundary>{withSuspense(<DmsStandalonePage />)}</ErrorBoundary>,
|
||||
},
|
||||
{
|
||||
path: '/calendar-standalone',
|
||||
element: withSuspense(<CalendarStandalonePage />),
|
||||
element: <ErrorBoundary>{withSuspense(<CalendarStandalonePage />)}</ErrorBoundary>,
|
||||
},
|
||||
{
|
||||
path: '/mail-standalone',
|
||||
element: withSuspense(<MailStandalonePage />),
|
||||
element: <ErrorBoundary>{withSuspense(<MailStandalonePage />)}</ErrorBoundary>,
|
||||
},
|
||||
{
|
||||
path: '/contacts-standalone',
|
||||
element: withSuspense(<ContactsStandalonePage />),
|
||||
element: <ErrorBoundary>{withSuspense(<ContactsStandalonePage />)}</ErrorBoundary>,
|
||||
},
|
||||
{
|
||||
path: '/password-reset',
|
||||
|
||||
Reference in New Issue
Block a user