- Implement session management API (/api/v1/session/create, verify, logout) - Add session restoration from localStorage on page reload - Fix session_id priority when loading drafts (use current, not old from DB) - Add unified_id and claim_id to wizard payload sent to n8n - Add Docker volume for frontend HMR (Hot Module Replacement) - Add comprehensive session logging for debugging Components updated: - backend/app/api/session.py (NEW) - Session management endpoints - backend/app/main.py - Include session router - frontend/src/components/form/Step1Phone.tsx v2.0 - Create session after SMS - frontend/src/pages/ClaimForm.tsx v3.8 - Session restoration & priority fix - frontend/src/components/form/StepWizardPlan.tsx v1.4 - Add unified_id/claim_id - docker-compose.yml - Add frontend volume for live reload Session flow: 1. User verifies phone -> session created in Redis (24h TTL) 2. session_token saved to localStorage 3. Page reload -> session restored automatically 4. Draft selected -> current session_id used (not old from DB) 5. Wizard submit -> unified_id, claim_id, session_id sent to n8n 6. Logout -> session removed from Redis & localStorage Fixes: - Session token not persisting after page reload - unified_id missing in n8n webhook payload - Old session_id from draft overwriting current session - Frontend changes requiring container rebuild
274 lines
12 KiB
TypeScript
274 lines
12 KiB
TypeScript
import { Card, Timeline, Tag, Descriptions } from 'antd';
|
||
import { CheckCircleOutlined, LoadingOutlined, ExclamationCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||
|
||
interface DebugEvent {
|
||
timestamp: string;
|
||
type: 'policy_check' | 'ocr' | 'ai_analysis' | 'upload' | 'error';
|
||
status: 'pending' | 'success' | 'warning' | 'error';
|
||
message: string;
|
||
data?: any;
|
||
}
|
||
|
||
interface Props {
|
||
events: DebugEvent[];
|
||
formData: any;
|
||
}
|
||
|
||
export default function DebugPanel({ events, formData }: Props) {
|
||
const getIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'success': return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
|
||
case 'pending': return <LoadingOutlined style={{ color: '#1890ff' }} />;
|
||
case 'warning': return <ExclamationCircleOutlined style={{ color: '#faad14' }} />;
|
||
case 'error': return <CloseCircleOutlined style={{ color: '#f5222d' }} />;
|
||
default: return <CheckCircleOutlined />;
|
||
}
|
||
};
|
||
|
||
const getColor = (status: string) => {
|
||
switch (status) {
|
||
case 'success': return 'success';
|
||
case 'pending': return 'processing';
|
||
case 'warning': return 'warning';
|
||
case 'error': return 'error';
|
||
default: return 'default';
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div style={{
|
||
position: 'sticky',
|
||
top: 20,
|
||
height: 'calc(100vh - 40px)',
|
||
overflowY: 'auto'
|
||
}}>
|
||
<Card
|
||
title="🔧 Debug Console"
|
||
size="small"
|
||
style={{
|
||
background: '#1e1e1e',
|
||
color: '#d4d4d4',
|
||
border: '1px solid #333'
|
||
}}
|
||
styles={{
|
||
header: {
|
||
background: '#252526',
|
||
color: '#fff',
|
||
borderBottom: '1px solid #333'
|
||
},
|
||
body: {
|
||
padding: 12
|
||
}
|
||
}}
|
||
>
|
||
{/* Текущие данные формы */}
|
||
<div style={{ marginBottom: 16, padding: 12, background: '#2d2d30', borderRadius: 4 }}>
|
||
<div style={{ fontSize: 12, color: '#9cdcfe', marginBottom: 8, fontFamily: 'monospace' }}>
|
||
<strong>Form Data:</strong>
|
||
</div>
|
||
<pre style={{
|
||
margin: 0,
|
||
fontSize: 11,
|
||
color: '#ce9178',
|
||
fontFamily: 'Consolas, monospace',
|
||
maxHeight: 150,
|
||
overflow: 'auto'
|
||
}}>
|
||
{JSON.stringify(formData, null, 2)}
|
||
</pre>
|
||
</div>
|
||
|
||
{/* События */}
|
||
<div style={{ marginBottom: 8, fontSize: 12, color: '#9cdcfe', fontFamily: 'monospace' }}>
|
||
<strong>Events Log:</strong>
|
||
</div>
|
||
|
||
<Timeline
|
||
style={{ marginTop: 16 }}
|
||
items={events.length === 0 ? [
|
||
{
|
||
color: 'gray',
|
||
children: <span style={{ color: '#888', fontSize: 12 }}>Нет событий...</span>
|
||
}
|
||
] : events.map((event, index) => ({
|
||
key: index,
|
||
dot: getIcon(event.status),
|
||
children: (
|
||
<div style={{ fontSize: 11, fontFamily: 'monospace' }}>
|
||
<div style={{ color: '#888', marginBottom: 4 }}>
|
||
{event.timestamp}
|
||
</div>
|
||
<div style={{ marginBottom: 8 }}>
|
||
<Tag color={getColor(event.status)} style={{ fontSize: 11 }}>
|
||
{event.type.toUpperCase()}
|
||
</Tag>
|
||
<span style={{ color: '#d4d4d4' }}>{event.message}</span>
|
||
</div>
|
||
|
||
{event.data && (
|
||
<div style={{
|
||
marginTop: 8,
|
||
padding: 8,
|
||
background: '#1e1e1e',
|
||
borderRadius: 4,
|
||
border: '1px solid #333'
|
||
}}>
|
||
{event.type === 'policy_check' && event.data.found !== undefined && (
|
||
<Descriptions size="small" column={1} bordered>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Found</span>}
|
||
labelStyle={{ background: '#252526', color: '#9cdcfe' }}
|
||
contentStyle={{ background: '#1e1e1e', color: event.data.found ? '#4ec9b0' : '#f48771' }}
|
||
>
|
||
{event.data.found ? 'TRUE' : 'FALSE'}
|
||
</Descriptions.Item>
|
||
{event.data.holder_name && (
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Holder</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#ce9178' }}
|
||
>
|
||
{event.data.holder_name}
|
||
</Descriptions.Item>
|
||
)}
|
||
</Descriptions>
|
||
)}
|
||
|
||
{event.type === 'ocr' && (
|
||
<pre style={{
|
||
margin: 0,
|
||
fontSize: 10,
|
||
color: '#ce9178',
|
||
maxHeight: 100,
|
||
overflow: 'auto',
|
||
whiteSpace: 'pre-wrap'
|
||
}}>
|
||
{event.data.text?.substring(0, 300)}...
|
||
</pre>
|
||
)}
|
||
|
||
{event.type === 'ai_analysis' && (
|
||
<Descriptions size="small" column={1} bordered>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Type</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#4ec9b0' }}
|
||
>
|
||
{event.data.document_type}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Valid</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: event.data.is_valid ? '#4ec9b0' : '#f48771' }}
|
||
>
|
||
{event.data.is_valid ? 'TRUE' : 'FALSE'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Confidence</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
|
||
>
|
||
{(event.data.confidence * 100).toFixed(0)}%
|
||
</Descriptions.Item>
|
||
{event.data.extracted_data && Object.keys(event.data.extracted_data).length > 0 && (
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Extracted</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e' }}
|
||
>
|
||
<pre style={{
|
||
margin: 0,
|
||
fontSize: 10,
|
||
color: '#ce9178',
|
||
whiteSpace: 'pre-wrap'
|
||
}}>
|
||
{JSON.stringify(event.data.extracted_data, null, 2)}
|
||
</pre>
|
||
</Descriptions.Item>
|
||
)}
|
||
</Descriptions>
|
||
)}
|
||
|
||
{event.type === 'upload' && event.data.files && (
|
||
<div>
|
||
{event.data.files.map((file: any, idx: number) => (
|
||
<Descriptions key={idx} size="small" column={1} bordered style={{ marginBottom: 8 }}>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>File</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#ce9178', fontSize: 10 }}
|
||
>
|
||
{file.filename}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 9, fontFamily: 'monospace' }}
|
||
>
|
||
{file.file_id}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
|
||
>
|
||
{(file.size / 1024).toFixed(1)} KB
|
||
</Descriptions.Item>
|
||
{file.url && (
|
||
<Descriptions.Item
|
||
label={<span style={{ color: '#9cdcfe' }}>S3 URL</span>}
|
||
labelStyle={{ background: '#252526' }}
|
||
contentStyle={{ background: '#1e1e1e', fontSize: 9, wordBreak: 'break-all' }}
|
||
>
|
||
<a
|
||
href={file.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{ color: '#4ec9b0', textDecoration: 'underline' }}
|
||
>
|
||
{file.url}
|
||
</a>
|
||
<div style={{ marginTop: 4 }}>
|
||
<a
|
||
href={file.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{
|
||
fontSize: 10,
|
||
color: '#569cd6',
|
||
background: '#1e1e1e',
|
||
padding: '2px 6px',
|
||
borderRadius: 3,
|
||
border: '1px solid #333'
|
||
}}
|
||
>
|
||
🔗 Открыть в новой вкладке
|
||
</a>
|
||
</div>
|
||
</Descriptions.Item>
|
||
)}
|
||
</Descriptions>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}))}
|
||
/>
|
||
|
||
{events.length > 0 && (
|
||
<div style={{ marginTop: 16, padding: 8, background: '#2d2d30', borderRadius: 4, textAlign: 'center' }}>
|
||
<span style={{ fontSize: 11, color: '#888' }}>
|
||
Total events: {events.length}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|