Scheduled research persona generation
This commit is contained in:
@@ -1,44 +1,368 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ResearchWizard } from '../components/Research';
|
||||
import { BlogResearchResponse } from '../services/blogWriterApi';
|
||||
import { getResearchConfig, PersonaDefaults, refreshResearchPersona, ResearchPersona } from '../api/researchConfig';
|
||||
import { ResearchPersonaModal } from '../components/Research/ResearchPersonaModal';
|
||||
|
||||
const samplePresets = [
|
||||
{
|
||||
name: 'AI Marketing Tools',
|
||||
keywords: 'AI in marketing, automation tools, customer engagement',
|
||||
keywords: 'Research latest AI-powered marketing automation tools and customer engagement platforms',
|
||||
industry: 'Technology',
|
||||
targetAudience: 'Marketing professionals and SaaS founders',
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '🤖',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
config: {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'google' as const,
|
||||
max_sources: 15,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: true,
|
||||
include_trends: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Small Business SEO',
|
||||
keywords: 'local SEO, small business, Google My Business',
|
||||
keywords: 'Write a blog on local SEO strategies for small businesses and Google My Business optimization',
|
||||
industry: 'Marketing',
|
||||
targetAudience: 'Small business owners and local entrepreneurs',
|
||||
researchMode: 'targeted' as const,
|
||||
icon: '📈',
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
config: {
|
||||
mode: 'targeted' as const,
|
||||
provider: 'google' as const,
|
||||
max_sources: 12,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: false,
|
||||
include_competitors: true,
|
||||
include_trends: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Content Strategy',
|
||||
keywords: 'content planning, editorial calendar, content creation',
|
||||
keywords: 'Analyze content planning frameworks and editorial calendar best practices for B2B marketing',
|
||||
industry: 'Marketing',
|
||||
targetAudience: 'Content marketers and marketing managers',
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '✍️',
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
config: {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 20,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: false,
|
||||
include_trends: true,
|
||||
exa_category: 'research paper',
|
||||
exa_search_type: 'neural' as const,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Crypto Trends',
|
||||
keywords: 'Explore cryptocurrency market trends and blockchain adoption in enterprise',
|
||||
industry: 'Finance',
|
||||
targetAudience: 'Investors and blockchain developers',
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '₿',
|
||||
gradient: 'linear-gradient(135deg, #f7931a 0%, #ffa94d 100%)',
|
||||
config: {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 25,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: true,
|
||||
include_trends: true,
|
||||
exa_category: 'news',
|
||||
exa_search_type: 'neural' as const,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Healthcare Tech',
|
||||
keywords: 'Research telemedicine platforms and remote patient monitoring technologies',
|
||||
industry: 'Healthcare',
|
||||
targetAudience: 'Healthcare administrators and medical professionals',
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '⚕️',
|
||||
gradient: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%)',
|
||||
config: {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 20,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: false,
|
||||
include_trends: true,
|
||||
exa_category: 'research paper',
|
||||
exa_search_type: 'neural' as const,
|
||||
exa_include_domains: ['pubmed.gov', 'nejm.org', 'thelancet.com'],
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// Generate persona-specific presets dynamically
|
||||
const generatePersonaPresets = (persona: PersonaDefaults | null): typeof samplePresets => {
|
||||
if (!persona || !persona.industry || persona.industry === 'General') {
|
||||
return samplePresets;
|
||||
}
|
||||
|
||||
const industry = persona.industry;
|
||||
const audience = persona.target_audience || 'professionals';
|
||||
const exaCategory = persona.suggested_exa_category || '';
|
||||
const exaDomains = persona.suggested_domains || [];
|
||||
|
||||
// Build config objects conditionally based on whether we have Exa options
|
||||
const baseConfig1: any = {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 20,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: true,
|
||||
include_trends: true,
|
||||
exa_search_type: 'neural' as const,
|
||||
...(exaCategory ? { exa_category: exaCategory } : {}),
|
||||
...(exaDomains.length > 0 ? { exa_include_domains: exaDomains } : {}),
|
||||
};
|
||||
|
||||
const baseConfig2: any = {
|
||||
mode: 'targeted' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 15,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: false,
|
||||
include_trends: true,
|
||||
exa_search_type: 'neural' as const,
|
||||
...(exaCategory ? { exa_category: exaCategory } : {}),
|
||||
...(exaDomains.length > 0 ? { exa_include_domains: exaDomains } : {}),
|
||||
};
|
||||
|
||||
const baseConfig3: any = {
|
||||
mode: 'comprehensive' as const,
|
||||
provider: 'exa' as const,
|
||||
max_sources: 18,
|
||||
include_statistics: true,
|
||||
include_expert_quotes: true,
|
||||
include_competitors: true,
|
||||
include_trends: false,
|
||||
exa_search_type: 'neural' as const,
|
||||
...(exaCategory ? { exa_category: exaCategory } : {}),
|
||||
...(exaDomains.length > 0 ? { exa_include_domains: exaDomains } : {}),
|
||||
};
|
||||
|
||||
const generatedPresets = [
|
||||
{
|
||||
name: `${industry} Trends`,
|
||||
keywords: `Research latest trends and innovations in ${industry}`,
|
||||
industry,
|
||||
targetAudience: audience,
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '📊',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
config: baseConfig1,
|
||||
},
|
||||
{
|
||||
name: `${audience} Insights`,
|
||||
keywords: `Analyze ${audience} pain points and preferences in ${industry}`,
|
||||
industry,
|
||||
targetAudience: audience,
|
||||
researchMode: 'targeted' as const,
|
||||
icon: '🎯',
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
config: baseConfig2,
|
||||
},
|
||||
{
|
||||
name: `${industry} Best Practices`,
|
||||
keywords: `Investigate best practices and success stories in ${industry}`,
|
||||
industry,
|
||||
targetAudience: audience,
|
||||
researchMode: 'comprehensive' as const,
|
||||
icon: '⭐',
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
config: baseConfig3,
|
||||
}
|
||||
];
|
||||
|
||||
return [...generatedPresets, ...samplePresets.slice(0, 2)] as typeof samplePresets;
|
||||
};
|
||||
|
||||
export const ResearchTest: React.FC = () => {
|
||||
const [results, setResults] = useState<BlogResearchResponse | null>(null);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const [presetKeywords, setPresetKeywords] = useState<string[] | undefined>();
|
||||
const [presetIndustry, setPresetIndustry] = useState<string | undefined>();
|
||||
const [presetTargetAudience, setPresetTargetAudience] = useState<string | undefined>();
|
||||
const [presetMode, setPresetMode] = useState<any>();
|
||||
const [presetConfig, setPresetConfig] = useState<any>();
|
||||
const [personaData, setPersonaData] = useState<PersonaDefaults | null>(null);
|
||||
const [displayPresets, setDisplayPresets] = useState(samplePresets);
|
||||
const [showPersonaModal, setShowPersonaModal] = useState(false);
|
||||
const [personaChecked, setPersonaChecked] = useState(false);
|
||||
const [researchPersona, setResearchPersona] = useState<ResearchPersona | null>(null);
|
||||
|
||||
// Debug: Track modal state changes
|
||||
useEffect(() => {
|
||||
console.log('[ResearchTest] 🔍 Modal state changed:', showPersonaModal);
|
||||
}, [showPersonaModal]);
|
||||
|
||||
// Check for research persona and load persona data
|
||||
useEffect(() => {
|
||||
const loadPersonaPresets = async () => {
|
||||
console.log('[ResearchTest] Starting persona check...');
|
||||
try {
|
||||
const config = await getResearchConfig();
|
||||
console.log('[ResearchTest] Config received:', {
|
||||
hasResearchPersona: !!config.research_persona,
|
||||
onboardingCompleted: config.onboarding_completed,
|
||||
personaScheduled: config.persona_scheduled,
|
||||
personaDefaults: config.persona_defaults
|
||||
});
|
||||
|
||||
setPersonaData(config.persona_defaults || null);
|
||||
|
||||
// CASE 1: Research persona exists in database
|
||||
if (config.research_persona) {
|
||||
console.log('[ResearchTest] ✅ CASE 1: Research persona found in database');
|
||||
console.log('[ResearchTest] Persona details:', {
|
||||
defaultIndustry: config.research_persona.default_industry,
|
||||
defaultTargetAudience: config.research_persona.default_target_audience,
|
||||
hasRecommendedPresets: !!config.research_persona.recommended_presets,
|
||||
presetCount: config.research_persona.recommended_presets?.length || 0
|
||||
});
|
||||
|
||||
setResearchPersona(config.research_persona);
|
||||
|
||||
// Use AI-generated presets if persona exists
|
||||
if (config.research_persona.recommended_presets && config.research_persona.recommended_presets.length > 0) {
|
||||
console.log('[ResearchTest] Using AI-generated presets from persona');
|
||||
// Convert AI presets to display format
|
||||
const aiPresets = config.research_persona.recommended_presets.map((preset: any) => ({
|
||||
name: preset.name,
|
||||
keywords: preset.keywords.join(', '),
|
||||
industry: config.persona_defaults?.industry || 'General',
|
||||
targetAudience: config.persona_defaults?.target_audience || 'General',
|
||||
researchMode: preset.config?.mode || 'comprehensive',
|
||||
icon: preset.icon || '🔍',
|
||||
gradient: preset.gradient || 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
config: preset.config || {}
|
||||
}));
|
||||
setDisplayPresets([...aiPresets, ...samplePresets.slice(0, 2)]);
|
||||
} else {
|
||||
console.log('[ResearchTest] Persona exists but no recommended presets, using rule-based presets');
|
||||
const dynamicPresets = generatePersonaPresets(config.persona_defaults || null);
|
||||
setDisplayPresets(dynamicPresets);
|
||||
}
|
||||
} else {
|
||||
// CASE 2 & 3: No research persona found
|
||||
console.log('[ResearchTest] ⚠️ CASE 2/3: Research persona NOT found in database');
|
||||
console.log('[ResearchTest] Onboarding status:', {
|
||||
onboardingCompleted: config.onboarding_completed,
|
||||
personaScheduled: config.persona_scheduled
|
||||
});
|
||||
|
||||
const dynamicPresets = generatePersonaPresets(config.persona_defaults || null);
|
||||
setDisplayPresets(dynamicPresets);
|
||||
|
||||
// Show modal only if onboarding is completed
|
||||
if (config.onboarding_completed) {
|
||||
console.log('[ResearchTest] ✅ CASE 2: Onboarding completed but persona missing - SHOWING MODAL');
|
||||
console.log('[ResearchTest] Setting showPersonaModal to true');
|
||||
setShowPersonaModal(true);
|
||||
|
||||
// Log if persona was scheduled
|
||||
if (config.persona_scheduled) {
|
||||
console.log('[ResearchTest] ℹ️ Research persona generation scheduled for 20 minutes from now');
|
||||
} else {
|
||||
console.log('[ResearchTest] ⚠️ Persona was not scheduled (may have failed or already scheduled)');
|
||||
}
|
||||
} else {
|
||||
console.log('[ResearchTest] ✅ CASE 3: Onboarding not completed yet - SKIPPING modal');
|
||||
console.log('[ResearchTest] User has not completed onboarding, will use rule-based suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
setPersonaChecked(true);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('[ResearchTest] ❌ ERROR: Failed to load persona data:', error);
|
||||
console.error('[ResearchTest] Error details:', errorMessage);
|
||||
|
||||
// Use fallback presets on error
|
||||
setDisplayPresets(samplePresets);
|
||||
setPersonaChecked(true);
|
||||
|
||||
// Don't show modal on error - user can still use default presets
|
||||
// Error is already logged to console for debugging
|
||||
}
|
||||
};
|
||||
|
||||
loadPersonaPresets();
|
||||
}, []);
|
||||
|
||||
// Handle research persona generation
|
||||
const handleGeneratePersona = async () => {
|
||||
console.log('[ResearchTest] 🔄 User clicked "Generate Persona" - starting generation...');
|
||||
try {
|
||||
// Force refresh to generate new persona
|
||||
console.log('[ResearchTest] Calling refreshResearchPersona with force_refresh=true');
|
||||
const persona = await refreshResearchPersona(true);
|
||||
console.log('[ResearchTest] ✅ Persona generated successfully:', {
|
||||
defaultIndustry: persona.default_industry,
|
||||
hasRecommendedPresets: !!persona.recommended_presets
|
||||
});
|
||||
|
||||
setResearchPersona(persona);
|
||||
|
||||
// Reload config to get updated presets
|
||||
const config = await getResearchConfig();
|
||||
if (config.research_persona?.recommended_presets && config.research_persona.recommended_presets.length > 0) {
|
||||
console.log('[ResearchTest] Updating presets with AI-generated presets');
|
||||
const aiPresets = config.research_persona.recommended_presets.map((preset: any) => ({
|
||||
name: preset.name,
|
||||
keywords: preset.keywords.join(', '),
|
||||
industry: config.persona_defaults.industry || 'General',
|
||||
targetAudience: config.persona_defaults.target_audience || 'General',
|
||||
researchMode: preset.config?.mode || 'comprehensive',
|
||||
icon: preset.icon || '🔍',
|
||||
gradient: preset.gradient || 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
config: preset.config || {}
|
||||
}));
|
||||
setDisplayPresets([...aiPresets, ...samplePresets.slice(0, 2)]);
|
||||
}
|
||||
|
||||
console.log('[ResearchTest] ✅ Persona generation complete - closing modal');
|
||||
setShowPersonaModal(false);
|
||||
} catch (error) {
|
||||
console.error('[ResearchTest] ❌ Failed to generate research persona:', error);
|
||||
console.error('[ResearchTest] Error details:', error instanceof Error ? error.message : String(error));
|
||||
throw error; // Let modal handle the error display
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel - user chooses to skip persona generation
|
||||
const handleCancelPersona = () => {
|
||||
console.log('[ResearchTest] ✅ CASE 3: User cancelled persona generation');
|
||||
console.log('[ResearchTest] Continuing with rule-based suggestions');
|
||||
setShowPersonaModal(false);
|
||||
// Continue with rule-based suggestions (already set as displayPresets)
|
||||
};
|
||||
|
||||
const handleComplete = (researchResults: BlogResearchResponse) => {
|
||||
setResults(researchResults);
|
||||
};
|
||||
|
||||
const handlePresetClick = (preset: typeof samplePresets[0]) => {
|
||||
setPresetKeywords(preset.keywords.split(',').map(k => k.trim()));
|
||||
// Pass full research query as single keyword for intelligent parsing
|
||||
setPresetKeywords([preset.keywords]);
|
||||
setPresetIndustry(preset.industry);
|
||||
setPresetTargetAudience(preset.targetAudience);
|
||||
setPresetMode(preset.researchMode);
|
||||
setPresetConfig(preset.config);
|
||||
setResults(null);
|
||||
};
|
||||
|
||||
@@ -212,7 +536,7 @@ export const ResearchTest: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{samplePresets.map((preset, idx) => (
|
||||
{displayPresets.map((preset, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handlePresetClick(preset)}
|
||||
@@ -374,6 +698,9 @@ export const ResearchTest: React.FC = () => {
|
||||
<ResearchWizard
|
||||
initialKeywords={presetKeywords}
|
||||
initialIndustry={presetIndustry}
|
||||
initialTargetAudience={presetTargetAudience}
|
||||
initialResearchMode={presetMode}
|
||||
initialConfig={presetConfig}
|
||||
onComplete={handleComplete}
|
||||
/>
|
||||
</div>
|
||||
@@ -521,6 +848,17 @@ export const ResearchTest: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Research Persona Generation Modal */}
|
||||
<ResearchPersonaModal
|
||||
open={showPersonaModal}
|
||||
onClose={() => {
|
||||
console.log('[ResearchTest] Modal onClose called');
|
||||
setShowPersonaModal(false);
|
||||
}}
|
||||
onGenerate={handleGeneratePersona}
|
||||
onCancel={handleCancelPersona}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
701
frontend/src/pages/SchedulerDashboard.tsx
Normal file
701
frontend/src/pages/SchedulerDashboard.tsx
Normal file
@@ -0,0 +1,701 @@
|
||||
/**
|
||||
* 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 {
|
||||
Refresh as RefreshIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Pause as PauseIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
AccessTime as AccessTimeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import { getSchedulerDashboard, SchedulerDashboardData } from '../api/schedulerDashboard';
|
||||
// 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 OAuthTokenStatus from '../components/SchedulerDashboard/OAuthTokenStatus';
|
||||
import { TerminalTypography, terminalColors } from '../components/SchedulerDashboard/terminalTheme';
|
||||
|
||||
// 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 } = 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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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>
|
||||
</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>
|
||||
|
||||
{/* OAuth Token Status */}
|
||||
<Box mb={4}>
|
||||
<OAuthTokenStatus compact={true} />
|
||||
</Box>
|
||||
|
||||
{/* Execution Logs */}
|
||||
<Box mb={4}>
|
||||
<ExecutionLogsTable initialLimit={50} />
|
||||
</Box>
|
||||
|
||||
{/* Scheduler Event History */}
|
||||
<Box mb={4}>
|
||||
<SchedulerEventHistory limit={50} />
|
||||
</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;
|
||||
Reference in New Issue
Block a user