Scheduled research persona generation

This commit is contained in:
ajaysi
2025-11-05 08:51:00 +05:30
parent 55087c4f37
commit d99c7c83a7
98 changed files with 14518 additions and 828 deletions

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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',
};