- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites) - Remove localhost refs: openapi, inbox page - Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI - Add stub routes: knowledge/insights, recommendations, search, log-error - Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests - Fix inbox-server: ORDER BY created_at, package.json - Remove redundant app/api/inbox/files route (rewrites handle it) - knowledge/ in gitignore (large PDFs) Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
/**
|
||
* Error Boundary для обработки ошибок React компонентов
|
||
*/
|
||
'use client';
|
||
|
||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||
|
||
interface Props {
|
||
children: ReactNode;
|
||
fallback?: ReactNode;
|
||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||
}
|
||
|
||
interface State {
|
||
hasError: boolean;
|
||
error: Error | null;
|
||
errorInfo: ErrorInfo | null;
|
||
}
|
||
|
||
export class ErrorBoundary extends Component<Props, State> {
|
||
constructor(props: Props) {
|
||
super(props);
|
||
this.state = {
|
||
hasError: false,
|
||
error: null,
|
||
errorInfo: null,
|
||
};
|
||
}
|
||
|
||
static getDerivedStateFromError(error: Error): State {
|
||
return {
|
||
hasError: true,
|
||
error,
|
||
errorInfo: null,
|
||
};
|
||
}
|
||
|
||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||
// Логирование ошибки (только на клиенте)
|
||
if (typeof window !== 'undefined') {
|
||
// Используем console.error для клиентского логирования
|
||
console.error('React Error Boundary caught an error:', error, {
|
||
componentStack: errorInfo.componentStack,
|
||
errorBoundary: true,
|
||
});
|
||
|
||
// Отправка в Sentry через API (если настроен)
|
||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||
try {
|
||
fetch('/api/log-error', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: error.message,
|
||
stack: error.stack,
|
||
componentStack: errorInfo.componentStack,
|
||
}),
|
||
}).catch(() => {
|
||
// Игнорируем ошибки отправки
|
||
});
|
||
} catch (e) {
|
||
// Игнорируем ошибки
|
||
}
|
||
}
|
||
}
|
||
|
||
// Отправка в Sentry (если настроен)
|
||
if (typeof window !== 'undefined' && (window as any).Sentry) {
|
||
(window as any).Sentry.captureException(error, {
|
||
contexts: {
|
||
react: {
|
||
componentStack: errorInfo.componentStack,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
this.setState({
|
||
error,
|
||
errorInfo,
|
||
});
|
||
|
||
// Вызов пользовательского обработчика
|
||
if (this.props.onError) {
|
||
this.props.onError(error, errorInfo);
|
||
}
|
||
}
|
||
|
||
handleReset = () => {
|
||
this.setState({
|
||
hasError: false,
|
||
error: null,
|
||
errorInfo: null,
|
||
});
|
||
};
|
||
|
||
render() {
|
||
if (this.state.hasError) {
|
||
if (this.props.fallback) {
|
||
return this.props.fallback;
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
minHeight: '400px',
|
||
padding: '40px',
|
||
textAlign: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
maxWidth: '600px',
|
||
backgroundColor: 'white',
|
||
padding: '32px',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: '48px',
|
||
marginBottom: '16px',
|
||
}}
|
||
>
|
||
⚠️
|
||
</div>
|
||
<h2
|
||
style={{
|
||
fontSize: '24px',
|
||
fontWeight: 'bold',
|
||
marginBottom: '12px',
|
||
color: '#333',
|
||
}}
|
||
>
|
||
Произошла ошибка
|
||
</h2>
|
||
<p
|
||
style={{
|
||
fontSize: '16px',
|
||
color: '#666',
|
||
marginBottom: '24px',
|
||
lineHeight: '1.5',
|
||
}}
|
||
>
|
||
К сожалению, произошла непредвиденная ошибка. Мы уже работаем над её исправлением.
|
||
</p>
|
||
|
||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||
<details
|
||
style={{
|
||
marginBottom: '24px',
|
||
padding: '16px',
|
||
backgroundColor: '#f5f5f5',
|
||
borderRadius: '4px',
|
||
textAlign: 'left',
|
||
}}
|
||
>
|
||
<summary
|
||
style={{
|
||
cursor: 'pointer',
|
||
fontWeight: 'bold',
|
||
marginBottom: '8px',
|
||
}}
|
||
>
|
||
Детали ошибки (только в режиме разработки)
|
||
</summary>
|
||
<pre
|
||
style={{
|
||
fontSize: '12px',
|
||
overflow: 'auto',
|
||
whiteSpace: 'pre-wrap',
|
||
wordBreak: 'break-word',
|
||
}}
|
||
>
|
||
{this.state.error.toString()}
|
||
{this.state.errorInfo?.componentStack}
|
||
</pre>
|
||
</details>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||
<button
|
||
onClick={this.handleReset}
|
||
style={{
|
||
padding: '12px 24px',
|
||
backgroundColor: '#1e3a5f',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: '500',
|
||
}}
|
||
>
|
||
Попробовать снова
|
||
</button>
|
||
<button
|
||
onClick={() => window.location.href = '/'}
|
||
style={{
|
||
padding: '12px 24px',
|
||
backgroundColor: 'transparent',
|
||
color: '#1e3a5f',
|
||
border: '1px solid #1e3a5f',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
}}
|
||
>
|
||
На главную
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return this.props.children;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* HOC для обертки компонентов в Error Boundary
|
||
*/
|
||
export function withErrorBoundary<P extends object>(
|
||
Component: React.ComponentType<P>,
|
||
fallback?: ReactNode
|
||
) {
|
||
return function WrappedComponent(props: P) {
|
||
return (
|
||
<ErrorBoundary fallback={fallback}>
|
||
<Component {...props} />
|
||
</ErrorBoundary>
|
||
);
|
||
};
|
||
}
|