Files
ALwrity/frontend/src/pages/SchedulerDashboard.tsx
ajaysi 993000a540 fix(01-code-splitting): convert SchedulerDashboard MUI icons to individual imports
- Converted 7 barrel imports (Refresh, Schedule, CheckCircle, PlayArrow, Pause, TrendingUp, AccessTime) to per-file default imports
2026-05-14 09:11:50 +05:30

723 lines
28 KiB
TypeScript

/**
* Scheduler Dashboard Page
* Main page displaying scheduler status, jobs, execution logs, and insights.
* Terminal-themed UI with high readability.
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box,
Container,
Typography,
IconButton,
Tooltip,
Alert,
CircularProgress,
Chip
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import ScheduleIcon from '@mui/icons-material/Schedule';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import { useAuth } from '@clerk/clerk-react';
import { styled } from '@mui/material/styles';
import { getSchedulerDashboard, SchedulerDashboardData } from '../api/schedulerDashboard';
import { isBackendCooldownActive, logBackendCooldownSkipOnce } from '../api/client';
// Removed SchedulerStatsCards - metrics moved to header
import SchedulerJobsTree from '../components/SchedulerDashboard/SchedulerJobsTree';
import ExecutionLogsTable from '../components/SchedulerDashboard/ExecutionLogsTable';
import FailuresInsights from '../components/SchedulerDashboard/FailuresInsights';
import SchedulerEventHistory from '../components/SchedulerDashboard/SchedulerEventHistory';
import SchedulerCharts from '../components/SchedulerDashboard/SchedulerCharts';
import TaskMonitoringTabs from '../components/SchedulerDashboard/TaskMonitoringTabs';
import { TerminalTypography, terminalColors } from '../components/SchedulerDashboard/terminalTheme';
import { useSchedulerTaskAlerts } from '../hooks/useSchedulerTaskAlerts';
import TasksNeedingIntervention from '../components/SchedulerDashboard/TasksNeedingIntervention';
import HeaderControls from '../components/shared/HeaderControls';
// Terminal-themed styled components
const TerminalContainer = styled(Container)(({ theme }) => ({
backgroundColor: '#0a0a0a',
minHeight: '100vh',
color: '#00ff00',
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
padding: theme.spacing(3),
'& *': {
fontFamily: 'inherit',
}
}));
const TerminalHeader = styled(Box)({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
paddingBottom: 16,
borderBottom: '2px solid #00ff00',
});
const TerminalTitle = styled(Typography)<{ component?: React.ElementType }>(({ theme }) => ({
color: '#00ff00',
fontFamily: 'inherit',
fontSize: '1.75rem',
fontWeight: 'bold',
textShadow: '0 0 10px rgba(0, 255, 0, 0.5)',
letterSpacing: '2px',
}));
const TerminalSubtitle = styled(Typography)({
color: '#00ff88',
fontFamily: 'inherit',
fontSize: '0.875rem',
marginTop: 4,
opacity: 0.8,
});
const TerminalChip = styled(Chip)({
backgroundColor: '#1a1a1a',
color: '#00ff00',
border: '1px solid #00ff00',
fontFamily: 'inherit',
fontSize: '0.75rem',
'& .MuiChip-label': {
padding: '4px 8px',
}
});
const TerminalIconButton = styled(IconButton)({
color: '#00ff00',
border: '1px solid #00ff00',
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
boxShadow: '0 0 10px rgba(0, 255, 0, 0.3)',
},
'&:disabled': {
color: '#004400',
borderColor: '#004400',
}
});
// Metric bubble style for header - Ultra modern terminal aesthetic
const MetricBubble = styled(Box)({
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 14px',
backgroundColor: 'rgba(10, 10, 10, 0.8)',
border: '1px solid #00ff00',
borderRadius: '20px',
fontFamily: '"Courier New", "Monaco", "Consolas", "Fira Code", monospace',
fontSize: '0.875rem',
color: '#00ff00',
cursor: 'default',
position: 'relative',
overflow: 'hidden',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 0 0 rgba(0, 255, 0, 0)',
textShadow: '0 0 5px rgba(0, 255, 0, 0.3)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(0, 255, 0, 0.1), transparent)',
transition: 'left 0.5s ease',
},
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.15)',
borderColor: '#00ff88',
boxShadow: '0 0 20px rgba(0, 255, 0, 0.4), inset 0 0 10px rgba(0, 255, 0, 0.1)',
transform: 'translateY(-2px) scale(1.02)',
textShadow: '0 0 8px rgba(0, 255, 0, 0.6)',
'&::before': {
left: '100%',
},
},
'& .metric-icon': {
fontSize: '18px',
display: 'flex',
alignItems: 'center',
filter: 'drop-shadow(0 0 3px rgba(0, 255, 0, 0.5))',
transition: 'all 0.3s ease',
},
'&:hover .metric-icon': {
transform: 'scale(1.1) rotate(5deg)',
filter: 'drop-shadow(0 0 6px rgba(0, 255, 0, 0.8))',
},
'& .metric-value': {
fontWeight: 700,
fontSize: '0.9rem',
letterSpacing: '0.5px',
background: 'linear-gradient(135deg, #00ff00 0%, #00ff88 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
},
'& .metric-label': {
fontSize: '0.7rem',
opacity: 0.7,
marginLeft: '2px',
letterSpacing: '0.3px',
textTransform: 'uppercase',
fontWeight: 500,
}
});
const TerminalAlert = styled(Alert)({
backgroundColor: '#1a1a1a',
color: '#ff4444',
border: '1px solid #ff4444',
fontFamily: 'inherit',
'& .MuiAlert-icon': {
color: '#ff4444',
}
});
const TerminalLoading = styled(Box)({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px',
'& .MuiCircularProgress-root': {
color: '#00ff00',
}
});
const SchedulerDashboard: React.FC = () => {
const { isSignedIn, isLoaded, userId } = useAuth();
const [dashboardData, setDashboardData] = useState<SchedulerDashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [autoRefreshInterval, setAutoRefreshInterval] = useState<NodeJS.Timeout | null>(null);
const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState<string | null>(null);
// Poll for tasks needing intervention and show toast notifications
useSchedulerTaskAlerts({
enabled: isSignedIn && isLoaded,
interval: 60000 // Poll every minute
});
// Use refs to track loading state without causing re-renders
const loadingRef = useRef(false);
const refreshingRef = useRef(false);
const fetchDashboardData = useCallback(async (isManualRefresh = false) => {
// Prevent multiple simultaneous fetches using refs
if (loadingRef.current || refreshingRef.current) {
return;
}
if (isBackendCooldownActive()) {
logBackendCooldownSkipOnce('SchedulerDashboard');
return;
}
try {
loadingRef.current = !isManualRefresh;
refreshingRef.current = isManualRefresh;
if (isManualRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
const data = await getSchedulerDashboard();
// Always update state to ensure metrics are updated
// The comparison was preventing updates when cumulative stats changed
setDashboardData(data);
setLastUpdated(new Date());
setLastUpdateTimestamp(data.stats.last_update || null);
} catch (err: any) {
setError(err.message || 'Failed to fetch scheduler dashboard');
console.error('Error fetching scheduler dashboard:', err);
} finally {
loadingRef.current = false;
refreshingRef.current = false;
setLoading(false);
setRefreshing(false);
}
}, []); // Empty deps - function is stable
// Initial load - only once
useEffect(() => {
if (isLoaded && isSignedIn && !dashboardData) {
fetchDashboardData();
}
}, [isLoaded, isSignedIn]); // Removed fetchDashboardData to prevent re-renders
// Smart auto-refresh: Poll based on scheduler's check interval or next job execution
useEffect(() => {
if (!isSignedIn || !isLoaded || !dashboardData) return;
// Calculate polling interval based on scheduler's check interval
const checkIntervalMinutes = dashboardData.stats?.check_interval_minutes || 60;
// Poll slightly before the scheduler's next check (at 90% of interval)
// Convert to milliseconds and add some buffer
const pollingIntervalMs = Math.max(
(checkIntervalMinutes * 60 * 1000 * 0.9), // 90% of check interval
60000 // Minimum 60 seconds
);
// Alternatively, calculate based on next job execution time
let nextJobTime: Date | null = null;
if (dashboardData.jobs && dashboardData.jobs.length > 0) {
// Find the earliest next run time (only future jobs)
const now = Date.now();
const nextRunTimes = dashboardData.jobs
.map(job => {
if (!job.next_run_time) return null;
const jobTime = new Date(job.next_run_time);
// Only include future jobs
return jobTime.getTime() > now ? jobTime : null;
})
.filter((time): time is Date => time !== null && !isNaN(time.getTime()))
.sort((a, b) => a.getTime() - b.getTime());
if (nextRunTimes.length > 0) {
nextJobTime = nextRunTimes[0];
}
}
// Use next job time if it's sooner than the check interval
let finalIntervalMs = pollingIntervalMs;
if (nextJobTime) {
const msUntilNextJob = nextJobTime.getTime() - Date.now();
// Poll slightly before next job (at 90% of time remaining, min 10s before, max 2min before)
if (msUntilNextJob > 10000) { // At least 10 seconds in the future
// Poll 10 seconds before job, or 10% of time remaining, whichever is smaller
const pollBeforeJob = Math.min(
Math.max(msUntilNextJob * 0.1, 10000), // 10% of time or 10s minimum
msUntilNextJob - 10000 // But no more than 10s before
);
finalIntervalMs = Math.min(finalIntervalMs, pollBeforeJob);
} else if (msUntilNextJob > 0) {
// Job is very soon (< 10s), poll immediately (1 second)
finalIntervalMs = 1000;
}
}
// Cap at reasonable maximum (10 minutes) and minimum (10 seconds)
finalIntervalMs = Math.max(10000, Math.min(finalIntervalMs, 600000)); // 10s min, 10min max
const interval = setInterval(() => {
// Only fetch if we're not already loading/refreshing (using refs)
if (!loadingRef.current && !refreshingRef.current) {
fetchDashboardData();
}
}, finalIntervalMs);
setAutoRefreshInterval(interval);
// Log the polling interval for debugging
if (process.env.NODE_ENV === 'development') {
console.log(
`📊 Scheduler polling: ${Math.round(finalIntervalMs / 1000)}s ` +
`(check interval: ${checkIntervalMinutes}min, next job: ${nextJobTime ? nextJobTime.toLocaleTimeString() : 'none'})`
);
}
return () => {
clearInterval(interval);
};
}, [isSignedIn, isLoaded, dashboardData, fetchDashboardData]); // Re-run when dashboard data changes
// Format time ago
const formatTimeAgo = (date: Date | null) => {
if (!date) return 'Never';
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
if (diffSecs < 10) return 'Just now';
if (diffSecs < 60) return `${diffSecs}s ago`;
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
return `${diffHours}h ago`;
};
const handleManualRefresh = () => {
if (!refreshing && !loading) {
fetchDashboardData(true);
}
};
if (!isLoaded) {
return (
<TerminalContainer maxWidth="xl">
<TerminalLoading>
<CircularProgress />
</TerminalLoading>
</TerminalContainer>
);
}
if (!isSignedIn) {
return (
<TerminalContainer maxWidth="xl">
<TerminalAlert severity="warning">
Please sign in to view the scheduler dashboard.
</TerminalAlert>
</TerminalContainer>
);
}
return (
<TerminalContainer maxWidth="xl">
{/* Header */}
<TerminalHeader>
<Box display="flex" flexDirection="column" gap={2} flex={1}>
{/* Title Row */}
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={2}>
<ScheduleIcon sx={{ color: '#00ff00', fontSize: 32 }} />
<Box>
<TerminalTitle component="h1">
SCHEDULER DASHBOARD
</TerminalTitle>
<TerminalSubtitle>
Monitor task execution, jobs, and system status
</TerminalSubtitle>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={2}>
{lastUpdated && (
<TerminalChip
label={`Last updated: ${formatTimeAgo(lastUpdated)}`}
size="small"
icon={<CheckCircleIcon sx={{ color: '#00ff00', fontSize: 14 }} />}
/>
)}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
Dashboard Status
</Typography>
{dashboardData && (
<>
<Typography variant="caption" component="div" sx={{ mb: 0.5 }}>
<strong>Jobs:</strong> {dashboardData.jobs?.length || 0} total
(Recurring: {dashboardData.recurring_jobs || 0}, One-Time: {dashboardData.one_time_jobs || 0})
</Typography>
<Typography variant="caption" component="div" sx={{ mb: 0.5 }}>
<strong>Check Cycles:</strong> {
dashboardData.stats?.total_checks === 0
? '0 (First check pending - scheduler waiting for interval)'
: `${dashboardData.stats?.total_checks || 0} (${dashboardData.stats?.cumulative_total_check_cycles || 0} total)`
}
</Typography>
<Typography variant="caption" component="div" sx={{ mb: 0.5 }}>
<strong>Scheduler:</strong> {dashboardData.stats?.running ? 'Running' : 'Stopped'} |
<strong> Interval:</strong> {dashboardData.stats?.check_interval_minutes || 0} min
</Typography>
{dashboardData.stats && dashboardData.stats.tasks_found > 0 && (
<Typography variant="caption" component="div">
<strong>Tasks:</strong> {dashboardData.stats.tasks_found} found, {dashboardData.stats.tasks_executed} executed, {dashboardData.stats.tasks_failed} failed
</Typography>
)}
</>
)}
{!dashboardData && (
<Typography variant="caption">
Click to refresh dashboard data
</Typography>
)}
</Box>
}
arrow
>
<span>
<TerminalIconButton
onClick={handleManualRefresh}
disabled={refreshing || loading}
>
<RefreshIcon
sx={{
animation: refreshing ? 'spin 1s linear infinite' : 'none',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' }
}
}}
/>
</TerminalIconButton>
</span>
</Tooltip>
<HeaderControls colorMode="dark" />
</Box>
</Box>
{/* Metrics Bubbles Row */}
{dashboardData?.stats && (
<Box display="flex" alignItems="center" gap={1.5} flexWrap="wrap">
{/* Scheduler Status */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Scheduler Status
</Typography>
<Typography variant="caption">
{dashboardData.stats.running
? 'The scheduler is currently running and actively checking for due tasks.'
: 'The scheduler is stopped and not processing any tasks.'}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon" sx={{ color: dashboardData.stats.running ? '#00ff00' : '#ff4444' }}>
{dashboardData.stats.running ? <PlayArrowIcon fontSize="small" /> : <PauseIcon fontSize="small" />}
</Box>
<Box className="metric-value">
{dashboardData.stats.running ? 'Running' : 'Stopped'}
</Box>
</MetricBubble>
</Tooltip>
{/* Total Check Cycles */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Total Check Cycles
</Typography>
<Typography variant="caption">
{dashboardData.stats.cumulative_total_check_cycles > 0
? `Total check cycles: ${dashboardData.stats.cumulative_total_check_cycles.toLocaleString()} (${dashboardData.stats.total_checks} this session). The scheduler periodically checks for due tasks.`
: `No check cycles yet. The scheduler will run its first check cycle after the interval expires (${dashboardData.stats.check_interval_minutes} minutes).`}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon">
<CheckCircleIcon fontSize="small" />
</Box>
<Box className="metric-value">
{(dashboardData.stats.cumulative_total_check_cycles !== undefined && dashboardData.stats.cumulative_total_check_cycles !== null)
? dashboardData.stats.cumulative_total_check_cycles.toLocaleString()
: dashboardData.stats.total_checks.toLocaleString()}
</Box>
<Box className="metric-label">Cycles</Box>
</MetricBubble>
</Tooltip>
{/* Tasks Executed */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Tasks Executed
</Typography>
<Typography variant="caption">
{dashboardData.stats.cumulative_tasks_executed > 0
? `Total tasks executed: ${dashboardData.stats.cumulative_tasks_executed.toLocaleString()} (${dashboardData.stats.tasks_executed} this session). ${dashboardData.stats.tasks_failed > 0 ? `${dashboardData.stats.tasks_failed} failed.` : 'All successful.'}`
: 'No tasks have been executed yet. Tasks will appear here once the scheduler starts processing them.'}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon">
<TrendingUpIcon fontSize="small" />
</Box>
<Box className="metric-value">
{(dashboardData.stats.cumulative_tasks_executed !== undefined && dashboardData.stats.cumulative_tasks_executed !== null)
? dashboardData.stats.cumulative_tasks_executed.toLocaleString()
: dashboardData.stats.tasks_executed.toLocaleString()}
</Box>
<Box className="metric-label">Executed</Box>
</MetricBubble>
</Tooltip>
{/* Tasks Found */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Tasks Found
</Typography>
<Typography variant="caption">
{dashboardData.stats.cumulative_tasks_found > 0
? `Total tasks found: ${dashboardData.stats.cumulative_tasks_found.toLocaleString()} (${dashboardData.stats.tasks_found} this session). ${dashboardData.stats.tasks_executed} executed, ${dashboardData.stats.tasks_failed} failed.`
: 'No tasks have been found yet. Tasks will appear here once they are scheduled and due for execution.'}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon">
<ScheduleIcon fontSize="small" />
</Box>
<Box className="metric-value">
{(dashboardData.stats.cumulative_tasks_found !== undefined && dashboardData.stats.cumulative_tasks_found !== null)
? dashboardData.stats.cumulative_tasks_found.toLocaleString()
: dashboardData.stats.tasks_found.toLocaleString()}
</Box>
<Box className="metric-label">Found</Box>
</MetricBubble>
</Tooltip>
{/* Check Interval */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Check Interval
</Typography>
<Typography variant="caption">
{dashboardData.stats.intelligent_scheduling
? `Intelligent scheduling is enabled. The scheduler adjusts its check interval based on active strategies: ${dashboardData.stats.active_strategies_count > 0 ? '15-30 minutes when strategies are active' : '60 minutes when no active strategies'}. Current interval: ${dashboardData.stats.check_interval_minutes} minutes.`
: `Fixed check interval: ${dashboardData.stats.check_interval_minutes} minutes. The scheduler checks for due tasks at this interval.`}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon">
<AccessTimeIcon fontSize="small" />
</Box>
<Box className="metric-value">
{dashboardData.stats.check_interval_minutes >= 60
? `${Math.floor(dashboardData.stats.check_interval_minutes / 60)}h`
: `${dashboardData.stats.check_interval_minutes}m`}
</Box>
<Box className="metric-label">Interval</Box>
</MetricBubble>
</Tooltip>
{/* Active Strategies */}
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Active Strategies
</Typography>
<Typography variant="caption">
{dashboardData.stats.active_strategies_count > 0
? `There are ${dashboardData.stats.active_strategies_count} active content strategy(ies) with monitoring tasks. The scheduler will check more frequently when strategies are active.`
: 'No active content strategies with monitoring tasks. The scheduler will check less frequently (every 60 minutes) to conserve resources.'}
</Typography>
</Box>
}
arrow
>
<MetricBubble>
<Box className="metric-icon" sx={{ color: dashboardData.stats.active_strategies_count > 0 ? '#00ff00' : '#888' }}>
<TrendingUpIcon fontSize="small" />
</Box>
<Box className="metric-value">
{dashboardData.stats.active_strategies_count}
</Box>
<Box className="metric-label">Strategies</Box>
</MetricBubble>
</Tooltip>
</Box>
)}
</Box>
</TerminalHeader>
{/* Error Alert */}
{error && (
<TerminalAlert severity="error" sx={{ mb: 3 }} onClose={() => setError(null)}>
{error}
</TerminalAlert>
)}
{/* Loading State */}
{loading && !dashboardData ? (
<TerminalLoading>
<CircularProgress />
</TerminalLoading>
) : dashboardData ? (
<>
{/* Debug Info removed - status moved to refresh icon tooltip */}
{/* Stats Cards removed - metrics moved to header as bubbles */}
{/* Jobs Tree and Failures/Insights Side by Side */}
<Box display="flex" gap={3} flexDirection={{ xs: 'column', lg: 'row' }} mb={4} alignItems="stretch">
<Box flex={2} sx={{ display: 'flex', flexDirection: 'column' }}>
{dashboardData.jobs && dashboardData.jobs.length > 0 ? (
<SchedulerJobsTree
jobs={dashboardData.jobs}
recurringJobs={dashboardData.recurring_jobs || 0}
oneTimeJobs={dashboardData.one_time_jobs || 0}
/>
) : (
<TerminalAlert severity="info">No jobs scheduled</TerminalAlert>
)}
</Box>
<Box flex={1} sx={{ display: 'flex', flexDirection: 'column' }}>
<FailuresInsights stats={dashboardData.stats} />
</Box>
</Box>
{/* Tasks Needing Intervention - Show prominently but only when needed */}
{userId && (
<Box mb={4}>
<TasksNeedingIntervention userId={userId} />
</Box>
)}
{/* Task Monitoring Tabs */}
<Box mb={4}>
<TaskMonitoringTabs />
</Box>
{/* Execution Logs */}
<Box mb={4}>
<ExecutionLogsTable initialLimit={50} />
</Box>
{/* Scheduler Event History */}
<Box mb={4}>
<SchedulerEventHistory />
</Box>
{/* Scheduler Charts Visualization */}
<Box mb={4}>
<SchedulerCharts />
</Box>
</>
) : (
<TerminalAlert severity="info">
No scheduler data available. The scheduler may not be running.
</TerminalAlert>
)}
{/* Auto-refresh indicator */}
{autoRefreshInterval && dashboardData?.stats && (
<Box mt={2} display="flex" justifyContent="center">
<TerminalChip
icon={<CheckCircleIcon sx={{ color: '#00ff00', fontSize: 14 }} />}
label={`Auto-refresh: ${dashboardData.stats.check_interval_minutes}min interval`}
size="small"
/>
</Box>
)}
</TerminalContainer>
);
};
export default SchedulerDashboard;