import React, { Suspense, lazy, Component, ReactNode } from 'react'; import { Loader2 } from 'lucide-react'; import { logError } from '@/utils/errorLogger'; // ── Error Boundary ────────────────────────────────────────────────────── interface PluginErrorBoundaryProps { children: ReactNode; pluginName: string; } interface PluginErrorBoundaryState { hasError: boolean; error: Error | null; traceId: string | null; } function generateTraceId(): string { return 'plugin-err-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); } class PluginErrorBoundary extends Component { state: PluginErrorBoundaryState = { hasError: false, error: null, traceId: null }; static getDerivedStateFromError(error: Error): PluginErrorBoundaryState { return { hasError: true, error, traceId: generateTraceId() }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { logError(error, { componentStack: errorInfo.componentStack, pluginName: this.props.pluginName, trace_id: this.state.traceId, source: 'PluginErrorBoundary', }); } handleRetry = (): void => { this.setState({ hasError: false, error: null, traceId: null }); }; render() { if (this.state.hasError) { return (

Plugin konnte nicht geladen werden

Das Plugin „{this.props.pluginName}" hat einen Fehler verursacht.

{this.state.traceId && (

Trace-ID: {this.state.traceId}

)} {this.state.error && (
Fehlerdetails
                {this.state.error.message}
                {this.state.error.stack ? `\n\n${this.state.error.stack}` : ''}
              
)}
); } return this.props.children; } } // ── Static Chunk Map (ARCH-019) ───────────────────────────────────────── // // Vite cannot statically analyze dynamic import paths built at runtime // (`import(/* @vite-ignore */ path)`), so production builds would fail to // load plugin pages. All plugin-contributed component paths are known at // build time — they are declared in the backend manifests — so we register // them here as explicit lazy imports. Vite chunks each one correctly. // // Unknown paths (e.g. third-party plugins added later) fall back to the // runtime dynamic import, which works in dev mode. type ComponentModule = Record & { default?: React.ComponentType }; type LazyComponentFactory = () => Promise<{ default: React.ComponentType }>; /** * Normalize a module namespace to the { default } shape React.lazy expects: * uses the default export when present, otherwise the first named export * (same fallback as the runtime dynamic-import path below). */ function normalizeModule(m: ComponentModule): { default: React.ComponentType } { const Comp = (m.default ?? m[Object.keys(m)[0]]) as React.ComponentType; return { default: Comp }; } const STATIC_COMPONENT_MAP: Record = { // Pages '@/pages/AgentDashboard': () => import('@/pages/AgentDashboard').then(normalizeModule), // BUG (ghost page) fixed in Block I-D: page now exists. '@/pages/AIAssistant': () => import('@/pages/AIAssistant').then(normalizeModule), '@/pages/AISettings': () => import('@/pages/AISettings').then(normalizeModule), '@/pages/AutomationDashboard': () => import('@/pages/AutomationDashboard').then(normalizeModule), '@/pages/AutomationSettings': () => import('@/pages/AutomationSettings').then(normalizeModule), '@/pages/Calendar': () => import('@/pages/Calendar').then(normalizeModule), '@/pages/Communication': () => import('@/pages/Communication').then(normalizeModule), '@/pages/Dms': () => import('@/pages/Dms').then(normalizeModule), '@/pages/DmsTrash': () => import('@/pages/DmsTrash').then(normalizeModule), '@/pages/GlobalSearchResults': () => import('@/pages/GlobalSearchResults').then(normalizeModule), '@/pages/ImportExport': () => import('@/pages/ImportExport').then(normalizeModule), '@/pages/Mail': () => import('@/pages/Mail').then(normalizeModule), '@/pages/MailSettings': () => import('@/pages/MailSettings').then(normalizeModule), '@/pages/ProactiveAISettings': () => import('@/pages/ProactiveAISettings').then(normalizeModule), '@/pages/Reports': () => import('@/pages/Reports').then(normalizeModule), '@/pages/SettingsGroups': () => import('@/pages/SettingsGroups').then(normalizeModule), '@/pages/SettingsNotifications': () => import('@/pages/SettingsNotifications').then(normalizeModule), '@/pages/SettingsRoles': () => import('@/pages/SettingsRoles').then(normalizeModule), '@/pages/SettingsUsers': () => import('@/pages/SettingsUsers').then(normalizeModule), '@/pages/Tasks': () => import('@/pages/Tasks').then(normalizeModule), '@/pages/Wiki': () => import('@/pages/Wiki').then(normalizeModule), '@/pages/Workflows': () => import('@/pages/Workflows').then(normalizeModule), // NOTE: The backend manifests also declare contact detail tabs // (@/components/contact/Contact*Tab) whose components do not exist in the // frontend yet — they are intentionally NOT registered here; they fall // through to the runtime fallback and surface via the error boundary. // See PROGRESS.md "Ghost components" finding. }; // ── Lazy Component Cache ──────────────────────────────────────────────── const componentCache = new Map>>(); function getLazyComponent(componentPath: string): React.LazyExoticComponent> { if (componentCache.has(componentPath)) { return componentCache.get(componentPath)!; } // Known plugin components: statically chunked at build time (ARCH-019) const factory = STATIC_COMPONENT_MAP[componentPath]; if (factory) { const LazyComp = lazy(factory); componentCache.set(componentPath, LazyComp); return LazyComp; } // Fallback for unknown paths (third-party plugins): runtime dynamic // import. Works in dev; production needs the path registered above. const importPath = componentPath.replace(/^@\//, '../../'); const LazyComp = lazy(() => import(/* @vite-ignore */ importPath).then((m) => ({ default: m.default || m[Object.keys(m)[0]], })) ); componentCache.set(componentPath, LazyComp); return LazyComp; } // ── PluginPage Component ─────────────────────────────────────────────── interface PluginPageProps { component: string; pluginName: string; } /** * PluginPage — dynamically loads a plugin page component using React.lazy(). * Wraps the lazy component in Suspense with a centered spinner fallback * and an ErrorBoundary that shows a fallback message on failure. */ export function PluginPage({ component, pluginName }: PluginPageProps) { const LazyComp = getLazyComponent(component); return ( } > ); }