AI platform insights monitoring and website analysis monitoring services added
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user