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

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

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