AI platform insights monitoring and website analysis monitoring services added

This commit is contained in:
ajaysi
2025-11-11 15:57:45 +05:30
parent d99c7c83a7
commit 7191c7e7f0
81 changed files with 10860 additions and 1567 deletions

View File

@@ -1,6 +1,7 @@
/**
* OAuth Token Status Component
* Compact terminal-themed component for displaying OAuth token monitoring status
* with platform-specific execution logs in expanded sections
*/
import React, { useState, useEffect } from 'react';
@@ -16,6 +17,8 @@ import {
TableCell,
TableHead,
TableRow,
Chip,
Divider,
} from '@mui/material';
import {
RefreshCw,
@@ -30,8 +33,11 @@ import { useAuth } from '@clerk/clerk-react';
import {
getOAuthTokenStatus,
manualRefreshToken,
getOAuthTokenExecutionLogs,
OAuthTokenStatusResponse,
ManualRefreshResponse,
ExecutionLog,
ExecutionLogsResponse,
} from '../../api/oauthTokenMonitoring';
import {
TerminalPaper,
@@ -41,6 +47,8 @@ import {
TerminalChipError,
TerminalChipWarning,
TerminalAlert,
TerminalTableCell,
TerminalTableRow,
terminalColors,
} from './terminalTheme';
@@ -48,6 +56,14 @@ interface OAuthTokenStatusProps {
compact?: boolean;
}
interface PlatformLogs {
[platform: string]: {
logs: ExecutionLog[];
loading: boolean;
error: string | null;
};
}
const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) => {
const { userId } = useAuth();
const [status, setStatus] = useState<OAuthTokenStatusResponse | null>(null);
@@ -55,6 +71,8 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
const [refreshing, setRefreshing] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [expandedPlatform, setExpandedPlatform] = useState<string | null>(null);
const [platformLogs, setPlatformLogs] = useState<PlatformLogs>({});
const [hoveredLogId, setHoveredLogId] = useState<number | null>(null);
const fetchStatus = async () => {
if (!userId) return;
@@ -72,6 +90,48 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
}
};
const fetchPlatformLogs = async (platform: string) => {
if (!userId) return;
// Initialize platform logs state if not exists
if (!platformLogs[platform]) {
setPlatformLogs(prev => ({
...prev,
[platform]: { logs: [], loading: false, error: null }
}));
}
setPlatformLogs(prev => ({
...prev,
[platform]: { ...prev[platform], loading: true, error: null }
}));
try {
const response = await getOAuthTokenExecutionLogs(userId, platform, 10, 0); // Get latest 10 logs
if (response.success && response.data) {
setPlatformLogs(prev => ({
...prev,
[platform]: {
logs: response.data.logs || [],
loading: false,
error: null
}
}));
}
} catch (err: any) {
setPlatformLogs(prev => ({
...prev,
[platform]: {
...prev[platform],
loading: false,
error: err.message || 'Failed to fetch logs'
}
}));
console.error(`Error fetching logs for ${platform}:`, err);
}
};
useEffect(() => {
fetchStatus();
@@ -79,6 +139,13 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
const interval = setInterval(fetchStatus, 120000);
return () => clearInterval(interval);
}, [userId]);
// Fetch logs when platform is expanded
useEffect(() => {
if (expandedPlatform && userId) {
fetchPlatformLogs(expandedPlatform);
}
}, [expandedPlatform, userId]);
const handleRefresh = async (platform: string) => {
if (!userId) return;
@@ -91,6 +158,11 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
// Refresh status after manual refresh
await fetchStatus();
// Refresh logs if platform is expanded
if (expandedPlatform === platform) {
await fetchPlatformLogs(platform);
}
if (response.success) {
console.log(`Token refresh successful for ${platform}`);
} else {
@@ -103,6 +175,14 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
setRefreshing(null);
}
};
const handleExpandPlatform = (platform: string) => {
if (expandedPlatform === platform) {
setExpandedPlatform(null);
} else {
setExpandedPlatform(platform);
}
};
const getStatusIcon = (taskStatus: string | null, connected: boolean) => {
if (!connected) {
@@ -165,6 +245,39 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
};
return names[platform] || platform.toUpperCase();
};
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.token_status) {
return `Token: ${resultData.token_status}`;
}
if (resultData.platform) {
return `Platform: ${resultData.platform}`;
}
const str = JSON.stringify(resultData);
return str.length > 60 ? str.substring(0, 60) + '...' : str;
};
if (loading && !status) {
return (
@@ -231,6 +344,7 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
const platformStatus = status.data.platform_status[platform];
const task = platformStatus?.monitoring_task;
const isExpanded = expandedPlatform === platform;
const logs = platformLogs[platform];
return (
<React.Fragment key={platform}>
@@ -251,7 +365,47 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
</Box>
</TableCell>
<TableCell>
{getStatusChip(task?.status || null, platformStatus?.connected || false)}
<Box display="flex" alignItems="center" gap={1} flexWrap="wrap">
{getStatusChip(task?.status || null, platformStatus?.connected || false)}
{task?.last_success && (
<Tooltip title={`Last successful: ${formatDate(task.last_success)}`}>
<Chip
label={`${formatDate(task.last_success).split(',')[0].trim()}`}
size="small"
sx={{
backgroundColor: terminalColors.success + '40',
color: terminalColors.success,
fontFamily: 'monospace',
fontSize: '0.65rem',
height: '20px',
border: `1px solid ${terminalColors.success}40`,
'& .MuiChip-label': {
padding: '0 6px'
}
}}
/>
</Tooltip>
)}
{task?.next_check && (
<Tooltip title={`Next check: ${formatDate(task.next_check)}`}>
<Chip
label={`${formatDate(task.next_check).split(',')[0].trim()}`}
size="small"
sx={{
backgroundColor: terminalColors.info + '40',
color: terminalColors.info,
fontFamily: 'monospace',
fontSize: '0.65rem',
height: '20px',
border: `1px solid ${terminalColors.info}40`,
'& .MuiChip-label': {
padding: '0 6px'
}
}}
/>
</Tooltip>
)}
</Box>
</TableCell>
<TableCell>
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
@@ -263,7 +417,7 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
<Tooltip title={isExpanded ? "Hide details" : "Show details"}>
<IconButton
size="small"
onClick={() => setExpandedPlatform(isExpanded ? null : platform)}
onClick={() => handleExpandPlatform(platform)}
sx={{
color: terminalColors.primary,
'&:hover': {
@@ -318,20 +472,162 @@ const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) =
</TerminalTypography>
</TerminalAlert>
)}
{task?.last_success && (
<TerminalAlert severity="success" sx={{ mb: 1 }}>
<TerminalTypography variant="body2">
Last successful: {formatDate(task.last_success)}
{/* OAuth Monitoring Logs Section */}
{platformStatus?.connected && (
<>
<Divider sx={{ my: 1.5, borderColor: terminalColors.primary + '40' }} />
<TerminalTypography variant="subtitle2" fontWeight="bold" mb={1}>
🔐 Monitoring Logs
</TerminalTypography>
</TerminalAlert>
)}
{task?.next_check && (
<Box mt={1}>
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
Next check: {formatDate(task.next_check)}
</TerminalTypography>
</Box>
{logs?.loading ? (
<Box display="flex" alignItems="center" gap={1} p={1}>
<CircularProgress size={16} sx={{ color: terminalColors.primary }} />
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
Loading logs...
</TerminalTypography>
</Box>
) : logs?.error ? (
<TerminalAlert severity="error" sx={{ mb: 1 }}>
<TerminalTypography variant="caption">
{logs.error}
</TerminalTypography>
</TerminalAlert>
) : logs?.logs && logs.logs.length > 0 ? (
<Box sx={{
maxHeight: '300px',
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'rgba(0, 255, 0, 0.05)',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: terminalColors.primary + '80',
borderRadius: '4px',
'&:hover': {
backgroundColor: terminalColors.primary,
}
}
}}>
<Table size="small" sx={{
'& .MuiTableCell-root': {
color: terminalColors.primary,
borderColor: terminalColors.primary + '30',
fontSize: '0.7rem',
py: 0.5
}
}}>
<TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'rgba(0, 0, 0, 0.8)' }}>
<TableRow>
<TerminalTableCell>Date</TerminalTableCell>
<TerminalTableCell>Status</TerminalTableCell>
<TerminalTableCell>Result</TerminalTableCell>
<TerminalTableCell>Duration</TerminalTableCell>
</TableRow>
</TableHead>
<TableBody>
{logs.logs.map((log) => (
<React.Fragment key={log.id}>
<TerminalTableRow
onMouseEnter={() => setHoveredLogId(log.id)}
onMouseLeave={() => setHoveredLogId(null)}
sx={{
cursor: 'pointer',
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
}
}}
>
<TerminalTableCell>
<TerminalTypography variant="caption" fontSize="0.65rem">
{formatDate(log.execution_date)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
{getLogStatusChip(log.status)}
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="caption" fontSize="0.65rem" sx={{
fontFamily: 'monospace',
color: terminalColors.info,
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{formatLogResult(log.result_data)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="caption" fontSize="0.65rem">
{log.execution_time_ms ? `${log.execution_time_ms}ms` : 'N/A'}
</TerminalTypography>
</TerminalTableCell>
</TerminalTableRow>
{hoveredLogId === log.id && (
<TableRow>
<TableCell colSpan={4} sx={{ py: 1, backgroundColor: 'rgba(0, 255, 0, 0.08)', borderLeft: `3px solid ${terminalColors.primary}` }}>
<Box pl={2}>
<TerminalTypography variant="caption" fontWeight="bold" mb={0.5} display="block">
Full Details:
</TerminalTypography>
{log.error_message && (
<Box mb={1}>
<TerminalTypography variant="caption" fontWeight="bold" color={terminalColors.error} display="block" mb={0.5}>
Error:
</TerminalTypography>
<TerminalTypography variant="caption" fontSize="0.6rem" sx={{
fontFamily: 'monospace',
color: terminalColors.error,
wordBreak: 'break-word'
}}>
{log.error_message}
</TerminalTypography>
</Box>
)}
{log.result_data && (
<Box>
<TerminalTypography variant="caption" fontWeight="bold" color={terminalColors.info} display="block" mb={0.5}>
Result Data:
</TerminalTypography>
<TerminalTypography variant="caption" fontSize="0.6rem" sx={{
fontFamily: 'monospace',
color: terminalColors.info,
wordBreak: 'break-word',
whiteSpace: 'pre-wrap'
}}>
{typeof log.result_data === 'string' ? log.result_data : JSON.stringify(log.result_data, null, 2)}
</TerminalTypography>
</Box>
)}
</Box>
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
</Table>
{logs.logs.length >= 10 && (
<Box mt={1} textAlign="center">
<TerminalTypography variant="caption" color={terminalColors.textSecondary} sx={{ fontStyle: 'italic' }}>
Showing latest 10 logs. View all logs in OAuth Monitoring section.
</TerminalTypography>
</Box>
)}
</Box>
) : (
<TerminalTypography variant="caption" color={terminalColors.textSecondary} sx={{ fontStyle: 'italic' }}>
No monitoring logs available yet. Logs will appear after the first scheduled check.
</TerminalTypography>
)}
</>
)}
{/* Existing connection status messages */}
{!task && platformStatus?.connected && (
<TerminalAlert severity="info">
<TerminalTypography variant="body2">

View File

@@ -0,0 +1,560 @@
/**
* Platform Insights Status Component
* Compact terminal-themed component for displaying platform insights (GSC/Bing) 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,
} from '@mui/material';
import {
RefreshCw,
CheckCircle,
XCircle,
AlertTriangle,
Info,
ChevronDown,
ChevronUp,
Search,
Globe,
} from 'lucide-react';
import { useAuth } from '@clerk/clerk-react';
import {
getPlatformInsightsStatus,
getPlatformInsightsLogs,
PlatformInsightsStatusResponse,
PlatformInsightsTask,
PlatformInsightsExecutionLog,
PlatformInsightsLogsResponse,
} from '../../api/platformInsightsMonitoring';
import {
TerminalPaper,
TerminalTypography,
TerminalChip,
TerminalChipSuccess,
TerminalChipError,
TerminalChipWarning,
TerminalAlert,
TerminalTableCell,
TerminalTableRow,
terminalColors,
} from './terminalTheme';
interface PlatformInsightsStatusProps {
compact?: boolean;
}
interface TaskLogs {
[taskId: number]: {
logs: PlatformInsightsExecutionLog[];
loading: boolean;
error: string | null;
};
}
const PlatformInsightsStatus: React.FC<PlatformInsightsStatusProps> = ({ compact = true }) => {
const { userId } = useAuth();
const [status, setStatus] = useState<PlatformInsightsStatusResponse | null>(null);
const [loading, setLoading] = useState(true);
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 getPlatformInsightsStatus(userId);
setStatus(response);
} catch (err: any) {
setError(err.message || 'Failed to fetch platform insights status');
console.error('Error fetching platform insights 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: true, error: null }
}));
} else {
setTaskLogs(prev => ({
...prev,
[taskId]: { ...prev[taskId], loading: true, error: null }
}));
}
try {
console.log(`[PlatformInsights] Fetching logs for task ${taskId}...`);
const response = await getPlatformInsightsLogs(userId, 10, taskId);
console.log(`[PlatformInsights] Received logs response:`, {
success: response.success,
logsCount: response.logs?.length || 0,
totalCount: response.total_count,
hasLogs: !!(response.logs && response.logs.length > 0),
firstLog: response.logs?.[0] || null
});
if (response.success && response.logs && Array.isArray(response.logs)) {
setTaskLogs(prev => ({
...prev,
[taskId]: {
logs: response.logs,
loading: false,
error: null
}
}));
} else {
console.warn(`[PlatformInsights] Invalid logs response structure:`, response);
setTaskLogs(prev => ({
...prev,
[taskId]: {
logs: prev[taskId]?.logs || [],
loading: false,
error: response.success === false ? 'Failed to fetch logs' : 'Invalid response structure'
}
}));
}
} catch (err: any) {
console.error(`[PlatformInsights] Error fetching logs for task ${taskId}:`, err);
setTaskLogs(prev => ({
...prev,
[taskId]: {
logs: prev[taskId]?.logs || [],
loading: false,
error: err.message || 'Failed to fetch logs'
}
}));
}
};
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 only run weekly, 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 formatDate = (dateString: string | null) => {
if (!dateString) return 'Never';
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch {
return dateString;
}
};
const formatDuration = (ms: number | null) => {
if (!ms) return 'N/A';
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
};
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.info} />;
}
};
const getStatusChip = (status: string) => {
switch (status) {
case 'active':
return <TerminalChipSuccess label="Active" />;
case 'failed':
return <TerminalChipError label="Failed" />;
case 'paused':
return <TerminalChipWarning label="Paused" />;
default:
return <TerminalChip label={status} />;
}
};
const getPlatformIcon = (platform: string) => {
switch (platform) {
case 'gsc':
return <Search size={16} />;
case 'bing':
return <Globe size={16} />;
default:
return <Info size={16} />;
}
};
const getPlatformName = (platform: string) => {
switch (platform) {
case 'gsc':
return 'Google Search Console';
case 'bing':
return 'Bing Webmaster Tools';
default:
return platform.toUpperCase();
}
};
const allTasks = [
...(status?.gsc_tasks || []).map(t => ({ ...t, platform: 'gsc' as const })),
...(status?.bing_tasks || []).map(t => ({ ...t, platform: 'bing' as const }))
];
if (loading && !status) {
return (
<TerminalPaper>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
<CircularProgress size={20} sx={{ color: terminalColors.success }} />
<TerminalTypography>Loading platform insights tasks...</TerminalTypography>
</Box>
</TerminalPaper>
);
}
if (error) {
return (
<TerminalPaper>
<TerminalAlert severity="error" sx={{ m: 2 }}>
{error}
</TerminalAlert>
</TerminalPaper>
);
}
if (!status || allTasks.length === 0) {
return (
<TerminalPaper>
<Box sx={{ p: 3, textAlign: 'center' }}>
<TerminalTypography variant="body1" sx={{ mb: 1 }}>
No platform insights tasks found.
</TerminalTypography>
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
Connect GSC or Bing in onboarding Step 5 to create insights tasks.
</TerminalTypography>
</Box>
</TerminalPaper>
);
}
return (
<TerminalPaper>
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Search size={20} color={terminalColors.primary} />
<TerminalTypography variant="h6">
Platform Insights Tasks
</TerminalTypography>
<TerminalChip label={`${allTasks.length} tasks`} />
</Box>
<IconButton
onClick={fetchStatus}
disabled={loading}
sx={{
color: terminalColors.primary,
border: `1px solid ${terminalColors.border}`,
'&:hover': {
backgroundColor: terminalColors.backgroundHover,
}
}}
>
<RefreshCw size={16} />
</IconButton>
</Box>
<Divider sx={{ borderColor: terminalColors.border, mb: 2 }} />
<Table size="small">
<TableHead>
<TableRow>
<TerminalTableCell sx={{ width: '5%', fontSize: '0.75rem' }} />
<TerminalTableCell sx={{ width: '15%', fontSize: '0.75rem' }}>Platform</TerminalTableCell>
<TerminalTableCell sx={{ width: '30%', fontSize: '0.75rem' }}>Site URL</TerminalTableCell>
<TerminalTableCell sx={{ width: '15%', fontSize: '0.75rem' }}>Status</TerminalTableCell>
<TerminalTableCell sx={{ width: '35%', fontSize: '0.75rem' }}>Timing</TerminalTableCell>
</TableRow>
</TableHead>
<TableBody>
{allTasks.map((task) => {
const isExpanded = expandedTaskId === task.id;
const logs = taskLogs[task.id];
return (
<React.Fragment key={task.id}>
<TerminalTableRow
sx={{
cursor: 'pointer',
'&:hover': {
backgroundColor: terminalColors.backgroundHover,
}
}}
onClick={() => handleToggleExpand(task.id)}
>
<TerminalTableCell sx={{ width: '5%' }}>
<IconButton size="small" sx={{ color: terminalColors.primary }}>
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</IconButton>
</TerminalTableCell>
<TerminalTableCell sx={{ width: '15%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getPlatformIcon(task.platform)}
<Typography sx={{ fontFamily: 'inherit', color: terminalColors.text, fontSize: '0.875rem' }}>
{getPlatformName(task.platform)}
</Typography>
</Box>
</TerminalTableCell>
<TerminalTableCell sx={{ width: '30%' }}>
{task.site_url ? (
<Typography
sx={{
fontFamily: 'inherit',
color: terminalColors.textSecondary,
fontSize: '0.75rem',
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{task.site_url}
</Typography>
) : (
<Typography sx={{ fontFamily: 'inherit', color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Default site
</Typography>
)}
</TerminalTableCell>
<TerminalTableCell sx={{ width: '15%' }}>
{getStatusChip(task.status)}
</TerminalTableCell>
<TerminalTableCell sx={{ width: '35%' }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{task.last_success && (
<Tooltip title={`Last successful: ${formatDate(task.last_success)}`}>
<Chip
label="Success"
size="small"
sx={{
backgroundColor: terminalColors.background,
color: terminalColors.success,
border: `1px solid ${terminalColors.success}`,
fontSize: '0.65rem',
height: 20,
fontFamily: 'inherit'
}}
/>
</Tooltip>
)}
{task.next_check && (
<Tooltip title={`Next check: ${formatDate(task.next_check)}`}>
<Chip
label="Scheduled"
size="small"
sx={{
backgroundColor: terminalColors.background,
color: terminalColors.info,
border: `1px solid ${terminalColors.info}`,
fontSize: '0.65rem',
height: 20,
fontFamily: 'inherit'
}}
/>
</Tooltip>
)}
</Box>
</TerminalTableCell>
</TerminalTableRow>
<TableRow>
<TableCell colSpan={5} 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>
)}
<TerminalTypography variant="subtitle2" sx={{ mb: 1 }}>
Execution Logs
</TerminalTypography>
{logs?.loading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 2 }}>
<CircularProgress size={16} sx={{ color: terminalColors.success }} />
<TerminalTypography variant="body2">Loading logs...</TerminalTypography>
</Box>
) : logs?.error ? (
<TerminalAlert severity="error" sx={{ mb: 2 }}>
{logs.error}
</TerminalAlert>
) : logs?.logs && logs.logs.length > 0 ? (
<Box
sx={{
maxHeight: 300,
overflowY: 'auto',
border: `1px solid ${terminalColors.border}`,
borderRadius: 1,
}}
>
<Table size="small">
<TableHead>
<TableRow>
<TerminalTableCell>Date</TerminalTableCell>
<TerminalTableCell>Status</TerminalTableCell>
<TerminalTableCell>Source</TerminalTableCell>
<TerminalTableCell>Duration</TerminalTableCell>
</TableRow>
</TableHead>
<TableBody>
{logs.logs.map((log) => (
<React.Fragment key={log.id}>
<TerminalTableRow
sx={{
'&:hover': {
backgroundColor: terminalColors.backgroundHover,
},
cursor: 'pointer',
}}
onMouseEnter={() => setHoveredLogId(log.id)}
onMouseLeave={() => setHoveredLogId(null)}
>
<TerminalTableCell>
{formatDate(log.execution_date)}
</TerminalTableCell>
<TerminalTableCell>
{log.status === 'success' ? (
<TerminalChipSuccess label="Success" />
) : log.status === 'failed' ? (
<TerminalChipError label="Failed" />
) : (
<TerminalChip label={log.status} />
)}
</TerminalTableCell>
<TerminalTableCell>
<Chip
label={log.data_source || 'N/A'}
size="small"
sx={{
backgroundColor: terminalColors.background,
color: terminalColors.textSecondary,
border: `1px solid ${terminalColors.border}`,
fontSize: '0.65rem',
height: 18,
fontFamily: 'inherit'
}}
/>
</TerminalTableCell>
<TerminalTableCell>
{formatDuration(log.execution_time_ms)}
</TerminalTableCell>
</TerminalTableRow>
{hoveredLogId === log.id && (
<TableRow>
<TableCell colSpan={4} sx={{ py: 1, border: 0, backgroundColor: terminalColors.backgroundSecondary }}>
{log.error_message && (
<Box sx={{ mb: 1 }}>
<TerminalTypography variant="caption" sx={{ color: terminalColors.error, fontWeight: 'bold' }}>
Error:
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.text, ml: 1 }}>
{log.error_message}
</TerminalTypography>
</Box>
)}
{log.result_data && (
<Box>
<TerminalTypography variant="caption" sx={{ color: terminalColors.info, fontWeight: 'bold' }}>
Result:
</TerminalTypography>
<Box
component="pre"
sx={{
fontFamily: 'inherit',
fontSize: '0.7rem',
color: terminalColors.textSecondary,
backgroundColor: terminalColors.background,
p: 1,
borderRadius: 1,
overflow: 'auto',
maxHeight: 150,
mt: 0.5,
border: `1px solid ${terminalColors.border}`,
}}
>
{JSON.stringify(log.result_data, null, 2)}
</Box>
</Box>
)}
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
</Table>
</Box>
) : (
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary, p: 2 }}>
No execution logs yet.
</TerminalTypography>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
})}
</TableBody>
</Table>
</Box>
</TerminalPaper>
);
};
export default PlatformInsightsStatus;

View File

@@ -16,7 +16,8 @@ import {
Legend,
ResponsiveContainer
} from 'recharts';
import { Box, Paper, CircularProgress } from '@mui/material';
import { Box, Paper, CircularProgress, Modal, IconButton } from '@mui/material';
import { Close as CloseIcon, OpenInFull as MaximizeIcon } from '@mui/icons-material';
import { TerminalTypography, TerminalPaper, terminalColors } from './terminalTheme';
import { getSchedulerEventHistory, SchedulerEvent } from '../../api/schedulerDashboard';
@@ -25,10 +26,54 @@ interface SchedulerChartsProps {
events?: SchedulerEvent[];
}
interface ChartModalProps {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const ChartModal: React.FC<ChartModalProps> = ({ open, onClose, title, children }) => {
return (
<Modal
open={open}
onClose={onClose}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
}}
>
<TerminalPaper
sx={{
position: 'relative',
width: '90%',
maxWidth: '1200px',
maxHeight: '90vh',
overflow: 'auto',
p: 3,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<TerminalTypography variant="h5" sx={{ color: terminalColors.primary }}>
{title}
</TerminalTypography>
<IconButton onClick={onClose} sx={{ color: terminalColors.primary }}>
<CloseIcon />
</IconButton>
</Box>
{children}
</TerminalPaper>
</Modal>
);
};
const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents }) => {
const [events, setEvents] = useState<SchedulerEvent[]>(propEvents || []);
const [loading, setLoading] = useState(!propEvents);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState<string | null>(null);
// Fetch events if not provided as prop
useEffect(() => {
@@ -37,10 +82,10 @@ const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents })
try {
setLoading(true);
setError(null);
// Fetch all events for visualization (no pagination limit)
// Pass undefined to get all event types
// Fetch events for visualization (max 500 per backend API limit)
// Pass undefined to get all event types, use 30 days for charts
console.log('📊 Charts - Fetching event history...');
const response = await getSchedulerEventHistory(1000, 0, undefined);
const response = await getSchedulerEventHistory(500, 0, undefined, 30);
console.log('📊 Charts - Fetched events:', {
totalEvents: response.events?.length || 0,
totalCount: response.total_count,
@@ -216,58 +261,172 @@ const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents })
);
}
const handleChartClick = (chartId: string) => {
setModalOpen(chartId);
};
const handleModalClose = () => {
setModalOpen(null);
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Summary Stats */}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 2 }}>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.check_cycles}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Check Cycles
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.tasks_executed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Tasks Executed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.error, fontSize: '1.5rem' }}>
{totals.tasks_failed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Tasks Failed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.job_completed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Jobs Completed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.error, fontSize: '1.5rem' }}>
{totals.job_failed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Jobs Failed
</TerminalTypography>
</TerminalPaper>
<Box>
{/* Compact Charts in Single Row */}
<Box sx={{ display: 'flex', gap: 2, overflowX: 'auto', pb: 2 }}>
{/* Task Execution Trends - Compact */}
<Box
sx={{
flex: '0 0 300px',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
},
}}
onClick={() => handleChartClick('task-execution')}
>
<TerminalPaper sx={{ p: 2, position: 'relative' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, fontSize: '0.875rem' }}>
Task Execution Trends
</TerminalTypography>
<MaximizeIcon sx={{ fontSize: 16, color: terminalColors.primary }} />
</Box>
<ResponsiveContainer width="100%" height={150}>
<LineChart data={chartData.slice(-7)}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
dataKey="date"
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
interval="preserveStartEnd"
/>
<YAxis
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
width={30}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="tasks_executed"
stroke={terminalColors.success}
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="tasks_failed"
stroke={terminalColors.error}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</TerminalPaper>
</Box>
{/* Job Status Distribution - Compact */}
<Box
sx={{
flex: '0 0 300px',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
},
}}
onClick={() => handleChartClick('job-status')}
>
<TerminalPaper sx={{ p: 2, position: 'relative' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, fontSize: '0.875rem' }}>
Job Status Distribution
</TerminalTypography>
<MaximizeIcon sx={{ fontSize: 16, color: terminalColors.primary }} />
</Box>
<ResponsiveContainer width="100%" height={150}>
<BarChart data={chartData.slice(-7)}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
dataKey="date"
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
interval="preserveStartEnd"
/>
<YAxis
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
width={30}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="job_completed"
fill={terminalColors.success}
radius={[4, 4, 0, 0]}
/>
<Bar
dataKey="job_failed"
fill={terminalColors.error}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</TerminalPaper>
</Box>
{/* Check Cycles - Compact */}
<Box
sx={{
flex: '0 0 300px',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
},
}}
onClick={() => handleChartClick('check-cycles')}
>
<TerminalPaper sx={{ p: 2, position: 'relative' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, fontSize: '0.875rem' }}>
Check Cycles Over Time
</TerminalTypography>
<MaximizeIcon sx={{ fontSize: 16, color: terminalColors.primary }} />
</Box>
<ResponsiveContainer width="100%" height={150}>
<BarChart data={chartData.slice(-7)}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
dataKey="date"
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
interval="preserveStartEnd"
/>
<YAxis
stroke={terminalColors.primary}
tick={{ fill: terminalColors.primary, fontSize: 10 }}
width={30}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="check_cycles"
fill={terminalColors.primary}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</TerminalPaper>
</Box>
</Box>
{/* Task Execution Trends */}
<TerminalPaper sx={{ p: 3 }}>
<TerminalTypography variant="h6" sx={{ mb: 2, color: terminalColors.primary }}>
Task Execution Trends (Last 30 Days)
</TerminalTypography>
<ResponsiveContainer width="100%" height={300}>
{/* Modals for Expanded Charts */}
<ChartModal
open={modalOpen === 'task-execution'}
onClose={handleModalClose}
title="Task Execution Trends (Last 30 Days)"
>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
@@ -309,14 +468,14 @@ const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents })
/>
</LineChart>
</ResponsiveContainer>
</TerminalPaper>
</ChartModal>
{/* Job Status Distribution */}
<TerminalPaper sx={{ p: 3 }}>
<TerminalTypography variant="h6" sx={{ mb: 2, color: terminalColors.primary }}>
Job Status Distribution (Last 30 Days)
</TerminalTypography>
<ResponsiveContainer width="100%" height={300}>
<ChartModal
open={modalOpen === 'job-status'}
onClose={handleModalClose}
title="Job Status Distribution (Last 30 Days)"
>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
@@ -349,14 +508,14 @@ const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents })
/>
</BarChart>
</ResponsiveContainer>
</TerminalPaper>
</ChartModal>
{/* Check Cycles Over Time */}
<TerminalPaper sx={{ p: 3 }}>
<TerminalTypography variant="h6" sx={{ mb: 2, color: terminalColors.primary }}>
Check Cycles Over Time (Last 30 Days)
</TerminalTypography>
<ResponsiveContainer width="100%" height={300}>
<ChartModal
open={modalOpen === 'check-cycles'}
onClose={handleModalClose}
title="Check Cycles Over Time (Last 30 Days)"
>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
<XAxis
@@ -376,7 +535,51 @@ const SchedulerCharts: React.FC<SchedulerChartsProps> = ({ events: propEvents })
/>
</BarChart>
</ResponsiveContainer>
</TerminalPaper>
</ChartModal>
{/* Summary Stats - Compact */}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: 2, mt: 2 }}>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.check_cycles}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Check Cycles
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.tasks_executed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Tasks Executed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.error, fontSize: '1.5rem' }}>
{totals.tasks_failed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Tasks Failed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.primary, fontSize: '1.5rem' }}>
{totals.job_completed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Jobs Completed
</TerminalTypography>
</TerminalPaper>
<TerminalPaper sx={{ p: 2, textAlign: 'center' }}>
<TerminalTypography variant="h6" sx={{ color: terminalColors.error, fontSize: '1.5rem' }}>
{totals.job_failed}
</TerminalTypography>
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
Jobs Failed
</TerminalTypography>
</TerminalPaper>
</Box>
</Box>
);
};

