/** * 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(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); const [lastUpdated, setLastUpdated] = useState(null); const [autoRefreshInterval, setAutoRefreshInterval] = useState(null); const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState(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 ( ); } if (!isSignedIn) { return ( Please sign in to view the scheduler dashboard. ); } return ( {/* Header */} {/* Title Row */} SCHEDULER DASHBOARD Monitor task execution, jobs, and system status {lastUpdated && ( } /> )} Dashboard Status {dashboardData && ( <> Jobs: {dashboardData.jobs?.length || 0} total (Recurring: {dashboardData.recurring_jobs || 0}, One-Time: {dashboardData.one_time_jobs || 0}) Check Cycles: { 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)` } Scheduler: {dashboardData.stats?.running ? 'Running' : 'Stopped'} | Interval: {dashboardData.stats?.check_interval_minutes || 0} min {dashboardData.stats && dashboardData.stats.tasks_found > 0 && ( Tasks: {dashboardData.stats.tasks_found} found, {dashboardData.stats.tasks_executed} executed, {dashboardData.stats.tasks_failed} failed )} )} {!dashboardData && ( Click to refresh dashboard data )} } arrow > {/* Metrics Bubbles Row */} {dashboardData?.stats && ( {/* Scheduler Status */} Scheduler Status {dashboardData.stats.running ? 'The scheduler is currently running and actively checking for due tasks.' : 'The scheduler is stopped and not processing any tasks.'} } arrow > {dashboardData.stats.running ? : } {dashboardData.stats.running ? 'Running' : 'Stopped'} {/* Total Check Cycles */} Total Check Cycles {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).`} } arrow > {(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()} Cycles {/* Tasks Executed */} Tasks Executed {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.'} } arrow > {(dashboardData.stats.cumulative_tasks_executed !== undefined && dashboardData.stats.cumulative_tasks_executed !== null) ? dashboardData.stats.cumulative_tasks_executed.toLocaleString() : dashboardData.stats.tasks_executed.toLocaleString()} Executed {/* Tasks Found */} Tasks Found {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.'} } arrow > {(dashboardData.stats.cumulative_tasks_found !== undefined && dashboardData.stats.cumulative_tasks_found !== null) ? dashboardData.stats.cumulative_tasks_found.toLocaleString() : dashboardData.stats.tasks_found.toLocaleString()} Found {/* Check Interval */} Check Interval {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.`} } arrow > {dashboardData.stats.check_interval_minutes >= 60 ? `${Math.floor(dashboardData.stats.check_interval_minutes / 60)}h` : `${dashboardData.stats.check_interval_minutes}m`} Interval {/* Active Strategies */} Active Strategies {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.'} } arrow > 0 ? '#00ff00' : '#888' }}> {dashboardData.stats.active_strategies_count} Strategies )} {/* Error Alert */} {error && ( setError(null)}> {error} )} {/* Loading State */} {loading && !dashboardData ? ( ) : 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 */} {dashboardData.jobs && dashboardData.jobs.length > 0 ? ( ) : ( No jobs scheduled )} {/* Tasks Needing Intervention - Show prominently but only when needed */} {userId && ( )} {/* Task Monitoring Tabs */} {/* Execution Logs */} {/* Scheduler Event History */} {/* Scheduler Charts Visualization */} ) : ( No scheduler data available. The scheduler may not be running. )} {/* Auto-refresh indicator */} {autoRefreshInterval && dashboardData?.stats && ( } label={`Auto-refresh: ${dashboardData.stats.check_interval_minutes}min interval`} size="small" /> )} ); }; export default SchedulerDashboard;