608 lines
22 KiB
TypeScript
608 lines
22 KiB
TypeScript
/**
|
|
* Website Analysis Status Component
|
|
* Compact terminal-themed component for displaying website analysis task status
|
|
* with execution logs in expanded sections
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
IconButton,
|
|
Tooltip,
|
|
CircularProgress,
|
|
Collapse,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableRow,
|
|
Chip,
|
|
Divider,
|
|
Button,
|
|
} from '@mui/material';
|
|
import {
|
|
RefreshCw,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertTriangle,
|
|
Info,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Globe,
|
|
Users,
|
|
} from 'lucide-react';
|
|
import { useAuth } from '@clerk/clerk-react';
|
|
import {
|
|
getWebsiteAnalysisStatus,
|
|
retryWebsiteAnalysis,
|
|
getWebsiteAnalysisLogs,
|
|
WebsiteAnalysisStatusResponse,
|
|
WebsiteAnalysisTask,
|
|
WebsiteAnalysisExecutionLog,
|
|
WebsiteAnalysisLogsResponse,
|
|
} from '../../api/websiteAnalysisMonitoring';
|
|
import {
|
|
TerminalPaper,
|
|
TerminalTypography,
|
|
TerminalChip,
|
|
TerminalChipSuccess,
|
|
TerminalChipError,
|
|
TerminalChipWarning,
|
|
TerminalAlert,
|
|
TerminalTableCell,
|
|
TerminalTableRow,
|
|
terminalColors,
|
|
} from './terminalTheme';
|
|
|
|
interface WebsiteAnalysisStatusProps {
|
|
compact?: boolean;
|
|
}
|
|
|
|
interface TaskLogs {
|
|
[taskId: number]: {
|
|
logs: WebsiteAnalysisExecutionLog[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
};
|
|
}
|
|
|
|
const WebsiteAnalysisStatus: React.FC<WebsiteAnalysisStatusProps> = ({ compact = true }) => {
|
|
const { userId } = useAuth();
|
|
const [status, setStatus] = useState<WebsiteAnalysisStatusResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState<number | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [expandedTaskId, setExpandedTaskId] = useState<number | null>(null);
|
|
const [taskLogs, setTaskLogs] = useState<TaskLogs>({});
|
|
const [hoveredLogId, setHoveredLogId] = useState<number | null>(null);
|
|
|
|
const fetchStatus = async () => {
|
|
if (!userId) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const response = await getWebsiteAnalysisStatus(userId);
|
|
setStatus(response);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to fetch website analysis status');
|
|
console.error('Error fetching website analysis status:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchTaskLogs = async (taskId: number) => {
|
|
if (!userId) return;
|
|
|
|
// Initialize task logs state if not exists
|
|
if (!taskLogs[taskId]) {
|
|
setTaskLogs(prev => ({
|
|
...prev,
|
|
[taskId]: { logs: [], loading: false, error: null }
|
|
}));
|
|
}
|
|
|
|
// Check if already loading
|
|
if (taskLogs[taskId]?.loading) return;
|
|
|
|
setTaskLogs(prev => ({
|
|
...prev,
|
|
[taskId]: { ...prev[taskId], loading: true, error: null }
|
|
}));
|
|
|
|
try {
|
|
console.log(`[WebsiteAnalysis] Fetching logs for task ${taskId}...`);
|
|
const response = await getWebsiteAnalysisLogs(userId, 10, 0, taskId);
|
|
console.log(`[WebsiteAnalysis] Received logs response:`, {
|
|
logsCount: response.logs?.length || 0,
|
|
totalCount: response.total_count,
|
|
hasLogs: !!(response.logs && response.logs.length > 0),
|
|
firstLog: response.logs?.[0] || null
|
|
});
|
|
|
|
if (response.logs && Array.isArray(response.logs)) {
|
|
setTaskLogs(prev => ({
|
|
...prev,
|
|
[taskId]: { logs: response.logs, loading: false, error: null }
|
|
}));
|
|
} else {
|
|
console.warn(`[WebsiteAnalysis] Invalid logs response structure:`, response);
|
|
setTaskLogs(prev => ({
|
|
...prev,
|
|
[taskId]: { logs: [], loading: false, error: 'Invalid response structure' }
|
|
}));
|
|
}
|
|
} catch (err: any) {
|
|
console.error(`[WebsiteAnalysis] Error fetching logs for task ${taskId}:`, err);
|
|
setTaskLogs(prev => ({
|
|
...prev,
|
|
[taskId]: { ...prev[taskId], loading: false, error: err.message || 'Failed to fetch logs' }
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleRetry = async (taskId: number) => {
|
|
if (!userId) return;
|
|
|
|
try {
|
|
setRefreshing(taskId);
|
|
await retryWebsiteAnalysis(taskId);
|
|
await fetchStatus(); // Refresh status
|
|
} catch (err: any) {
|
|
console.error('Error retrying website analysis:', err);
|
|
alert(err.message || 'Failed to retry website analysis');
|
|
} finally {
|
|
setRefreshing(null);
|
|
}
|
|
};
|
|
|
|
const handleToggleExpand = (taskId: number) => {
|
|
if (expandedTaskId === taskId) {
|
|
setExpandedTaskId(null);
|
|
} else {
|
|
setExpandedTaskId(taskId);
|
|
// Always fetch logs when expanding to get latest data
|
|
fetchTaskLogs(taskId);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchStatus();
|
|
// Refresh every 5 minutes (same as other dashboard components)
|
|
// Tasks run on schedule (every 10 days for competitors, etc.), so frequent polling is unnecessary
|
|
const interval = setInterval(fetchStatus, 5 * 60 * 1000);
|
|
return () => clearInterval(interval);
|
|
}, [userId]);
|
|
|
|
// Fetch logs when task is expanded (similar to OAuth pattern)
|
|
useEffect(() => {
|
|
if (expandedTaskId && userId) {
|
|
fetchTaskLogs(expandedTaskId);
|
|
}
|
|
}, [expandedTaskId, userId]);
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return <CheckCircle size={16} color={terminalColors.success} />;
|
|
case 'failed':
|
|
return <XCircle size={16} color={terminalColors.error} />;
|
|
case 'paused':
|
|
return <AlertTriangle size={16} color={terminalColors.warning} />;
|
|
default:
|
|
return <Info size={16} color={terminalColors.primary} />;
|
|
}
|
|
};
|
|
|
|
const getStatusChip = (taskStatus: string) => {
|
|
switch (taskStatus) {
|
|
case 'active':
|
|
return <TerminalChipSuccess label="Active" size="small" />;
|
|
case 'failed':
|
|
return <TerminalChipError label="Failed" size="small" />;
|
|
case 'paused':
|
|
return <TerminalChipWarning label="Paused" size="small" />;
|
|
default:
|
|
return <TerminalChip label={taskStatus} size="small" />;
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string | null) => {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
};
|
|
|
|
const getLogStatusChip = (logStatus: string) => {
|
|
switch (logStatus) {
|
|
case 'success':
|
|
return <TerminalChipSuccess label="Success" size="small" />;
|
|
case 'failed':
|
|
return <TerminalChipError label="Failed" size="small" />;
|
|
case 'running':
|
|
return <TerminalChipWarning label="Running" size="small" />;
|
|
default:
|
|
return <Chip label={logStatus} size="small" />;
|
|
}
|
|
};
|
|
|
|
const formatLogResult = (resultData: any): string => {
|
|
if (!resultData) return 'N/A';
|
|
if (typeof resultData === 'string') {
|
|
try {
|
|
resultData = JSON.parse(resultData);
|
|
} catch {
|
|
return resultData.substring(0, 50);
|
|
}
|
|
}
|
|
|
|
if (resultData.style_analysis) {
|
|
return 'Analysis completed';
|
|
}
|
|
if (resultData.crawl_result) {
|
|
return 'Crawl completed';
|
|
}
|
|
const str = JSON.stringify(resultData);
|
|
return str.length > 60 ? str.substring(0, 60) + '...' : str;
|
|
};
|
|
|
|
if (loading && !status) {
|
|
return (
|
|
<TerminalPaper sx={{ p: 2 }}>
|
|
<Box display="flex" justifyContent="center" alignItems="center" p={2}>
|
|
<CircularProgress size={20} sx={{ color: terminalColors.primary }} />
|
|
</Box>
|
|
</TerminalPaper>
|
|
);
|
|
}
|
|
|
|
if (!status) {
|
|
return null;
|
|
}
|
|
|
|
const allTasks = [
|
|
...status.data.user_website_tasks,
|
|
...status.data.competitor_tasks
|
|
];
|
|
|
|
const renderTaskRow = (task: WebsiteAnalysisTask) => {
|
|
const isExpanded = expandedTaskId === task.id;
|
|
const logs = taskLogs[task.id]?.logs || [];
|
|
const logsLoading = taskLogs[task.id]?.loading || false;
|
|
const logsError = taskLogs[task.id]?.error;
|
|
|
|
return (
|
|
<React.Fragment key={task.id}>
|
|
<TerminalTableRow
|
|
sx={{
|
|
cursor: 'pointer',
|
|
'&:hover': { backgroundColor: terminalColors.backgroundHover }
|
|
}}
|
|
onClick={() => handleToggleExpand(task.id)}
|
|
>
|
|
<TerminalTableCell>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{task.task_type === 'user_website' ? (
|
|
<Globe size={16} color={terminalColors.primary} />
|
|
) : (
|
|
<Users size={16} color={terminalColors.secondary} />
|
|
)}
|
|
<TerminalTypography variant="body2" sx={{ fontWeight: 500 }}>
|
|
{task.website_url}
|
|
</TerminalTypography>
|
|
</Box>
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{getStatusIcon(task.status)}
|
|
{getStatusChip(task.status)}
|
|
</Box>
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
<Box display="flex" alignItems="center" gap={0.5} flexWrap="wrap">
|
|
{task.last_success && (
|
|
<Tooltip title={`Last successful: ${formatDate(task.last_success)}`}>
|
|
<Chip
|
|
label={`Last: ${formatDate(task.last_success).split(',')[0]}`}
|
|
size="small"
|
|
sx={{
|
|
height: 20,
|
|
fontSize: '0.7rem',
|
|
border: `1px solid ${terminalColors.border}`,
|
|
backgroundColor: terminalColors.background,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
{task.next_check && (
|
|
<Tooltip title={`Next check: ${formatDate(task.next_check)}`}>
|
|
<Chip
|
|
label={`Next: ${formatDate(task.next_check).split(',')[0]}`}
|
|
size="small"
|
|
sx={{
|
|
height: 20,
|
|
fontSize: '0.7rem',
|
|
border: `1px solid ${terminalColors.border}`,
|
|
backgroundColor: terminalColors.background,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
</TerminalTableCell>
|
|
<TerminalTableCell>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{task.status === 'failed' && (
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRetry(task.id);
|
|
}}
|
|
disabled={refreshing === task.id}
|
|
sx={{
|
|
minWidth: 'auto',
|
|
px: 1,
|
|
py: 0.5,
|
|
fontSize: '0.7rem',
|
|
borderColor: terminalColors.border,
|
|
color: terminalColors.text,
|
|
'&:hover': {
|
|
borderColor: terminalColors.primary,
|
|
backgroundColor: terminalColors.backgroundHover,
|
|
},
|
|
}}
|
|
>
|
|
{refreshing === task.id ? <CircularProgress size={12} /> : 'Retry'}
|
|
</Button>
|
|
)}
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleToggleExpand(task.id);
|
|
}}
|
|
sx={{ color: terminalColors.text }}
|
|
>
|
|
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
</IconButton>
|
|
</Box>
|
|
</TerminalTableCell>
|
|
</TerminalTableRow>
|
|
|
|
<TableRow>
|
|
<TableCell colSpan={4} sx={{ py: 0, border: 0 }}>
|
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
|
<Box sx={{ p: 2, backgroundColor: terminalColors.backgroundSecondary }}>
|
|
{task.failure_reason && (
|
|
<TerminalAlert severity="error" sx={{ mb: 2 }}>
|
|
Error: {task.failure_reason}
|
|
</TerminalAlert>
|
|
)}
|
|
|
|
<Typography variant="h6" sx={{ mb: 1, color: terminalColors.text, fontSize: '0.9rem' }}>
|
|
Monitoring Logs
|
|
</Typography>
|
|
|
|
{logsLoading ? (
|
|
<Box display="flex" justifyContent="center" p={2}>
|
|
<CircularProgress size={16} sx={{ color: terminalColors.primary }} />
|
|
</Box>
|
|
) : logsError ? (
|
|
<TerminalAlert severity="error">{logsError}</TerminalAlert>
|
|
) : logs.length === 0 ? (
|
|
<Typography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
|
No execution logs yet
|
|
</Typography>
|
|
) : (
|
|
<Box
|
|
sx={{
|
|
maxHeight: '300px',
|
|
overflowY: 'auto',
|
|
border: `1px solid ${terminalColors.border}`,
|
|
borderRadius: 1,
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
},
|
|
'&::-webkit-scrollbar-track': {
|
|
backgroundColor: terminalColors.background,
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: terminalColors.border,
|
|
borderRadius: '4px',
|
|
},
|
|
}}
|
|
>
|
|
<Table size="small" stickyHeader>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell sx={{ backgroundColor: terminalColors.background, color: terminalColors.text, fontSize: '0.75rem', py: 1 }}>
|
|
Date
|
|
</TableCell>
|
|
<TableCell sx={{ backgroundColor: terminalColors.background, color: terminalColors.text, fontSize: '0.75rem', py: 1 }}>
|
|
Status
|
|
</TableCell>
|
|
<TableCell sx={{ backgroundColor: terminalColors.background, color: terminalColors.text, fontSize: '0.75rem', py: 1 }}>
|
|
Result
|
|
</TableCell>
|
|
<TableCell sx={{ backgroundColor: terminalColors.background, color: terminalColors.text, fontSize: '0.75rem', py: 1 }}>
|
|
Duration
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{logs.map((log) => (
|
|
<React.Fragment key={log.id}>
|
|
<TableRow
|
|
sx={{
|
|
'&:hover': { backgroundColor: terminalColors.backgroundHover },
|
|
cursor: 'pointer',
|
|
}}
|
|
onMouseEnter={() => setHoveredLogId(log.id)}
|
|
onMouseLeave={() => setHoveredLogId(null)}
|
|
>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>
|
|
{formatDate(log.execution_date)}
|
|
</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>
|
|
{getLogStatusChip(log.status)}
|
|
</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>
|
|
{formatLogResult(log.result_data)}
|
|
</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>
|
|
{log.execution_time_ms ? `${log.execution_time_ms}ms` : 'N/A'}
|
|
</TerminalTableCell>
|
|
</TableRow>
|
|
{hoveredLogId === log.id && (
|
|
<TableRow>
|
|
<TableCell colSpan={4} sx={{ py: 1, border: 0, backgroundColor: terminalColors.backgroundSecondary }}>
|
|
{log.error_message && (
|
|
<Box sx={{ mb: 1 }}>
|
|
<Typography variant="caption" sx={{ color: terminalColors.error, fontWeight: 'bold' }}>
|
|
Error:
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: terminalColors.text, display: 'block', ml: 1 }}>
|
|
{log.error_message}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
{log.result_data && (
|
|
<Box>
|
|
<Typography variant="caption" sx={{ color: terminalColors.textSecondary, fontWeight: 'bold' }}>
|
|
Result Data:
|
|
</Typography>
|
|
<pre style={{
|
|
fontSize: '0.7rem',
|
|
color: terminalColors.text,
|
|
margin: '4px 0 0 0',
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word'
|
|
}}>
|
|
{JSON.stringify(log.result_data, null, 2)}
|
|
</pre>
|
|
</Box>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Collapse>
|
|
</TableCell>
|
|
</TableRow>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<TerminalPaper sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
|
<TerminalTypography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Globe size={20} />
|
|
Website Analysis Status
|
|
</TerminalTypography>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{status && (
|
|
<TerminalChip
|
|
label={`${status.data.active_tasks} Active`}
|
|
size="small"
|
|
/>
|
|
)}
|
|
{status && status.data.failed_tasks > 0 && (
|
|
<TerminalChipError
|
|
label={`${status.data.failed_tasks} Failed`}
|
|
size="small"
|
|
/>
|
|
)}
|
|
<IconButton
|
|
size="small"
|
|
onClick={fetchStatus}
|
|
disabled={loading}
|
|
sx={{ color: terminalColors.text }}
|
|
>
|
|
<RefreshCw size={16} />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
|
|
{error && (
|
|
<TerminalAlert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</TerminalAlert>
|
|
)}
|
|
|
|
{status && (
|
|
<>
|
|
{status.data.user_website_tasks.length > 0 && (
|
|
<Box mb={2}>
|
|
<TerminalTypography variant="subtitle2" sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Globe size={14} />
|
|
User Website ({status.data.user_website_tasks.length})
|
|
</TerminalTypography>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Website</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Status</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Timing</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Actions</TerminalTableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{status.data.user_website_tasks.map(renderTaskRow)}
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
)}
|
|
|
|
{status.data.competitor_tasks.length > 0 && (
|
|
<Box>
|
|
<TerminalTypography variant="subtitle2" sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Users size={14} />
|
|
Competitors ({status.data.competitor_tasks.length})
|
|
</TerminalTypography>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Website</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Status</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Timing</TerminalTableCell>
|
|
<TerminalTableCell sx={{ fontSize: '0.75rem' }}>Actions</TerminalTableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{status.data.competitor_tasks.map(renderTaskRow)}
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
)}
|
|
|
|
{allTasks.length === 0 && (
|
|
<Box p={2} textAlign="center">
|
|
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
|
No website analysis tasks found. Complete onboarding to create tasks.
|
|
</TerminalTypography>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
</TerminalPaper>
|
|
);
|
|
};
|
|
|
|
export default WebsiteAnalysisStatus;
|
|
|