View File

@@ -37,14 +37,16 @@ interface SchedulerEventHistoryProps {
limit?: number;
}
const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 50 }) => {
const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = () => {
const [events, setEvents] = useState<SchedulerEvent[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(limit);
const [isExpanded, setIsExpanded] = useState(false);
const [rowsPerPage, setRowsPerPage] = useState(5); // Start with 5, expand to 50 on hover
const [totalCount, setTotalCount] = useState(0);
const [eventTypeFilter, setEventTypeFilter] = useState<string>('all');
const [daysFilter, setDaysFilter] = useState<number>(7);
const fetchEvents = async () => {
try {
@@ -54,7 +56,8 @@ const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 5
const response = await getSchedulerEventHistory(
rowsPerPage,
page * rowsPerPage,
eventTypeFilter !== 'all' ? eventTypeFilter as any : undefined
eventTypeFilter !== 'all' ? eventTypeFilter as any : undefined,
daysFilter
);
setEvents(response.events);
@@ -70,7 +73,16 @@ const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 5
useEffect(() => {
fetchEvents();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, rowsPerPage, eventTypeFilter]); // fetchEvents is stable, no need to include
}, [page, rowsPerPage, eventTypeFilter, daysFilter]); // fetchEvents is stable, no need to include
// Expand to 50 rows on hover
const handleMouseEnter = () => {
if (!isExpanded) {
setIsExpanded(true);
setRowsPerPage(50);
setPage(0); // Reset to first page when expanding
}
};
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
@@ -169,40 +181,92 @@ const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 5
}
return (
<TerminalPaper>
<TerminalPaper
onMouseEnter={handleMouseEnter}
sx={{
cursor: isExpanded ? 'default' : 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: isExpanded ? undefined : '0 4px 8px rgba(0,0,0,0.2)',
}
}}
>
<Box p={2}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2} flexWrap="wrap" gap={2}>
<TerminalTypography variant="h6">
📜 Scheduler Event History
{!isExpanded && (
<Tooltip title="Hover to expand and see more events with pagination">
<Typography
component="span"
sx={{
fontSize: '0.7rem',
color: terminalColors.info,
ml: 1,
fontStyle: 'italic'
}}
>
(Hover to expand)
</Typography>
</Tooltip>
)}
</TerminalTypography>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel sx={{ color: terminalColors.primary }}>Event Type</InputLabel>
<Select
value={eventTypeFilter}
onChange={(e) => {
setEventTypeFilter(e.target.value);
setPage(0);
}}
sx={{
color: terminalColors.primary,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: terminalColors.primary,
},
'& .MuiSvgIcon-root': {
<Box display="flex" gap={2} flexWrap="wrap">
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel sx={{ color: terminalColors.primary }}>Days</InputLabel>
<Select
value={daysFilter}
onChange={(e) => {
setDaysFilter(e.target.value as number);
setPage(0);
}}
sx={{
color: terminalColors.primary,
}
}}
>
<MenuItem value="all">All Events</MenuItem>
<MenuItem value="check_cycle">Check Cycles</MenuItem>
<MenuItem value="interval_adjustment">Interval Adjustments</MenuItem>
<MenuItem value="start">Scheduler Start</MenuItem>
<MenuItem value="stop">Scheduler Stop</MenuItem>
<MenuItem value="job_scheduled">Job Scheduled</MenuItem>
<MenuItem value="job_completed">Job Completed</MenuItem>
<MenuItem value="job_failed">Job Failed</MenuItem>
</Select>
</FormControl>
'& .MuiOutlinedInput-notchedOutline': {
borderColor: terminalColors.primary,
},
'& .MuiSvgIcon-root': {
color: terminalColors.primary,
}
}}
>
<MenuItem value={1}>Last 1 day</MenuItem>
<MenuItem value={3}>Last 3 days</MenuItem>
<MenuItem value={7}>Last 7 days</MenuItem>
<MenuItem value={14}>Last 14 days</MenuItem>
<MenuItem value={30}>Last 30 days</MenuItem>
<MenuItem value={90}>Last 90 days</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel sx={{ color: terminalColors.primary }}>Event Type</InputLabel>
<Select
value={eventTypeFilter}
onChange={(e) => {
setEventTypeFilter(e.target.value);
setPage(0);
}}
sx={{
color: terminalColors.primary,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: terminalColors.primary,
},
'& .MuiSvgIcon-root': {
color: terminalColors.primary,
}
}}
>
<MenuItem value="all">All Events</MenuItem>
<MenuItem value="check_cycle">Check Cycles</MenuItem>
<MenuItem value="interval_adjustment">Interval Adjustments</MenuItem>
<MenuItem value="start">Scheduler Start</MenuItem>
<MenuItem value="stop">Scheduler Stop</MenuItem>
<MenuItem value="job_scheduled">Job Scheduled</MenuItem>
<MenuItem value="job_completed">Job Completed</MenuItem>
<MenuItem value="job_failed">Job Failed</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
{events.length === 0 ? (
@@ -284,24 +348,33 @@ const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 5
</Table>
</TableContainer>
<TablePagination
component="div"
count={totalCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100]}
sx={{
color: terminalColors.primary,
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
{isExpanded && (
<TablePagination
component="div"
count={totalCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100]}
sx={{
color: terminalColors.primary,
},
'& .MuiIconButton-root': {
color: terminalColors.primary,
}
}}
/>
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
color: terminalColors.primary,
},
'& .MuiIconButton-root': {
color: terminalColors.primary,
}
}}
/>
)}
{!isExpanded && totalCount > events.length && (
<Box p={2} textAlign="center">
<TerminalTypography variant="body2" sx={{ color: terminalColors.info, fontStyle: 'italic' }}>
Showing {events.length} of {totalCount} events. Hover to expand and see more with pagination.
</TerminalTypography>
</Box>
)}
</>
)}
</Box>

