Scheduled research persona generation
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
/**
|
||||
* Execution Logs Table Component
|
||||
* Displays task execution logs in a table with pagination and filtering.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Visibility as VisibilityIcon
|
||||
} from '@mui/icons-material';
|
||||
import { getExecutionLogs, getRecentSchedulerLogs, ExecutionLog, ExecutionLogsResponse } from '../../api/schedulerDashboard';
|
||||
import {
|
||||
TerminalPaper,
|
||||
TerminalTypography,
|
||||
TerminalChipSuccess,
|
||||
TerminalChipError,
|
||||
TerminalChipWarning,
|
||||
TerminalTableCell,
|
||||
TerminalTableRow,
|
||||
TerminalAlert,
|
||||
terminalColors
|
||||
} from './terminalTheme';
|
||||
|
||||
interface ExecutionLogsTableProps {
|
||||
initialLimit?: number;
|
||||
}
|
||||
|
||||
const ExecutionLogsTable: React.FC<ExecutionLogsTableProps> = ({ initialLimit = 50 }) => {
|
||||
const [logs, setLogs] = useState<ExecutionLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(initialLimit);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [statusFilter, setStatusFilter] = useState<'success' | 'failed' | 'running' | 'skipped' | 'all'>('all');
|
||||
const [isShowingSchedulerLogs, setIsShowingSchedulerLogs] = useState(false);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// First, try to fetch actual execution logs
|
||||
const response = await getExecutionLogs(
|
||||
rowsPerPage,
|
||||
page * rowsPerPage,
|
||||
statusFilter === 'all' ? undefined : statusFilter
|
||||
);
|
||||
|
||||
console.log('📋 Execution Logs Response:', JSON.stringify({
|
||||
logsCount: response.logs?.length || 0,
|
||||
totalCount: response.total_count,
|
||||
hasLogs: !!(response.logs && response.logs.length > 0),
|
||||
isSchedulerLogs: response.is_scheduler_logs,
|
||||
firstLog: response.logs?.[0] || null
|
||||
}, null, 2));
|
||||
|
||||
// If we have actual execution logs, use them
|
||||
if (response.logs && response.logs.length > 0 && !response.is_scheduler_logs) {
|
||||
console.log('✅ Using execution logs:', response.logs.length);
|
||||
setLogs(response.logs);
|
||||
setTotalCount(response.total_count || 0);
|
||||
setIsShowingSchedulerLogs(false);
|
||||
} else {
|
||||
// No execution logs available, fetch scheduler logs as fallback (latest 5 only)
|
||||
console.log('📋 No execution logs found, fetching latest scheduler logs...');
|
||||
try {
|
||||
const schedulerLogsResponse = await getRecentSchedulerLogs();
|
||||
console.log('📋 Scheduler Logs Response:', JSON.stringify({
|
||||
logsCount: schedulerLogsResponse.logs?.length || 0,
|
||||
totalCount: schedulerLogsResponse.total_count,
|
||||
isSchedulerLogs: schedulerLogsResponse.is_scheduler_logs,
|
||||
allLogs: schedulerLogsResponse.logs || []
|
||||
}, null, 2));
|
||||
|
||||
if (schedulerLogsResponse.logs && schedulerLogsResponse.logs.length > 0) {
|
||||
console.log('✅ Setting scheduler logs:', schedulerLogsResponse.logs.length, 'logs');
|
||||
setLogs(schedulerLogsResponse.logs);
|
||||
setTotalCount(schedulerLogsResponse.total_count || 0);
|
||||
setIsShowingSchedulerLogs(true);
|
||||
} else {
|
||||
console.warn('⚠️ Scheduler logs response is empty');
|
||||
setLogs([]);
|
||||
setTotalCount(0);
|
||||
setIsShowingSchedulerLogs(false);
|
||||
}
|
||||
} catch (schedulerErr: any) {
|
||||
console.error('❌ Error fetching scheduler logs:', schedulerErr);
|
||||
setLogs([]);
|
||||
setTotalCount(0);
|
||||
setIsShowingSchedulerLogs(false);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch execution logs');
|
||||
console.error('❌ Error fetching execution logs:', err);
|
||||
|
||||
// Try to fetch scheduler logs as fallback even on error (latest 5 only)
|
||||
try {
|
||||
const schedulerLogsResponse = await getRecentSchedulerLogs();
|
||||
setLogs(schedulerLogsResponse.logs || []);
|
||||
setTotalCount(schedulerLogsResponse.total_count || 0);
|
||||
setIsShowingSchedulerLogs(true);
|
||||
} catch (schedulerErr: any) {
|
||||
console.error('❌ Error fetching scheduler logs:', schedulerErr);
|
||||
setLogs([]);
|
||||
setTotalCount(0);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, rowsPerPage, statusFilter]); // fetchLogs is stable, no need to include
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircleIcon fontSize="small" color="success" />;
|
||||
case 'failed':
|
||||
return <ErrorIcon fontSize="small" color="error" />;
|
||||
case 'running':
|
||||
return <ScheduleIcon fontSize="small" color="primary" />;
|
||||
default:
|
||||
return <ScheduleIcon fontSize="small" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string): "success" | "error" | "warning" | "default" => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'error';
|
||||
case 'running':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const formatExecutionTime = (ms: number | null) => {
|
||||
if (!ms) return 'N/A';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ScheduleIcon sx={{ color: terminalColors.primary }} />
|
||||
<TerminalTypography variant="h6" component="h2" sx={{ fontSize: '1.25rem', fontWeight: 'bold' }}>
|
||||
Execution Logs
|
||||
</TerminalTypography>
|
||||
{isShowingSchedulerLogs && (
|
||||
<TerminalChipWarning
|
||||
label="Showing Scheduler Logs"
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 120,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: terminalColors.primary,
|
||||
'& fieldset': {
|
||||
borderColor: terminalColors.primary,
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: terminalColors.secondary,
|
||||
},
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: terminalColors.textSecondary,
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: terminalColors.primary,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
label="Status"
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value as any);
|
||||
setPage(0);
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
border: `1px solid ${terminalColors.primary}`,
|
||||
'& .MuiMenuItem-root': {
|
||||
color: terminalColors.primary,
|
||||
fontFamily: 'monospace',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.15)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
<MenuItem value="success">Success</MenuItem>
|
||||
<MenuItem value="failed">Failed</MenuItem>
|
||||
<MenuItem value="running">Running</MenuItem>
|
||||
<MenuItem value="skipped">Skipped</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Tooltip title="Refresh logs">
|
||||
<IconButton
|
||||
onClick={fetchLogs}
|
||||
size="small"
|
||||
sx={{
|
||||
color: terminalColors.primary,
|
||||
border: `1px solid ${terminalColors.primary}`,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<TerminalAlert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</TerminalAlert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" p={3}>
|
||||
<CircularProgress sx={{ color: terminalColors.primary }} />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{isShowingSchedulerLogs && (
|
||||
<TerminalAlert severity="info" sx={{ mb: 2 }}>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.875rem' }}>
|
||||
Showing latest 5 scheduler activity logs (job scheduling, completion, failures).
|
||||
Historical execution logs are available in the Event History section below.
|
||||
</TerminalTypography>
|
||||
</TerminalAlert>
|
||||
)}
|
||||
<TableContainer
|
||||
sx={{
|
||||
backgroundColor: terminalColors.background,
|
||||
maxHeight: '600px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<Table size="small" sx={{ minWidth: 650 }}>
|
||||
<TableHead>
|
||||
<TerminalTableRow>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Task</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Status</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Execution Time</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Duration</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>User ID</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Date</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Error</TerminalTableCell>
|
||||
</TerminalTableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(() => {
|
||||
// Debug logging
|
||||
if (logs.length > 0) {
|
||||
console.log('🔍 Rendering logs table:', {
|
||||
logsCount: logs.length,
|
||||
loading,
|
||||
isShowingSchedulerLogs,
|
||||
firstLogId: logs[0]?.id,
|
||||
firstLogStatus: logs[0]?.status
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{logs.length === 0 && !loading ? (
|
||||
<TerminalTableRow>
|
||||
<TerminalTableCell colSpan={7} align="center">
|
||||
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||||
<ScheduleIcon sx={{ color: terminalColors.textSecondary, fontSize: 48, mb: 2, opacity: 0.5 }} />
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, mb: 1, fontWeight: 'bold' }}>
|
||||
{isShowingSchedulerLogs ? 'No Scheduler Logs Yet' : 'No Execution Logs Yet'}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary, mb: 1 }}>
|
||||
{isShowingSchedulerLogs
|
||||
? 'Scheduler activity logs (job scheduling, restoration, etc.) will appear here when the scheduler starts or schedules jobs.'
|
||||
: 'Execution logs will appear here once the scheduler runs and executes tasks.'}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem', fontStyle: 'italic', display: 'block' }}>
|
||||
{isShowingSchedulerLogs
|
||||
? 'These logs show scheduler activity (job restoration, scheduling) when actual task execution logs are not available.'
|
||||
: 'The scheduler checks for due tasks every 60 minutes (or based on active strategies).'}
|
||||
{!isShowingSchedulerLogs && totalCount === 0 && ' Currently, no tasks have been executed yet.'}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</TerminalTableCell>
|
||||
</TerminalTableRow>
|
||||
) : loading ? (
|
||||
<TerminalTableRow>
|
||||
<TerminalTableCell colSpan={7} align="center">
|
||||
<Box sx={{ py: 3, display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 2 }}>
|
||||
<CircularProgress size={24} sx={{ color: terminalColors.primary }} />
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
||||
Loading execution logs...
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</TerminalTableCell>
|
||||
</TerminalTableRow>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
// Debug: log each row being rendered
|
||||
if (log.id === logs[0]?.id) {
|
||||
console.log('🎯 Rendering first log row:', log.id, log.status, log.task?.task_title);
|
||||
}
|
||||
return (
|
||||
<TerminalTableRow
|
||||
key={log.id}
|
||||
sx={{
|
||||
backgroundColor: terminalColors.background,
|
||||
color: terminalColors.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
<Box>
|
||||
<TerminalTypography variant="body2" fontWeight="medium" sx={{ fontSize: '0.875rem' }}>
|
||||
{log.is_scheduler_log
|
||||
? (log.task?.task_title || `Scheduler Event: ${log.event_type || 'unknown'}`)
|
||||
: (log.task?.task_title || `Task #${log.task_id}`)
|
||||
}
|
||||
</TerminalTypography>
|
||||
{log.is_scheduler_log && log.job_id && (
|
||||
<TerminalTypography variant="caption" sx={{ fontSize: '0.7rem', color: terminalColors.textSecondary, display: 'block', mt: 0.5 }}>
|
||||
Job ID: {log.job_id}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
{!log.is_scheduler_log && log.task?.component_name && (
|
||||
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
{log.task.component_name}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
{log.is_scheduler_log && log.task?.metric && (
|
||||
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
Function: {log.task.metric}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</Box>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
{log.status === 'success' ? (
|
||||
<TerminalChipSuccess
|
||||
icon={getStatusIcon(log.status)}
|
||||
label={log.status}
|
||||
size="small"
|
||||
/>
|
||||
) : log.status === 'failed' ? (
|
||||
<TerminalChipError
|
||||
icon={getStatusIcon(log.status)}
|
||||
label={log.status}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<TerminalChipWarning
|
||||
icon={getStatusIcon(log.status)}
|
||||
label={log.status}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.875rem', color: terminalColors.primary }}>
|
||||
{formatExecutionTime(log.execution_time_ms)}
|
||||
</TerminalTypography>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.875rem', color: terminalColors.primary }}>
|
||||
{log.execution_date ? formatDate(log.execution_date) : 'N/A'}
|
||||
</TerminalTypography>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
{log.user_id ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '0.875rem', color: terminalColors.primary }}>
|
||||
{String(log.user_id).substring(0, 12)}...
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }}>
|
||||
System
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.875rem', color: terminalColors.primary }}>
|
||||
{formatDate(log.created_at)}
|
||||
</TerminalTypography>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ color: terminalColors.primary }}>
|
||||
{log.error_message ? (
|
||||
<Tooltip title={log.error_message} arrow>
|
||||
<TerminalTypography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: '0.875rem',
|
||||
color: terminalColors.error,
|
||||
maxWidth: 300,
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
cursor: 'help'
|
||||
}}
|
||||
>
|
||||
{log.error_message}
|
||||
</TerminalTypography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }}>
|
||||
-
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
</TerminalTableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Only show pagination for actual execution logs, not scheduler logs */}
|
||||
{!isShowingSchedulerLogs && logs.length > 0 && (
|
||||
<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': {
|
||||
color: terminalColors.textSecondary,
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
'& .MuiTablePagination-select': {
|
||||
color: terminalColors.primary,
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
'& .MuiIconButton-root': {
|
||||
color: terminalColors.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
}
|
||||
},
|
||||
'& .MuiIconButton-root.Mui-disabled': {
|
||||
color: terminalColors.textSecondary,
|
||||
opacity: 0.3,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Info message for scheduler logs */}
|
||||
{isShowingSchedulerLogs && logs.length > 0 && (
|
||||
<Box mt={2}>
|
||||
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem', fontStyle: 'italic' }}>
|
||||
Displaying latest 5 scheduler activity logs. Only the most recent logs are shown here.
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TerminalPaper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecutionLogsTable;
|
||||
|
||||
297
frontend/src/components/SchedulerDashboard/FailuresInsights.tsx
Normal file
297
frontend/src/components/SchedulerDashboard/FailuresInsights.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Failures & Insights Component
|
||||
* Displays recent failures, error messages, and scheduler insights.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Divider,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Error as ErrorIcon,
|
||||
Warning as WarningIcon,
|
||||
Info as InfoIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { getExecutionLogs, getRecentSchedulerLogs, ExecutionLog } from '../../api/schedulerDashboard';
|
||||
import { SchedulerStats } from '../../api/schedulerDashboard';
|
||||
import {
|
||||
TerminalPaper,
|
||||
TerminalTypography,
|
||||
TerminalAlert,
|
||||
TerminalAccordion,
|
||||
terminalColors
|
||||
} from './terminalTheme';
|
||||
|
||||
interface FailuresInsightsProps {
|
||||
stats: SchedulerStats;
|
||||
}
|
||||
|
||||
const FailuresInsights: React.FC<FailuresInsightsProps> = ({ stats }) => {
|
||||
const [recentFailures, setRecentFailures] = useState<ExecutionLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFailures = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// First try to get execution logs with failed status
|
||||
const executionLogsResponse = await getExecutionLogs(10, 0, 'failed');
|
||||
|
||||
// Also get scheduler logs (which include job_failed events)
|
||||
const schedulerLogsResponse = await getRecentSchedulerLogs();
|
||||
|
||||
// Combine both, filtering for failed status
|
||||
const allFailures: ExecutionLog[] = [
|
||||
...executionLogsResponse.logs.filter(log => log.status === 'failed'),
|
||||
...(schedulerLogsResponse.logs || []).filter(log => log.status === 'failed')
|
||||
];
|
||||
|
||||
// Sort by execution_date descending (most recent first) and limit to 10
|
||||
allFailures.sort((a, b) => {
|
||||
const dateA = new Date(a.execution_date).getTime();
|
||||
const dateB = new Date(b.execution_date).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
setRecentFailures(allFailures.slice(0, 10));
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch failures');
|
||||
console.error('Error fetching failures:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFailures();
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// Generate insights based on stats
|
||||
const generateInsights = () => {
|
||||
const insights: Array<{ type: 'info' | 'warning' | 'error' | 'success'; message: string }> = [];
|
||||
|
||||
// Scheduler status insight
|
||||
if (!stats.running) {
|
||||
insights.push({
|
||||
type: 'error',
|
||||
message: 'Scheduler is stopped. Tasks will not be executed until scheduler is restarted.'
|
||||
});
|
||||
} else {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: 'Scheduler is running and processing tasks normally.'
|
||||
});
|
||||
}
|
||||
|
||||
// Active strategies insight
|
||||
if (stats.active_strategies_count === 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `No active strategies detected. Using ${stats.max_check_interval_minutes}min check interval (idle mode).`
|
||||
});
|
||||
} else {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `${stats.active_strategies_count} active strategy(ies) with monitoring tasks. Using ${stats.min_check_interval_minutes}min check interval.`
|
||||
});
|
||||
}
|
||||
|
||||
// Failure rate insight
|
||||
const totalExecutions = stats.tasks_executed + stats.tasks_failed;
|
||||
if (totalExecutions > 0) {
|
||||
const failureRate = (stats.tasks_failed / totalExecutions) * 100;
|
||||
if (failureRate > 20) {
|
||||
insights.push({
|
||||
type: 'error',
|
||||
message: `High failure rate: ${failureRate.toFixed(1)}% of tasks are failing. Review error logs for details.`
|
||||
});
|
||||
} else if (failureRate > 10) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `Moderate failure rate: ${failureRate.toFixed(1)}% of tasks are failing. Monitor for patterns.`
|
||||
});
|
||||
} else if (stats.tasks_failed > 0) {
|
||||
insights.push({
|
||||
type: 'info',
|
||||
message: `Low failure rate: ${failureRate.toFixed(1)}% of tasks are failing. System is healthy.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check interval insight
|
||||
if (stats.intelligent_scheduling) {
|
||||
insights.push({
|
||||
type: 'success',
|
||||
message: `Intelligent scheduling enabled. Interval automatically adjusts based on active strategies (${stats.min_check_interval_minutes}-${stats.max_check_interval_minutes}min range).`
|
||||
});
|
||||
}
|
||||
|
||||
// Last check insight
|
||||
if (stats.last_check) {
|
||||
try {
|
||||
const lastCheck = new Date(stats.last_check);
|
||||
const now = new Date();
|
||||
const diffMins = Math.floor((now.getTime() - lastCheck.getTime()) / 60000);
|
||||
|
||||
if (diffMins > stats.check_interval_minutes * 2) {
|
||||
insights.push({
|
||||
type: 'warning',
|
||||
message: `Last check was ${diffMins} minutes ago. Expected interval is ${stats.check_interval_minutes} minutes. Scheduler may be delayed.`
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore date parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
return insights;
|
||||
};
|
||||
|
||||
const insights = generateInsights();
|
||||
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
||||
<InfoIcon sx={{ color: terminalColors.primary }} />
|
||||
<TerminalTypography variant="h6" component="h2" sx={{ fontSize: '1.25rem', fontWeight: 'bold' }}>
|
||||
Failures & Insights
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
|
||||
{/* Recent Failures */}
|
||||
<Box mb={3} sx={{ flexShrink: 0 }}>
|
||||
<TerminalTypography variant="subtitle1" fontWeight="medium" mb={1} sx={{ fontSize: '1rem', color: terminalColors.textSecondary }}>
|
||||
Recent Failures ({recentFailures.length})
|
||||
</TerminalTypography>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" p={2}>
|
||||
<CircularProgress size={24} sx={{ color: terminalColors.primary }} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<TerminalAlert severity="error">{error}</TerminalAlert>
|
||||
) : recentFailures.length === 0 ? (
|
||||
<TerminalAlert severity="success" icon={<CheckCircleIcon />}>
|
||||
No recent failures. All tasks are executing successfully.
|
||||
</TerminalAlert>
|
||||
) : (
|
||||
<List>
|
||||
{recentFailures.map((log, index) => (
|
||||
<React.Fragment key={log.id}>
|
||||
<TerminalAccordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ color: terminalColors.primary }} />}
|
||||
sx={{
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1} width="100%">
|
||||
<ErrorIcon sx={{ color: terminalColors.error }} fontSize="small" />
|
||||
<TerminalTypography variant="body2" sx={{ flexGrow: 1, fontSize: '0.875rem' }}>
|
||||
{log.task?.task_title || `Task #${log.task_id}`}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="caption" sx={{ color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
{formatDate(log.execution_date)}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ backgroundColor: terminalColors.background }}>
|
||||
<Box>
|
||||
<TerminalTypography variant="body2" gutterBottom sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }}>
|
||||
<strong style={{ color: terminalColors.primary }}>Component:</strong> {log.task?.component_name || 'Unknown'}
|
||||
</TerminalTypography>
|
||||
{log.error_message && (
|
||||
<Box sx={{ mt: 1, p: 1, border: `1px solid ${terminalColors.error}`, borderRadius: 1, backgroundColor: terminalColors.backgroundLight }}>
|
||||
<TerminalTypography variant="body2" fontWeight="bold" gutterBottom sx={{ color: terminalColors.error, fontSize: '0.875rem' }}>
|
||||
Error Message
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.error, fontSize: '0.875rem', fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
|
||||
{log.error_message}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
{log.execution_time_ms && (
|
||||
<TerminalTypography variant="caption" sx={{ mt: 1, display: 'block', color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
Execution time: {log.execution_time_ms}ms
|
||||
</TerminalTypography>
|
||||
)}
|
||||
{log.user_id && (
|
||||
<TerminalTypography variant="caption" sx={{ mt: 0.5, display: 'block', color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
User ID: {log.user_id}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</TerminalAccordion>
|
||||
{index < recentFailures.length - 1 && <Divider sx={{ borderColor: terminalColors.border }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3, borderColor: terminalColors.border, flexShrink: 0 }} />
|
||||
|
||||
{/* Scheduler Insights */}
|
||||
<Box sx={{ flex: 1, overflow: 'auto', minHeight: 0, flexShrink: 1 }}>
|
||||
<TerminalTypography variant="subtitle1" fontWeight="medium" mb={1} sx={{ fontSize: '1rem', color: terminalColors.textSecondary }}>
|
||||
Scheduler Insights
|
||||
</TerminalTypography>
|
||||
<List>
|
||||
{insights.map((insight, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
{insight.type === 'error' && <ErrorIcon sx={{ color: terminalColors.error }} />}
|
||||
{insight.type === 'warning' && <WarningIcon sx={{ color: terminalColors.warning }} />}
|
||||
{insight.type === 'info' && <InfoIcon sx={{ color: terminalColors.info }} />}
|
||||
{insight.type === 'success' && <CheckCircleIcon sx={{ color: terminalColors.success }} />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<TerminalTypography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: '0.875rem',
|
||||
color: insight.type === 'error' ? terminalColors.error : terminalColors.text
|
||||
}}
|
||||
>
|
||||
{insight.message}
|
||||
</TerminalTypography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < insights.length - 1 && <Divider component="li" sx={{ borderColor: terminalColors.border }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FailuresInsights;
|
||||
|
||||
364
frontend/src/components/SchedulerDashboard/OAuthTokenStatus.tsx
Normal file
364
frontend/src/components/SchedulerDashboard/OAuthTokenStatus.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* OAuth Token Status Component
|
||||
* Compact terminal-themed component for displaying OAuth token monitoring status
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import {
|
||||
getOAuthTokenStatus,
|
||||
manualRefreshToken,
|
||||
OAuthTokenStatusResponse,
|
||||
ManualRefreshResponse,
|
||||
} from '../../api/oauthTokenMonitoring';
|
||||
import {
|
||||
TerminalPaper,
|
||||
TerminalTypography,
|
||||
TerminalChip,
|
||||
TerminalChipSuccess,
|
||||
TerminalChipError,
|
||||
TerminalChipWarning,
|
||||
TerminalAlert,
|
||||
terminalColors,
|
||||
} from './terminalTheme';
|
||||
|
||||
interface OAuthTokenStatusProps {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const OAuthTokenStatus: React.FC<OAuthTokenStatusProps> = ({ compact = true }) => {
|
||||
const { userId } = useAuth();
|
||||
const [status, setStatus] = useState<OAuthTokenStatusResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedPlatform, setExpandedPlatform] = useState<string | null>(null);
|
||||
|
||||
const fetchStatus = async () => {
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await getOAuthTokenStatus(userId);
|
||||
setStatus(response);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch token status');
|
||||
console.error('Error fetching OAuth token status:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
|
||||
// Poll for status updates every 2 minutes
|
||||
const interval = setInterval(fetchStatus, 120000);
|
||||
return () => clearInterval(interval);
|
||||
}, [userId]);
|
||||
|
||||
const handleRefresh = async (platform: string) => {
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
setRefreshing(platform);
|
||||
setError(null);
|
||||
const response: ManualRefreshResponse = await manualRefreshToken(userId, platform);
|
||||
|
||||
// Refresh status after manual refresh
|
||||
await fetchStatus();
|
||||
|
||||
if (response.success) {
|
||||
console.log(`Token refresh successful for ${platform}`);
|
||||
} else {
|
||||
console.error(`Token refresh failed for ${platform}:`, response.data.execution_result.error_message);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || `Failed to refresh ${platform} token`);
|
||||
console.error(`Error refreshing ${platform} token:`, err);
|
||||
} finally {
|
||||
setRefreshing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (taskStatus: string | null, connected: boolean) => {
|
||||
if (!connected) {
|
||||
return <XCircle size={16} color={terminalColors.error} />;
|
||||
}
|
||||
|
||||
if (!taskStatus || taskStatus === 'not_created') {
|
||||
return <Info size={16} color={terminalColors.info} />;
|
||||
}
|
||||
|
||||
switch (taskStatus) {
|
||||
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 | null, connected: boolean) => {
|
||||
if (!connected) {
|
||||
return <TerminalChipError label="Not Connected" size="small" />;
|
||||
}
|
||||
|
||||
if (!taskStatus || taskStatus === 'not_created') {
|
||||
return <TerminalChip label={taskStatus || 'Not Created'} size="small" />;
|
||||
}
|
||||
|
||||
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 getPlatformDisplayName = (platform: string) => {
|
||||
const names: { [key: string]: string } = {
|
||||
gsc: 'GSC',
|
||||
bing: 'Bing',
|
||||
wordpress: 'WP',
|
||||
wix: 'Wix',
|
||||
};
|
||||
return names[platform] || platform.toUpperCase();
|
||||
};
|
||||
|
||||
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 platforms = ['gsc', 'bing', 'wordpress', 'wix'];
|
||||
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<TerminalTypography variant="h6" component="h3">
|
||||
OAuth Token Status
|
||||
</TerminalTypography>
|
||||
<Tooltip title="Refresh status">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={fetchStatus}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: terminalColors.primary,
|
||||
border: `1px solid ${terminalColors.primary}`,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
},
|
||||
'&:disabled': {
|
||||
color: '#004400',
|
||||
borderColor: '#004400',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<TerminalAlert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
||||
{error}
|
||||
</TerminalAlert>
|
||||
)}
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
||||
<Table size="small" sx={{ '& .MuiTableCell-root': { color: terminalColors.primary, borderColor: terminalColors.primary + '40' } }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Platform</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Last Check</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{platforms.map((platform) => {
|
||||
const platformStatus = status.data.platform_status[platform];
|
||||
const task = platformStatus?.monitoring_task;
|
||||
const isExpanded = expandedPlatform === platform;
|
||||
|
||||
return (
|
||||
<React.Fragment key={platform}>
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{getStatusIcon(task?.status || null, platformStatus?.connected || false)}
|
||||
<TerminalTypography variant="body2" fontWeight="medium">
|
||||
{getPlatformDisplayName(platform)}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusChip(task?.status || null, platformStatus?.connected || false)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
||||
{formatDate(task?.last_check || null)}
|
||||
</TerminalTypography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box display="flex" gap={0.5} justifyContent="flex-end">
|
||||
<Tooltip title={isExpanded ? "Hide details" : "Show details"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setExpandedPlatform(isExpanded ? null : platform)}
|
||||
sx={{
|
||||
color: terminalColors.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{platformStatus?.connected && (
|
||||
<Tooltip title="Manually refresh token">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRefresh(platform)}
|
||||
disabled={refreshing === platform}
|
||||
sx={{
|
||||
color: terminalColors.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||
},
|
||||
'&:disabled': {
|
||||
color: '#004400',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{refreshing === platform ? (
|
||||
<CircularProgress size={14} sx={{ color: terminalColors.primary }} />
|
||||
) : (
|
||||
<RefreshCw size={14} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} sx={{ py: 0, border: 0 }}>
|
||||
<Collapse in={isExpanded}>
|
||||
<Box p={2} sx={{ backgroundColor: 'rgba(0, 255, 0, 0.05)', borderLeft: `2px solid ${terminalColors.primary}` }}>
|
||||
{task?.failure_reason && (
|
||||
<TerminalAlert severity="error" sx={{ mb: 1 }}>
|
||||
<TerminalTypography variant="body2" fontWeight="bold">
|
||||
Last Failure:
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2">
|
||||
{task.failure_reason}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
||||
{formatDate(task.last_failure || null)}
|
||||
</TerminalTypography>
|
||||
</TerminalAlert>
|
||||
)}
|
||||
{task?.last_success && (
|
||||
<TerminalAlert severity="success" sx={{ mb: 1 }}>
|
||||
<TerminalTypography variant="body2">
|
||||
Last successful: {formatDate(task.last_success)}
|
||||
</TerminalTypography>
|
||||
</TerminalAlert>
|
||||
)}
|
||||
{task?.next_check && (
|
||||
<Box mt={1}>
|
||||
<TerminalTypography variant="caption" color={terminalColors.textSecondary}>
|
||||
Next check: {formatDate(task.next_check)}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
{!task && platformStatus?.connected && (
|
||||
<TerminalAlert severity="info">
|
||||
<TerminalTypography variant="body2">
|
||||
Connected but no monitoring task. Create one manually or wait for onboarding completion.
|
||||
</TerminalTypography>
|
||||
</TerminalAlert>
|
||||
)}
|
||||
{!platformStatus?.connected && (
|
||||
<TerminalAlert severity="warning">
|
||||
<TerminalTypography variant="body2">
|
||||
Not connected. Connect in onboarding step 5.
|
||||
</TerminalTypography>
|
||||
</TerminalAlert>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthTokenStatus;
|
||||
|
||||
385
frontend/src/components/SchedulerDashboard/SchedulerCharts.tsx
Normal file
385
frontend/src/components/SchedulerDashboard/SchedulerCharts.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Scheduler Charts Component
|
||||
* Visualizes scheduler event history data using Recharts
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer
|
||||
} from 'recharts';
|
||||
import { Box, Paper, CircularProgress } from '@mui/material';
|
||||
import { TerminalTypography, TerminalPaper, terminalColors } from './terminalTheme';
|
||||
import { getSchedulerEventHistory, SchedulerEvent } from '../../api/schedulerDashboard';
|
||||
|
||||
interface SchedulerChartsProps {
|
||||
// Optional: can receive events as prop or fetch them internally
|
||||
events?: SchedulerEvent[];
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Fetch events if not provided as prop
|
||||
useEffect(() => {
|
||||
if (!propEvents) {
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
// Fetch all events for visualization (no pagination limit)
|
||||
// Pass undefined to get all event types
|
||||
console.log('📊 Charts - Fetching event history...');
|
||||
const response = await getSchedulerEventHistory(1000, 0, undefined);
|
||||
console.log('📊 Charts - Fetched events:', {
|
||||
totalEvents: response.events?.length || 0,
|
||||
totalCount: response.total_count,
|
||||
hasEvents: !!(response.events && response.events.length > 0),
|
||||
sampleEvent: response.events?.[0]
|
||||
});
|
||||
setEvents(response.events || []);
|
||||
} catch (err: any) {
|
||||
console.error('❌ Charts - Error fetching events:', err);
|
||||
console.error('❌ Charts - Error details:', {
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
responseData: err?.response?.data,
|
||||
stack: err?.stack
|
||||
});
|
||||
const errorMessage = err?.response?.data?.detail || err?.response?.data?.message || err?.message || String(err) || 'Failed to fetch event history';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchEvents();
|
||||
}
|
||||
}, [propEvents]);
|
||||
|
||||
// Process events for charting
|
||||
const chartData = useMemo(() => {
|
||||
if (!events || events.length === 0) return [];
|
||||
|
||||
// Group events by date (day)
|
||||
const eventsByDate: Record<string, {
|
||||
date: string;
|
||||
check_cycles: number;
|
||||
tasks_found: number;
|
||||
tasks_executed: number;
|
||||
tasks_failed: number;
|
||||
job_scheduled: number;
|
||||
job_completed: number;
|
||||
job_failed: number;
|
||||
}> = {};
|
||||
|
||||
events.forEach(event => {
|
||||
const date = event.event_date ? new Date(event.event_date).toLocaleDateString() : 'Unknown';
|
||||
|
||||
if (!eventsByDate[date]) {
|
||||
eventsByDate[date] = {
|
||||
date,
|
||||
check_cycles: 0,
|
||||
tasks_found: 0,
|
||||
tasks_executed: 0,
|
||||
tasks_failed: 0,
|
||||
job_scheduled: 0,
|
||||
job_completed: 0,
|
||||
job_failed: 0,
|
||||
};
|
||||
}
|
||||
|
||||
switch (event.event_type) {
|
||||
case 'check_cycle':
|
||||
eventsByDate[date].check_cycles++;
|
||||
eventsByDate[date].tasks_found += event.tasks_found || 0;
|
||||
eventsByDate[date].tasks_executed += event.tasks_executed || 0;
|
||||
eventsByDate[date].tasks_failed += event.tasks_failed || 0;
|
||||
break;
|
||||
case 'job_scheduled':
|
||||
eventsByDate[date].job_scheduled++;
|
||||
break;
|
||||
case 'job_completed':
|
||||
eventsByDate[date].job_completed++;
|
||||
break;
|
||||
case 'job_failed':
|
||||
eventsByDate[date].job_failed++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to array and sort by date
|
||||
return Object.values(eventsByDate).sort((a, b) => {
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
}).slice(-30); // Last 30 days
|
||||
}, [events]);
|
||||
|
||||
// Calculate totals for summary
|
||||
const totals = useMemo(() => {
|
||||
return events.reduce((acc, event) => {
|
||||
switch (event.event_type) {
|
||||
case 'check_cycle':
|
||||
acc.check_cycles++;
|
||||
acc.tasks_found += event.tasks_found || 0;
|
||||
acc.tasks_executed += event.tasks_executed || 0;
|
||||
acc.tasks_failed += event.tasks_failed || 0;
|
||||
break;
|
||||
case 'job_scheduled':
|
||||
acc.job_scheduled++;
|
||||
break;
|
||||
case 'job_completed':
|
||||
acc.job_completed++;
|
||||
break;
|
||||
case 'job_failed':
|
||||
acc.job_failed++;
|
||||
break;
|
||||
}
|
||||
return acc;
|
||||
}, {
|
||||
check_cycles: 0,
|
||||
tasks_found: 0,
|
||||
tasks_executed: 0,
|
||||
tasks_failed: 0,
|
||||
job_scheduled: 0,
|
||||
job_completed: 0,
|
||||
job_failed: 0,
|
||||
});
|
||||
}, [events]);
|
||||
|
||||
// Custom tooltip with terminal theme
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
backgroundColor: terminalColors.background,
|
||||
border: `1px solid ${terminalColors.primary}`,
|
||||
padding: 1,
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, fontWeight: 'bold', mb: 0.5 }}>
|
||||
{label}
|
||||
</TerminalTypography>
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<TerminalTypography
|
||||
key={index}
|
||||
variant="body2"
|
||||
sx={{ color: entry.color, fontSize: '0.75rem' }}
|
||||
>
|
||||
{entry.name}: {entry.value}
|
||||
</TerminalTypography>
|
||||
))}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<CircularProgress sx={{ color: terminalColors.primary, mb: 2 }} />
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
||||
Loading chart data...
|
||||
</TerminalTypography>
|
||||
</TerminalPaper>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.error }}>
|
||||
Error loading charts: {error}
|
||||
</TerminalTypography>
|
||||
</TerminalPaper>
|
||||
);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
||||
No event history data available yet. Charts will appear once scheduler events are logged.
|
||||
</TerminalTypography>
|
||||
</TerminalPaper>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
{/* 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}>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
wrapperStyle={{ color: terminalColors.primary, fontFamily: 'monospace' }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="tasks_found"
|
||||
stroke={terminalColors.info}
|
||||
strokeWidth={2}
|
||||
name="Tasks Found"
|
||||
dot={{ fill: terminalColors.info, r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="tasks_executed"
|
||||
stroke={terminalColors.success}
|
||||
strokeWidth={2}
|
||||
name="Tasks Executed"
|
||||
dot={{ fill: terminalColors.success, r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="tasks_failed"
|
||||
stroke={terminalColors.error}
|
||||
strokeWidth={2}
|
||||
name="Tasks Failed"
|
||||
dot={{ fill: terminalColors.error, r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</TerminalPaper>
|
||||
|
||||
{/* 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}>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
wrapperStyle={{ color: terminalColors.primary, fontFamily: 'monospace' }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="job_scheduled"
|
||||
fill={terminalColors.info}
|
||||
name="Scheduled"
|
||||
/>
|
||||
<Bar
|
||||
dataKey="job_completed"
|
||||
fill={terminalColors.success}
|
||||
name="Completed"
|
||||
/>
|
||||
<Bar
|
||||
dataKey="job_failed"
|
||||
fill={terminalColors.error}
|
||||
name="Failed"
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</TerminalPaper>
|
||||
|
||||
{/* 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}>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={terminalColors.border} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={terminalColors.primary}
|
||||
tick={{ fill: terminalColors.primary, fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Bar
|
||||
dataKey="check_cycles"
|
||||
fill={terminalColors.primary}
|
||||
name="Check Cycles"
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</TerminalPaper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulerCharts;
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Scheduler Event History Component
|
||||
* Displays historical scheduler events (check cycles, interval adjustments, etc.)
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
Chip,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TerminalPaper,
|
||||
TerminalTypography,
|
||||
TerminalChipSuccess,
|
||||
TerminalChipError,
|
||||
TerminalChipWarning,
|
||||
TerminalTableCell,
|
||||
TerminalTableRow,
|
||||
terminalColors
|
||||
} from './terminalTheme';
|
||||
import { getSchedulerEventHistory, SchedulerEvent } from '../../api/schedulerDashboard';
|
||||
|
||||
interface SchedulerEventHistoryProps {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const SchedulerEventHistory: React.FC<SchedulerEventHistoryProps> = ({ limit = 50 }) => {
|
||||
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 [totalCount, setTotalCount] = useState(0);
|
||||
const [eventTypeFilter, setEventTypeFilter] = useState<string>('all');
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await getSchedulerEventHistory(
|
||||
rowsPerPage,
|
||||
page * rowsPerPage,
|
||||
eventTypeFilter !== 'all' ? eventTypeFilter as any : undefined
|
||||
);
|
||||
|
||||
setEvents(response.events);
|
||||
setTotalCount(response.total_count);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch scheduler event history');
|
||||
console.error('Error fetching scheduler event history:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, rowsPerPage, eventTypeFilter]); // fetchEvents is stable, no need to include
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const getEventTypeColor = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case 'check_cycle':
|
||||
return terminalColors.success;
|
||||
case 'interval_adjustment':
|
||||
return terminalColors.warning;
|
||||
case 'start':
|
||||
return terminalColors.success;
|
||||
case 'stop':
|
||||
return terminalColors.error;
|
||||
case 'job_scheduled':
|
||||
return terminalColors.info;
|
||||
case 'job_completed':
|
||||
return terminalColors.success;
|
||||
case 'job_failed':
|
||||
return terminalColors.error;
|
||||
default:
|
||||
return terminalColors.info;
|
||||
}
|
||||
};
|
||||
|
||||
const formatEventDetails = (event: SchedulerEvent): string => {
|
||||
switch (event.event_type) {
|
||||
case 'check_cycle':
|
||||
return `Cycle #${event.check_cycle_number || 'N/A'} | ${event.tasks_found || 0} found, ${event.tasks_executed || 0} executed, ${event.tasks_failed || 0} failed | ${event.check_duration_seconds?.toFixed(2) || 'N/A'}s`;
|
||||
case 'interval_adjustment':
|
||||
return `${event.previous_interval_minutes || 'N/A'}min → ${event.new_interval_minutes || 'N/A'}min | ${event.active_strategies_count || 0} active strategies`;
|
||||
case 'start':
|
||||
return `Started with ${event.check_interval_minutes || 'N/A'}min interval | ${event.active_strategies_count || 0} active strategies`;
|
||||
case 'stop':
|
||||
return `Stopped gracefully | ${event.event_data?.total_checks || 0} total cycles`;
|
||||
case 'job_scheduled':
|
||||
const scheduledJob = event.event_data as any;
|
||||
return `Job: ${event.job_id || 'N/A'} | Function: ${scheduledJob?.function_name || 'N/A'} | User: ${event.user_id || 'system'}`;
|
||||
case 'job_completed':
|
||||
const completedJob = event.event_data as any;
|
||||
return `Job: ${event.job_id || 'N/A'} | Function: ${completedJob?.job_function || 'N/A'} | User: ${event.user_id || 'system'} | Time: ${completedJob?.execution_time_seconds?.toFixed(2) || 'N/A'}s`;
|
||||
case 'job_failed':
|
||||
const failedJob = event.event_data as any;
|
||||
const expensive = failedJob?.expensive_api_call ? '💰 Expensive API call wasted' : '';
|
||||
const errorMsg = event.error_message || failedJob?.exception_message || 'Unknown error';
|
||||
return `Job: ${event.job_id || 'N/A'} | Function: ${failedJob?.job_function || 'N/A'} | User: ${event.user_id || 'system'} | Error: ${errorMsg}${expensive ? ` | ${expensive}` : ''}`;
|
||||
default:
|
||||
return JSON.stringify(event.event_data || {});
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && events.length === 0) {
|
||||
return (
|
||||
<TerminalPaper>
|
||||
<Box p={3}>
|
||||
<TerminalTypography variant="h6" gutterBottom>
|
||||
📜 Scheduler Event History
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.info }}>
|
||||
Loading event history...
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<TerminalPaper>
|
||||
<Box p={3}>
|
||||
<TerminalTypography variant="h6" gutterBottom>
|
||||
📜 Scheduler Event History
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.error }}>
|
||||
Error: {error}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TerminalPaper>
|
||||
<Box p={2}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<TerminalTypography variant="h6">
|
||||
📜 Scheduler Event History
|
||||
</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': {
|
||||
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>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<Box p={3} textAlign="center">
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.info }}>
|
||||
No scheduler events found. Events will appear here as the scheduler runs.
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TerminalTableCell>Date</TerminalTableCell>
|
||||
<TerminalTableCell>Event Type</TerminalTableCell>
|
||||
<TerminalTableCell>Details</TerminalTableCell>
|
||||
{(events.some(e => e.event_type === 'job_failed' && e.error_message)) && (
|
||||
<TerminalTableCell>Error</TerminalTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{events.map((event) => (
|
||||
<TerminalTableRow key={event.id}>
|
||||
<TerminalTableCell>
|
||||
<TerminalTypography variant="body2" fontSize="0.75rem">
|
||||
{formatDate(event.event_date)}
|
||||
</TerminalTypography>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
<Chip
|
||||
label={event.event_type}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: getEventTypeColor(event.event_type),
|
||||
color: '#000',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
/>
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
<TerminalTypography variant="body2" fontSize="0.75rem" sx={{
|
||||
color: getEventTypeColor(event.event_type),
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{formatEventDetails(event)}
|
||||
</TerminalTypography>
|
||||
</TerminalTableCell>
|
||||
{event.event_type === 'job_failed' && event.error_message && (
|
||||
<TerminalTableCell>
|
||||
<Tooltip title={event.error_message} arrow>
|
||||
<TerminalTypography variant="body2" fontSize="0.7rem" sx={{
|
||||
color: terminalColors.error,
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '300px',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical'
|
||||
}}>
|
||||
{event.error_message}
|
||||
</TerminalTypography>
|
||||
</Tooltip>
|
||||
</TerminalTableCell>
|
||||
)}
|
||||
{event.event_type !== 'job_failed' && events.some(e => e.event_type === 'job_failed' && e.error_message) && (
|
||||
<TerminalTableCell></TerminalTableCell>
|
||||
)}
|
||||
</TerminalTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</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': {
|
||||
color: terminalColors.primary,
|
||||
},
|
||||
'& .MuiIconButton-root': {
|
||||
color: terminalColors.primary,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulerEventHistory;
|
||||
|
||||
272
frontend/src/components/SchedulerDashboard/SchedulerJobsTree.tsx
Normal file
272
frontend/src/components/SchedulerDashboard/SchedulerJobsTree.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Scheduler Jobs Tree Component
|
||||
* Displays scheduled jobs in tree structure matching log format.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import {
|
||||
Schedule as ScheduleIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Event as EventIcon,
|
||||
Person as PersonIcon,
|
||||
Storage as StorageIcon
|
||||
} from '@mui/icons-material';
|
||||
import { SchedulerJob } from '../../api/schedulerDashboard';
|
||||
import { TerminalPaper, TerminalTypography, TerminalChip, terminalColors } from './terminalTheme';
|
||||
|
||||
interface SchedulerJobsTreeProps {
|
||||
jobs: SchedulerJob[];
|
||||
recurringJobs: number;
|
||||
oneTimeJobs: number;
|
||||
}
|
||||
|
||||
const SchedulerJobsTree: React.FC<SchedulerJobsTreeProps> = ({
|
||||
jobs,
|
||||
recurringJobs,
|
||||
oneTimeJobs
|
||||
}) => {
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'Not scheduled';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getJobTypeIcon = (jobId: string) => {
|
||||
if (jobId === 'check_due_tasks') {
|
||||
return <RefreshIcon fontSize="small" />;
|
||||
}
|
||||
return <EventIcon fontSize="small" />;
|
||||
};
|
||||
|
||||
const getJobTypeLabel = (jobId: string, job?: SchedulerJob) => {
|
||||
if (jobId === 'check_due_tasks') {
|
||||
return 'Recurring';
|
||||
}
|
||||
if (jobId.includes('research_persona')) {
|
||||
return 'Research Persona';
|
||||
}
|
||||
if (jobId.includes('facebook_persona')) {
|
||||
return 'Facebook Persona';
|
||||
}
|
||||
if (jobId.includes('oauth_token_monitoring')) {
|
||||
// Extract platform from job ID or use platform field
|
||||
const platform = job?.platform ||
|
||||
jobId.split('_')[2] ||
|
||||
'OAuth';
|
||||
const platformNames: { [key: string]: string } = {
|
||||
'gsc': 'GSC',
|
||||
'bing': 'Bing',
|
||||
'wordpress': 'WordPress',
|
||||
'wix': 'Wix'
|
||||
};
|
||||
return `OAuth ${platformNames[platform] || platform.toUpperCase()}`;
|
||||
}
|
||||
return 'One-Time';
|
||||
};
|
||||
|
||||
const getJobTypeColor = (jobId: string) => {
|
||||
if (jobId === 'check_due_tasks') {
|
||||
return 'primary';
|
||||
}
|
||||
return 'secondary';
|
||||
};
|
||||
|
||||
// Separate recurring and one-time jobs
|
||||
const recurringJob = jobs.find(j => j.id === 'check_due_tasks');
|
||||
const oneTimeJobsList = jobs.filter(j => j.id !== 'check_due_tasks');
|
||||
|
||||
return (
|
||||
<TerminalPaper sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
||||
<ScheduleIcon sx={{ color: terminalColors.primary }} />
|
||||
<TerminalTypography variant="h6" component="h2" sx={{ fontSize: '1.25rem', fontWeight: 'bold' }}>
|
||||
Scheduled Jobs
|
||||
</TerminalTypography>
|
||||
<TerminalChip
|
||||
label={`${jobs.length} total`}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ fontFamily: 'monospace', fontSize: '0.875rem', color: terminalColors.text, flex: 1, overflow: 'auto', minHeight: 0 }}>
|
||||
{/* Header */}
|
||||
<Box mb={2} sx={{ flexShrink: 0 }}>
|
||||
<TerminalTypography variant="body2" sx={{ mb: 1, color: terminalColors.textSecondary }}>
|
||||
Recurring Jobs: {recurringJobs} | One-Time Jobs: {oneTimeJobs}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
|
||||
{/* Jobs Tree */}
|
||||
{jobs.length > 0 ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
{jobs.map((job, index) => {
|
||||
const isLast = index === jobs.length - 1;
|
||||
const prefix = isLast ? '└─' : '├─';
|
||||
const isRecurring = job.id === 'check_due_tasks';
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={job.id}
|
||||
sx={{
|
||||
mb: 2,
|
||||
display: 'block',
|
||||
borderLeft: `2px solid ${terminalColors.border}`,
|
||||
pl: 2,
|
||||
py: 1
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="flex-start"
|
||||
gap={1.5}
|
||||
flexWrap="wrap"
|
||||
sx={{
|
||||
width: '100%',
|
||||
minHeight: '50px',
|
||||
}}
|
||||
>
|
||||
{/* Tree prefix and chip */}
|
||||
<Box display="flex" alignItems="center" gap={1} sx={{ flexShrink: 0 }}>
|
||||
<TerminalTypography component="span" sx={{ fontFamily: 'monospace', color: terminalColors.primary, fontSize: '1.2rem' }}>
|
||||
{prefix}
|
||||
</TerminalTypography>
|
||||
<TerminalChip
|
||||
icon={getJobTypeIcon(job.id)}
|
||||
label={getJobTypeLabel(job.id, job)}
|
||||
size="small"
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Job details */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', mb: 0.5 }}>
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.primary,
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
{job.id}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5, alignItems: 'center', mt: 0.5 }}>
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.textSecondary,
|
||||
fontSize: '0.8rem',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
>
|
||||
Trigger: {job.trigger_type}
|
||||
</TerminalTypography>
|
||||
{job.next_run_time && (
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.textSecondary,
|
||||
fontSize: '0.8rem',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
>
|
||||
Next Run: {formatDate(job.next_run_time)}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
{job.user_id && (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<PersonIcon fontSize="small" sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }} />
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.textSecondary,
|
||||
fontSize: '0.8rem',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
>
|
||||
User: {String(job.user_id)}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
{job.platform && (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.primary,
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 'bold',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
>
|
||||
Platform: {job.platform.toUpperCase()}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
{job.user_job_store && job.user_job_store !== 'default' && (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<StorageIcon fontSize="small" sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }} />
|
||||
<TerminalTypography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
color: terminalColors.textSecondary,
|
||||
fontSize: '0.8rem',
|
||||
wordBreak: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
>
|
||||
Store: {job.user_job_store}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<TerminalTypography variant="body2" sx={{ fontStyle: 'italic', color: terminalColors.textSecondary }}>
|
||||
No jobs scheduled
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</Box>
|
||||
</TerminalPaper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulerJobsTree;
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Scheduler Stats Cards Component
|
||||
* Displays scheduler metrics in card format.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid, Typography, Box } from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Pause as PauseIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
AccessTime as AccessTimeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { SchedulerStats } from '../../api/schedulerDashboard';
|
||||
import { TerminalCard, TerminalCardContent, TerminalTypography, TerminalChip, TerminalChipSuccess, TerminalChipError, terminalColors } from './terminalTheme';
|
||||
|
||||
interface SchedulerStatsCardsProps {
|
||||
stats: SchedulerStats;
|
||||
}
|
||||
|
||||
const SchedulerStatsCards: React.FC<SchedulerStatsCardsProps> = ({ stats }) => {
|
||||
// Debug: Only log if cumulative values are actually present (not just 0 from defaults)
|
||||
// Suppress logging when all cumulative values are 0 to reduce console noise
|
||||
if (stats.cumulative_total_check_cycles !== undefined) {
|
||||
const hasCumulativeData = stats.cumulative_total_check_cycles > 0 ||
|
||||
stats.cumulative_tasks_found > 0 ||
|
||||
stats.cumulative_tasks_executed > 0;
|
||||
|
||||
// Only log if there's actual cumulative data or if this is the first render
|
||||
if (hasCumulativeData || stats.total_checks > 0) {
|
||||
console.log('📊 StatsCards received stats:', {
|
||||
total_checks: stats.total_checks,
|
||||
cumulative_total_check_cycles: stats.cumulative_total_check_cycles,
|
||||
cumulative_tasks_found: stats.cumulative_tasks_found,
|
||||
cumulative_tasks_executed: stats.cumulative_tasks_executed,
|
||||
cumulative_tasks_failed: stats.cumulative_tasks_failed,
|
||||
has_cumulative_data: hasCumulativeData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (running: boolean) => {
|
||||
return running ? 'success' : 'error';
|
||||
};
|
||||
|
||||
const getStatusIcon = (running: boolean) => {
|
||||
return running ? <PlayArrowIcon /> : <PauseIcon />;
|
||||
};
|
||||
|
||||
const formatTime = (minutes: number) => {
|
||||
if (minutes >= 60) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays}d ago`;
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: 'Scheduler Status',
|
||||
value: stats.running ? 'Running' : 'Stopped',
|
||||
icon: getStatusIcon(stats.running),
|
||||
color: getStatusColor(stats.running),
|
||||
subtitle: stats.running ? 'Active' : 'Inactive'
|
||||
},
|
||||
{
|
||||
title: 'Total Check Cycles',
|
||||
value: (stats.cumulative_total_check_cycles !== undefined && stats.cumulative_total_check_cycles !== null)
|
||||
? stats.cumulative_total_check_cycles.toLocaleString()
|
||||
: stats.total_checks.toLocaleString(),
|
||||
icon: <CheckCircleIcon />,
|
||||
color: 'primary' as const,
|
||||
subtitle: (stats.cumulative_total_check_cycles !== undefined && stats.cumulative_total_check_cycles !== null && stats.cumulative_total_check_cycles > 0)
|
||||
? `${stats.total_checks.toLocaleString()} this session (${stats.cumulative_total_check_cycles.toLocaleString()} total)`
|
||||
: stats.total_checks === 0
|
||||
? 'No cycles yet (scheduler waiting)'
|
||||
: 'Since startup'
|
||||
},
|
||||
{
|
||||
title: 'Tasks Executed',
|
||||
value: (stats.cumulative_tasks_executed !== undefined && stats.cumulative_tasks_executed !== null)
|
||||
? stats.cumulative_tasks_executed.toLocaleString()
|
||||
: stats.tasks_executed.toLocaleString(),
|
||||
icon: <TrendingUpIcon />,
|
||||
color: 'success' as const,
|
||||
subtitle: (stats.cumulative_tasks_executed !== undefined && stats.cumulative_tasks_executed !== null && stats.cumulative_tasks_executed > 0)
|
||||
? `${stats.tasks_executed.toLocaleString()} this session (${stats.cumulative_tasks_executed.toLocaleString()} total)`
|
||||
: stats.tasks_executed === 0
|
||||
? 'No tasks executed yet'
|
||||
: `${stats.tasks_failed > 0 ? `${stats.tasks_failed} failed` : 'All successful'}`
|
||||
},
|
||||
{
|
||||
title: 'Tasks Found',
|
||||
value: (stats.cumulative_tasks_found !== undefined && stats.cumulative_tasks_found !== null)
|
||||
? stats.cumulative_tasks_found.toLocaleString()
|
||||
: stats.tasks_found.toLocaleString(),
|
||||
icon: <ScheduleIcon />,
|
||||
color: 'info' as const,
|
||||
subtitle: (stats.cumulative_tasks_found !== undefined && stats.cumulative_tasks_found !== null && stats.cumulative_tasks_found > 0)
|
||||
? `${stats.tasks_found.toLocaleString()} this session (${stats.cumulative_tasks_found.toLocaleString()} total)`
|
||||
: stats.tasks_found === 0
|
||||
? 'No tasks scheduled yet'
|
||||
: `${stats.tasks_executed} executed, ${stats.tasks_failed} failed`
|
||||
},
|
||||
{
|
||||
title: 'Check Interval',
|
||||
value: formatTime(stats.check_interval_minutes),
|
||||
icon: <AccessTimeIcon />,
|
||||
color: 'secondary' as const,
|
||||
subtitle: stats.intelligent_scheduling
|
||||
? `Intelligent (${stats.active_strategies_count > 0 ? '15min' : '60min'} range)`
|
||||
: 'Fixed interval'
|
||||
},
|
||||
{
|
||||
title: 'Active Strategies',
|
||||
value: stats.active_strategies_count.toString(),
|
||||
icon: <TrendingUpIcon />,
|
||||
color: stats.active_strategies_count > 0 ? 'success' : 'default' as const,
|
||||
subtitle: stats.active_strategies_count > 0
|
||||
? 'With monitoring tasks'
|
||||
: 'No active strategies'
|
||||
}
|
||||
];
|
||||
|
||||
const getCardIconColor = (cardColor: string) => {
|
||||
switch (cardColor) {
|
||||
case 'success':
|
||||
return terminalColors.success;
|
||||
case 'error':
|
||||
return terminalColors.error;
|
||||
case 'primary':
|
||||
return terminalColors.primary;
|
||||
case 'info':
|
||||
return terminalColors.info;
|
||||
case 'secondary':
|
||||
return terminalColors.secondary;
|
||||
default:
|
||||
return terminalColors.text;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{cards.map((card, index) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||
<TerminalCard sx={{ height: '100%' }}>
|
||||
<TerminalCardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: '4px',
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
border: `1px solid ${getCardIconColor(card.color)}`,
|
||||
color: getCardIconColor(card.color),
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{card.icon}
|
||||
</Box>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
|
||||
{card.title}
|
||||
</TerminalTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
<TerminalTypography variant="h4" component="div" sx={{ fontWeight: 600, mb: 0.5, fontSize: '1.75rem', color: terminalColors.primary }}>
|
||||
{card.value}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary, fontSize: '0.875rem' }}>
|
||||
{card.subtitle}
|
||||
</TerminalTypography>
|
||||
{card.title === 'Scheduler Status' && stats.last_check && (
|
||||
<TerminalTypography variant="caption" sx={{ mt: 1, display: 'block', color: terminalColors.textSecondary, fontSize: '0.75rem' }}>
|
||||
Last check: {formatDate(stats.last_check)}
|
||||
</TerminalTypography>
|
||||
)}
|
||||
</TerminalCardContent>
|
||||
</TerminalCard>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulerStatsCards;
|
||||
|
||||
187
frontend/src/components/SchedulerDashboard/terminalTheme.ts
Normal file
187
frontend/src/components/SchedulerDashboard/terminalTheme.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Terminal Theme Styling
|
||||
* Shared terminal-themed styles for scheduler dashboard components
|
||||
*/
|
||||
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box, Paper, Card, CardContent, Typography, Chip, TableCell, TableRow, Alert, Accordion } from '@mui/material';
|
||||
|
||||
export const TerminalPaper = styled(Paper)({
|
||||
backgroundColor: '#0a0a0a',
|
||||
border: '1px solid #00ff00',
|
||||
color: '#00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
padding: 16,
|
||||
minHeight: '200px', // Ensure minimum height for visibility
|
||||
'& *': {
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit', // Ensure all text inherits the green color
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalCard = styled(Card)({
|
||||
backgroundColor: '#0a0a0a',
|
||||
border: '1px solid #00ff00',
|
||||
color: '#00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
transition: 'all 0.2s',
|
||||
minHeight: '120px', // Ensure cards have minimum height
|
||||
'&:hover': {
|
||||
borderColor: '#00ff88',
|
||||
boxShadow: '0 0 15px rgba(0, 255, 0, 0.3)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
'& *': {
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit', // Ensure all text inherits the green color
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalCardContent = styled(CardContent)({
|
||||
color: '#00ff00',
|
||||
'&:last-child': {
|
||||
paddingBottom: 16,
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalTypography = styled(Typography)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
color: '#00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
}));
|
||||
|
||||
export const TerminalChip = styled(Chip)({
|
||||
backgroundColor: '#1a1a1a',
|
||||
color: '#00ff00',
|
||||
border: '1px solid #00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
fontSize: '0.75rem',
|
||||
'& .MuiChip-label': {
|
||||
padding: '4px 8px',
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
color: '#00ff00',
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalChipSuccess = styled(Chip)({
|
||||
backgroundColor: '#0a2a0a',
|
||||
color: '#00ff00',
|
||||
border: '1px solid #00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
fontSize: '0.75rem',
|
||||
'& .MuiChip-label': {
|
||||
padding: '4px 8px',
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
color: '#00ff00',
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalChipError = styled(Chip)({
|
||||
backgroundColor: '#2a0a0a',
|
||||
color: '#ff4444',
|
||||
border: '1px solid #ff4444',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
fontSize: '0.75rem',
|
||||
'& .MuiChip-label': {
|
||||
padding: '4px 8px',
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ff4444',
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalChipWarning = styled(Chip)({
|
||||
backgroundColor: '#2a2a0a',
|
||||
color: '#ffd700',
|
||||
border: '1px solid #ffd700',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
fontSize: '0.75rem',
|
||||
'& .MuiChip-label': {
|
||||
padding: '4px 8px',
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ffd700',
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalTableCell = styled(TableCell)({
|
||||
color: '#00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
borderColor: '#004400',
|
||||
fontSize: '0.875rem',
|
||||
});
|
||||
|
||||
export const TerminalTableRow = styled(TableRow)({
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.05)',
|
||||
},
|
||||
'&:nth-of-type(even)': {
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.02)',
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalAlert = styled(Alert)({
|
||||
backgroundColor: '#1a1a1a',
|
||||
color: '#ff4444',
|
||||
border: '1px solid #ff4444',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#ff4444',
|
||||
},
|
||||
'&.MuiAlert-standardSuccess': {
|
||||
color: '#00ff00',
|
||||
borderColor: '#00ff00',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#00ff00',
|
||||
}
|
||||
},
|
||||
'&.MuiAlert-standardWarning': {
|
||||
color: '#ffd700',
|
||||
borderColor: '#ffd700',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#ffd700',
|
||||
}
|
||||
},
|
||||
'&.MuiAlert-standardInfo': {
|
||||
color: '#00ffff',
|
||||
borderColor: '#00ffff',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#00ffff',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalAccordion = styled(Accordion)({
|
||||
backgroundColor: '#1a1a1a',
|
||||
border: '1px solid #00ff00',
|
||||
color: '#00ff00',
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
'&:before': {
|
||||
display: 'none',
|
||||
},
|
||||
'&.Mui-expanded': {
|
||||
margin: 0,
|
||||
}
|
||||
});
|
||||
|
||||
export const TerminalBox = styled(Box)({
|
||||
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
|
||||
color: '#00ff00',
|
||||
});
|
||||
|
||||
// Color constants
|
||||
export const terminalColors = {
|
||||
primary: '#00ff00',
|
||||
secondary: '#00ff88',
|
||||
error: '#ff4444',
|
||||
warning: '#ffd700',
|
||||
info: '#00ffff',
|
||||
success: '#00ff00',
|
||||
background: '#0a0a0a',
|
||||
backgroundLight: '#1a1a1a',
|
||||
text: '#00ff00',
|
||||
textSecondary: '#00ff88',
|
||||
border: '#00ff00',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user