View File

@@ -3,7 +3,7 @@
* Displays scheduled jobs in tree structure matching log format.
*/
import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Box } from '@mui/material';
import {
Schedule as ScheduleIcon,
@@ -26,6 +26,34 @@ const SchedulerJobsTree: React.FC<SchedulerJobsTreeProps> = ({
recurringJobs,
oneTimeJobs
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const DEFAULT_DISPLAY_COUNT = 3; // Show only 3 jobs by default
const COLLAPSE_DELAY = 2000; // 2 seconds delay before collapsing
const handleMouseEnter = () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
setIsExpanded(true);
};
const handleMouseLeave = () => {
hoverTimeoutRef.current = setTimeout(() => {
setIsExpanded(false);
}, COLLAPSE_DELAY);
};
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
};
}, []);
const displayedJobs = isExpanded ? jobs : jobs.slice(0, DEFAULT_DISPLAY_COUNT);
const hasMoreJobs = jobs.length > DEFAULT_DISPLAY_COUNT;
const formatDate = (dateString: string | null) => {
if (!dateString) return 'Not scheduled';
try {
@@ -66,6 +94,26 @@ const SchedulerJobsTree: React.FC<SchedulerJobsTreeProps> = ({
};
return `OAuth ${platformNames[platform] || platform.toUpperCase()}`;
}
if (jobId.includes('website_analysis')) {
// Extract task type from job
const taskType = job?.task_type || 'Website';
const taskTypeNames: { [key: string]: string } = {
'user_website': 'User Website',
'competitor': 'Competitor'
};
return `Website Analysis - ${taskTypeNames[taskType] || taskType}`;
}
if (jobId.includes('platform_insights')) {
// Extract platform from job ID or use platform field
const platform = job?.platform ||
jobId.split('_')[2] ||
'Platform';
const platformNames: { [key: string]: string } = {
'gsc': 'GSC Insights',
'bing': 'Bing Insights'
};
return platformNames[platform] || `${platform.toUpperCase()} Insights`;
}
return 'One-Time';
};
@@ -93,19 +141,38 @@ const SchedulerJobsTree: React.FC<SchedulerJobsTreeProps> = ({
/>
</Box>
<Box sx={{ fontFamily: 'monospace', fontSize: '0.875rem', color: terminalColors.text, flex: 1, overflow: 'auto', minHeight: 0 }}>
<Box
sx={{
fontFamily: 'monospace',
fontSize: '0.875rem',
color: terminalColors.text,
flex: 1,
overflow: 'auto',
minHeight: 0
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Header */}
<Box mb={2} sx={{ flexShrink: 0 }}>
<TerminalTypography variant="body2" sx={{ mb: 1, color: terminalColors.textSecondary }}>
Recurring Jobs: {recurringJobs} | One-Time Jobs: {oneTimeJobs}
{hasMoreJobs && !isExpanded && (
<TerminalTypography
component="span"
sx={{ ml: 1, color: terminalColors.primary, fontStyle: 'italic', fontSize: '0.75rem' }}
>
(Hover to see all {jobs.length} jobs)
</TerminalTypography>
)}
</TerminalTypography>
</Box>
{/* Jobs Tree */}
{jobs.length > 0 ? (
{displayedJobs.length > 0 ? (
<Box sx={{ flex: 1 }}>
{jobs.map((job, index) => {
const isLast = index === jobs.length - 1;
{displayedJobs.map((job, index) => {
const isLast = index === displayedJobs.length - 1 && (!hasMoreJobs || isExpanded);
const prefix = isLast ? '└─' : '├─';
const isRecurring = job.id === 'check_due_tasks';

View File

@@ -0,0 +1,126 @@
/**
* Task Monitoring Tabs Component
* Organizes OAuth Token Status, Website Analysis Status, and Platform Insights in tabs
*/
import React, { useState } from 'react';
import { Box, Tabs, Tab } from '@mui/material';
import { styled } from '@mui/material/styles';
import OAuthTokenStatus from './OAuthTokenStatus';
import WebsiteAnalysisStatus from './WebsiteAnalysisStatus';
import PlatformInsightsStatus from './PlatformInsightsStatus';
import { TerminalPaper, terminalColors } from './terminalTheme';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`task-monitoring-tabpanel-${index}`}
aria-labelledby={`task-monitoring-tab-${index}`}
>
<Box sx={{ pt: 3, display: value === index ? 'block' : 'none' }}>{children}</Box>
</div>
);
};
// Terminal-themed button-like tab styling
const TerminalTab = styled(Tab)({
minHeight: 48,
padding: '8px 16px',
textTransform: 'none',
fontFamily: 'monospace',
fontSize: '0.875rem',
fontWeight: 400,
color: terminalColors.textSecondary,
backgroundColor: 'transparent',
border: `1px solid ${terminalColors.border}`,
borderBottom: 'none',
borderRadius: '4px 4px 0 0',
marginRight: '4px',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: terminalColors.backgroundHover,
color: terminalColors.primary,
borderColor: terminalColors.primary,
},
'&.Mui-selected': {
color: terminalColors.primary,
backgroundColor: terminalColors.background,
borderColor: terminalColors.primary,
fontWeight: 600,
},
'&:focus': {
outline: `2px solid ${terminalColors.primary}`,
outlineOffset: '-2px',
},
});
const TaskMonitoringTabs: React.FC = () => {
const [value, setValue] = useState(0);
const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
return (
<TerminalPaper sx={{ p: 0 }}>
<Box sx={{ borderBottom: 1, borderColor: terminalColors.border, px: 2, pt: 2 }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="task monitoring tabs"
sx={{
minHeight: 48,
'& .MuiTabs-indicator': {
display: 'none', // Hide default indicator, we use border styling instead
},
'& .MuiTabs-flexContainer': {
gap: 1,
},
}}
>
<TerminalTab
label="OAuth Token Status"
id="task-monitoring-tab-0"
aria-controls="task-monitoring-tabpanel-0"
/>
<TerminalTab
label="Website Analysis"
id="task-monitoring-tab-1"
aria-controls="task-monitoring-tabpanel-1"
/>
<TerminalTab
label="Platform Insights"
id="task-monitoring-tab-2"
aria-controls="task-monitoring-tabpanel-2"
/>
</Tabs>
</Box>
<TabPanel value={value} index={0}>
<Box sx={{ p: 2 }}>
<OAuthTokenStatus compact={true} />
</Box>
</TabPanel>
<TabPanel value={value} index={1}>
<Box sx={{ p: 2 }}>
<WebsiteAnalysisStatus compact={true} />
</Box>
</TabPanel>
<TabPanel value={value} index={2}>
<Box sx={{ p: 2 }}>
<PlatformInsightsStatus compact={true} />
</Box>
</TabPanel>
</TerminalPaper>
);
};
export default TaskMonitoringTabs;

View File

@@ -0,0 +1,607 @@
/**
* 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;

View File

@@ -180,6 +180,8 @@ export const terminalColors = {
success: '#00ff00',
background: '#0a0a0a',
backgroundLight: '#1a1a1a',
backgroundHover: 'rgba(0, 255, 0, 0.05)',
backgroundSecondary: 'rgba(0, 255, 0, 0.05)',
text: '#00ff00',
textSecondary: '#00ff88',
border: '#00ff00',