Merge branch 'pr-399'

This commit is contained in:
ajaysi
2026-03-09 14:26:13 +05:30
5 changed files with 298 additions and 363 deletions

View File

@@ -529,77 +529,76 @@ async def get_semantic_cache_stats(current_user: dict = Depends(get_current_user
async def get_sif_indexing_health(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
try:
from models.website_analysis_monitoring_models import SIFIndexingTask, SIFIndexingExecutionLog
user_id = str(current_user.get("id"))
db = get_session_for_user(user_id)
if not db:
db_session = get_session_for_user(user_id)
if not db_session:
raise HTTPException(status_code=500, detail="Database connection unavailable")
try:
tasks = (
db.query(SIFIndexingTask)
.filter(SIFIndexingTask.user_id == user_id)
.order_by(SIFIndexingTask.created_at.desc())
.all()
)
dashboard_service = SEODashboardService(db_session)
onboarding_task_health = await dashboard_service.get_onboarding_scheduled_task_health(user_id)
sif_health = onboarding_task_health.get("tasks", {}).get("SIFIndexingTask", {})
if not tasks:
if sif_health.get("status") == "not_scheduled":
return {
"has_task": False,
"status": "not_scheduled",
"message": "SIF indexing task not yet scheduled for this website.",
}
latest = tasks[0]
latest_log = (
db.query(SIFIndexingExecutionLog)
.filter(SIFIndexingExecutionLog.task_id == latest.id)
.order_by(SIFIndexingExecutionLog.execution_date.desc())
.first()
)
last_run_status = latest_log.status if latest_log else None
last_run_time = (
latest_log.execution_date.isoformat() if latest_log and latest_log.execution_date else None
)
last_error = (
(latest_log.error_message or "")[:500] if latest_log and latest_log.error_message else None
)
overall_status = "healthy"
if latest.consecutive_failures and latest.consecutive_failures > 0:
if (sif_health.get("consecutive_failures") or 0) > 0:
overall_status = "warning"
if latest.status in {"needs_intervention"}:
if sif_health.get("status") in {"failed", "needs_intervention"}:
overall_status = "critical"
return {
"has_task": True,
"status": overall_status,
"task": {
"id": latest.id,
"website_url": latest.website_url,
"raw_status": latest.status,
"next_execution": latest.next_execution.isoformat() if latest.next_execution else None,
"last_success": latest.last_success.isoformat() if latest.last_success else None,
"last_failure": latest.last_failure.isoformat() if latest.last_failure else None,
"consecutive_failures": latest.consecutive_failures or 0,
"failure_pattern": latest.failure_pattern,
"raw_status": sif_health.get("status"),
"next_execution": sif_health.get("next_execution"),
"last_success": sif_health.get("last_success"),
"last_failure": sif_health.get("last_failure"),
"consecutive_failures": sif_health.get("consecutive_failures") or 0,
},
"last_run": {
"status": last_run_status,
"time": last_run_time,
"error_message": last_error,
"status": sif_health.get("latest_execution", {}).get("status"),
"time": sif_health.get("latest_execution", {}).get("execution_date"),
"error_message": sif_health.get("latest_execution", {}).get("error_message"),
},
}
finally:
db.close()
db_session.close()
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get SIF indexing health: {e}")
raise HTTPException(status_code=500, detail="Failed to get SIF indexing health")
async def get_onboarding_task_health(
current_user: dict = Depends(get_current_user),
site_url: Optional[str] = None,
) -> Dict[str, Any]:
"""Get consolidated onboarding scheduled SEO task health."""
try:
user_id = str(current_user.get("id"))
db_session = get_session_for_user(user_id)
if not db_session:
raise HTTPException(status_code=500, detail="Database connection unavailable")
try:
dashboard_service = SEODashboardService(db_session)
return await dashboard_service.get_onboarding_scheduled_task_health(user_id, site_url)
finally:
db_session.close()
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get onboarding task health: {e}")
raise HTTPException(status_code=500, detail="Failed to get onboarding scheduled task health")
# New comprehensive SEO analysis endpoints
async def analyze_seo_comprehensive(request: SEOAnalysisRequest) -> SEOAnalysisResponse:
"""

View File

@@ -130,7 +130,8 @@ from api.seo_dashboard import (
analyze_urls_ai,
AnalyzeURLsRequest,
get_analyzed_pages,
get_semantic_health # Phase 2B: Semantic health monitoring
get_semantic_health, # Phase 2B: Semantic health monitoring
get_onboarding_task_health,
)
@@ -316,6 +317,13 @@ async def refresh_analytics_data_endpoint(current_user: dict = Depends(get_curre
"""Refresh analytics data by invalidating cache and fetching fresh data."""
return await refresh_analytics_data(current_user, site_url)
@app.get("/api/seo-dashboard/onboarding-task-health")
async def onboarding_task_health_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None):
"""Get consolidated health for onboarding-scheduled SEO tasks."""
return await get_onboarding_task_health(current_user, site_url)
@app.get("/api/seo-dashboard/health")
async def seo_dashboard_health():
"""Health check for SEO dashboard."""

View File

@@ -6,7 +6,7 @@ and other analytics sources for the SEO dashboard. Leverages existing
OAuth connections from onboarding step 5.
"""
from typing import Dict, Any, Optional, List
from typing import Dict, Any, Optional, List, Type
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func
@@ -21,7 +21,16 @@ from api.content_planning.services.content_strategy.onboarding.data_integration
from .analytics_aggregator import AnalyticsAggregator
from .competitive_analyzer import CompetitiveAnalyzer
from models.onboarding import SEOPageAudit, WebsiteAnalysis, OnboardingSession
from models.website_analysis_monitoring_models import OnboardingFullWebsiteAnalysisTask
from models.website_analysis_monitoring_models import (
OnboardingFullWebsiteAnalysisTask,
OnboardingFullWebsiteAnalysisExecutionLog,
DeepCompetitorAnalysisTask,
DeepCompetitorAnalysisExecutionLog,
SIFIndexingTask,
SIFIndexingExecutionLog,
MarketTrendsTask,
MarketTrendsExecutionLog,
)
from models.advertools_monitoring_models import AdvertoolsTask
logger = get_service_logger("seo_dashboard")
@@ -209,6 +218,118 @@ class SEODashboardService:
"fix_scheduled_pages": 0,
"worst_pages": []
}
async def get_onboarding_scheduled_task_health(
self,
user_id: str,
site_url: Optional[str] = None,
) -> Dict[str, Any]:
"""Return consolidated health for all onboarding scheduled SEO jobs."""
site_key = (site_url or "").rstrip("/")
task_matrix = {
"OnboardingFullWebsiteAnalysisTask": {
"label": "Onboarding Full Website Analysis",
"task_model": OnboardingFullWebsiteAnalysisTask,
"log_model": OnboardingFullWebsiteAnalysisExecutionLog,
},
"DeepCompetitorAnalysisTask": {
"label": "Deep Competitor Analysis",
"task_model": DeepCompetitorAnalysisTask,
"log_model": DeepCompetitorAnalysisExecutionLog,
},
"SIFIndexingTask": {
"label": "SIF Indexing",
"task_model": SIFIndexingTask,
"log_model": SIFIndexingExecutionLog,
},
"MarketTrendsTask": {
"label": "Market Trends",
"task_model": MarketTrendsTask,
"log_model": MarketTrendsExecutionLog,
},
}
task_health: Dict[str, Any] = {}
for task_name, config in task_matrix.items():
task_health[task_name] = self._get_single_task_health(
user_id=user_id,
task_model=config["task_model"],
log_model=config["log_model"],
label=config["label"],
site_key=site_key,
)
return {
"status": "ok",
"website_url": site_key or None,
"tasks": task_health,
"last_updated": datetime.utcnow().isoformat(),
}
def _get_single_task_health(
self,
user_id: str,
task_model: Type[Any],
log_model: Type[Any],
label: str,
site_key: str,
) -> Dict[str, Any]:
query = self.db.query(task_model).filter(task_model.user_id == str(user_id))
if site_key:
query = query.filter(task_model.website_url.like(f"{site_key}%"))
task = query.order_by(task_model.updated_at.desc()).first()
if not task:
return {
"label": label,
"status": "not_scheduled",
"next_execution": None,
"last_success": None,
"last_failure": None,
"consecutive_failures": 0,
"latest_execution": None,
}
latest_log = (
self.db.query(log_model)
.filter(log_model.task_id == task.id)
.order_by(log_model.execution_date.desc())
.first()
)
log_summary = None
if latest_log:
log_summary = {
"status": latest_log.status,
"execution_date": latest_log.execution_date.isoformat() if latest_log.execution_date else None,
"execution_time_ms": latest_log.execution_time_ms,
"error_message": (latest_log.error_message or "")[:500] if latest_log.error_message else None,
"result_summary": self._summarize_execution_result(latest_log.result_data),
}
return {
"label": label,
"status": task.status or "not_scheduled",
"next_execution": task.next_execution.isoformat() if task.next_execution else None,
"last_success": task.last_success.isoformat() if task.last_success else None,
"last_failure": task.last_failure.isoformat() if task.last_failure else None,
"consecutive_failures": task.consecutive_failures or 0,
"latest_execution": log_summary,
}
def _summarize_execution_result(self, result_data: Any) -> Optional[str]:
if not isinstance(result_data, dict):
return None
for key in ("summary", "message", "status_message", "note"):
value = result_data.get(key)
if isinstance(value, str) and value.strip():
return value[:300]
if result_data:
return f"Result keys: {', '.join(sorted(result_data.keys())[:6])}"
return None
async def get_gsc_data(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
"""Get GSC data for the specified site."""

View File

@@ -56,6 +56,33 @@ export interface SIFIndexingHealth {
};
}
export type OnboardingTaskStatus = 'active' | 'failed' | 'paused' | 'needs_intervention' | 'not_scheduled';
export interface OnboardingScheduledTaskHealthItem {
label: string;
status: OnboardingTaskStatus;
next_execution: string | null;
last_success: string | null;
last_failure: string | null;
consecutive_failures: number;
latest_execution: {
status: string | null;
execution_date: string | null;
execution_time_ms: number | null;
error_message: string | null;
result_summary: string | null;
} | null;
}
export interface OnboardingScheduledTaskHealthResponse {
status: string;
website_url?: string | null;
tasks: Record<string, OnboardingScheduledTaskHealthItem>;
last_updated: string;
}
export interface SEODashboardData {
health_score: SEOHealthScore;
key_insight: string;
@@ -172,5 +199,18 @@ export const seoDashboardAPI = {
console.error('Error fetching SIF indexing health:', error);
throw error;
}
},
async getOnboardingTaskHealth(siteUrl?: string): Promise<OnboardingScheduledTaskHealthResponse> {
try {
const response = await apiClient.get('/api/seo-dashboard/onboarding-task-health', {
params: siteUrl ? { site_url: siteUrl } : undefined
});
return response.data;
} catch (error) {
console.error('Error fetching onboarding task health:', error);
throw error;
}
}
};

View File

@@ -18,7 +18,6 @@ import {
AccordionSummary,
AccordionDetails,
CircularProgress,
Drawer
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth, useUser, SignOutButton, useClerk } from '@clerk/clerk-react';
@@ -33,7 +32,6 @@ import {
Schedule as ScheduleIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
Close as CloseIcon,
AutoAwesome as AIIcon
} from '@mui/icons-material';
@@ -50,8 +48,7 @@ import { useSEODashboardStore } from '../../stores/seoDashboardStore';
// API
import { userDataAPI } from '../../api/userData';
import { SIFIndexingHealth } from '../../api/seoDashboard';
import { getSchedulerDashboard, SchedulerJob } from '../../api/schedulerDashboard';
import { OnboardingScheduledTaskHealthResponse, OnboardingTaskStatus } from '../../api/seoDashboard';
// Shared components
import PlatformAnalytics from '../shared/PlatformAnalytics';
@@ -121,11 +118,7 @@ const SEODashboard: React.FC = () => {
const [competitiveSitemapBenchmarkingReport, setCompetitiveSitemapBenchmarkingReport] = useState<any>(null);
const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false);
const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState<string | null>(null);
const [sifHealth, setSifHealth] = useState<SIFIndexingHealth | null>(null);
const [sifDetailsOpen, setSifDetailsOpen] = useState(false);
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJob[] | null>(null);
const [schedulerJobsLoading, setSchedulerJobsLoading] = useState(false);
const [schedulerJobsError, setSchedulerJobsError] = useState<string | null>(null);
const [onboardingTaskHealth, setOnboardingTaskHealth] = useState<OnboardingScheduledTaskHealthResponse | null>(null);
// PlatformAnalytics refresh handle
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
@@ -147,26 +140,6 @@ const SEODashboard: React.FC = () => {
fetchStrategicInsightsHistory();
}, []);
useEffect(() => {
if (!sifDetailsOpen || schedulerJobs || schedulerJobsLoading) return;
setSchedulerJobsLoading(true);
(async () => {
try {
const dashboard = await getSchedulerDashboard();
const currentUserId = dashboard.user_isolation?.current_user_id || null;
const filtered = dashboard.jobs.filter((job) =>
currentUserId ? job.user_id === currentUserId : Boolean(job.user_id)
);
setSchedulerJobs(filtered);
setSchedulerJobsError(null);
} catch (e) {
console.error('Failed to load scheduler jobs for SEO dashboard:', e);
setSchedulerJobsError('Failed to load scheduler jobs');
} finally {
setSchedulerJobsLoading(false);
}
})();
}, [sifDetailsOpen, schedulerJobs, schedulerJobsLoading]);
const fetchStrategicInsightsHistory = async () => {
setStrategicInsightsLoading(true);
@@ -260,16 +233,16 @@ const SEODashboard: React.FC = () => {
setLoading(true);
// Fetch platform status and user data in parallel
const [platformResponse, userData, sifHealthResponse] = await Promise.all([
const [platformResponse, userData, onboardingTaskHealthResponse] = await Promise.all([
apiClient.get('/api/seo-dashboard/platforms'),
userDataAPI.getUserData(),
apiClient.get('/api/seo-dashboard/sif-health')
apiClient.get('/api/seo-dashboard/onboarding-task-health')
]);
console.log('Platform status response:', platformResponse.status, platformResponse.statusText);
console.log('Platform status data:', platformResponse.data);
setPlatformStatus(platformResponse.data);
setSifHealth(sifHealthResponse.data);
setOnboardingTaskHealth(onboardingTaskHealthResponse.data);
websiteUrl = userData?.website_url || 'https://alwrity.com';
@@ -499,6 +472,23 @@ const SEODashboard: React.FC = () => {
}
// Show sign-in prompt if not authenticated
const statusUiMap: Record<OnboardingTaskStatus, { label: string; color: string; bg: string; border: string; action: string }> = {
active: { label: 'Active', color: '#22c55e', bg: 'rgba(34,197,94,0.12)', border: 'rgba(34,197,94,0.4)', action: 'No action needed. Monitor next execution to confirm regular runs.' },
failed: { label: 'Failed', color: '#ef4444', bg: 'rgba(239,68,68,0.12)', border: 'rgba(239,68,68,0.4)', action: 'Review the latest error and rerun after fixing data/source issues.' },
paused: { label: 'Paused', color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', border: 'rgba(245,158,11,0.4)', action: 'Resume this task from scheduler controls when ready.' },
needs_intervention: { label: 'Needs intervention', color: '#f97316', bg: 'rgba(249,115,22,0.12)', border: 'rgba(249,115,22,0.4)', action: 'Immediate action required. Inspect failures and reconfigure before retrying.' },
not_scheduled: { label: 'Not scheduled', color: '#94a3b8', bg: 'rgba(148,163,184,0.12)', border: 'rgba(148,163,184,0.4)', action: 'Complete onboarding scheduling or create the task in scheduler.' }
};
const orderedTaskKeys = [
'OnboardingFullWebsiteAnalysisTask',
'DeepCompetitorAnalysisTask',
'SIFIndexingTask',
'MarketTrendsTask'
];
if (!isLoaded) {
return <Skeleton variant="rectangular" height={200} />;
}
@@ -622,67 +612,18 @@ const SEODashboard: React.FC = () => {
}}
/>
</Tooltip>
{sifHealth && (
<Tooltip title="Semantic Indexing Status (SIF)">
<Box
onClick={() => setSifDetailsOpen(true)}
{onboardingTaskHealth && (
<Tooltip title="Onboarding Scheduled SEO Tasks">
<Chip
label={`Onboarding Tasks: ${Object.values(onboardingTaskHealth.tasks || {}).filter((task: any) => task?.status === 'active').length} active`}
size="small"
sx={{
ml: 2,
px: 2,
py: 0.75,
borderRadius: 999,
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
bgcolor:
sifHealth.status === 'healthy'
? 'rgba(76, 175, 80, 0.15)'
: sifHealth.status === 'warning'
? 'rgba(255, 152, 0, 0.15)'
: sifHealth.status === 'critical'
? 'rgba(244, 67, 54, 0.15)'
: 'rgba(255, 255, 255, 0.05)',
border:
sifHealth.status === 'healthy'
? '1px solid rgba(76, 175, 80, 0.5)'
: sifHealth.status === 'warning'
? '1px solid rgba(255, 152, 0, 0.5)'
: sifHealth.status === 'critical'
? '1px solid rgba(244, 67, 54, 0.5)'
: '1px solid rgba(255, 255, 255, 0.15)',
bgcolor: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor:
sifHealth.status === 'healthy'
? '#4CAF50'
: sifHealth.status === 'warning'
? '#FF9800'
: sifHealth.status === 'critical'
? '#F44336'
: 'rgba(255, 255, 255, 0.5)',
}}
/>
<Typography
variant="body2"
sx={{ color: 'rgba(255, 255, 255, 0.9)', fontWeight: 500 }}
>
Semantic Indexing:{' '}
{sifHealth.status === 'not_scheduled'
? 'Not scheduled yet'
: sifHealth.status === 'healthy'
? 'Up to date'
: sifHealth.status === 'warning'
? 'Issues detected'
: 'Needs intervention'}
</Typography>
</Box>
/>
</Tooltip>
)}
</Box>
@@ -1524,6 +1465,61 @@ const SEODashboard: React.FC = () => {
</Box>
)}
{onboardingTaskHealth && (
<Box sx={{ mt: 4 }}>
<GlassCard>
<Box sx={{ p: 3 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 700, mb: 0.5 }}>
Onboarding Scheduled SEO Tasks
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 2 }}>
Unified health view for onboarding automation jobs.
</Typography>
<Grid container spacing={2}>
{orderedTaskKeys.map((taskKey) => {
const task = onboardingTaskHealth.tasks?.[taskKey];
if (!task) return null;
const status = (task.status || 'not_scheduled') as OnboardingTaskStatus;
const ui = statusUiMap[status] || statusUiMap.not_scheduled;
return (
<Grid item xs={12} md={6} key={taskKey}>
<Box sx={{ p: 2, borderRadius: 2, border: `1px solid ${ui.border}`, bgcolor: 'rgba(15,23,42,0.5)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 600 }}>
{task.label || taskKey}
</Typography>
<Chip size="small" label={ui.label} sx={{ color: ui.color, bgcolor: ui.bg, border: `1px solid ${ui.border}` }} />
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.75)', display: 'block' }}>
Next: {task.next_execution ? new Date(task.next_execution).toLocaleString() : 'Not scheduled'}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.75)', display: 'block' }}>
Last success: {task.last_success ? new Date(task.last_success).toLocaleString() : 'No successful runs yet'}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.75)', display: 'block' }}>
Last failure: {task.last_failure ? new Date(task.last_failure).toLocaleString() : 'No failure recorded'}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.75)', display: 'block', mb: 1 }}>
Consecutive failures: {task.consecutive_failures ?? 0}
</Typography>
<Alert severity={status === 'active' ? 'success' : status === 'not_scheduled' ? 'info' : 'warning'} sx={{ bgcolor: 'rgba(15,23,42,0.65)', color: 'white', border: `1px solid ${ui.border}` }}>
{ui.action}
{task.latest_execution?.error_message ? ` Latest error: ${task.latest_execution.error_message}` : ''}
</Alert>
</Box>
</Grid>
);
})}
</Grid>
</Box>
</GlassCard>
</Box>
)}
{/* SEO Analyzer Panel */}
<SEOAnalyzerPanel
analysisData={analysisData}
@@ -1543,236 +1539,7 @@ const SEODashboard: React.FC = () => {
</AnimatePresence>
</Container>
<Drawer
anchor="right"
open={sifDetailsOpen}
onClose={() => setSifDetailsOpen(false)}
PaperProps={{
sx: {
width: { xs: '100%', sm: 360 },
maxWidth: '100vw',
bgcolor: 'rgba(15, 23, 42, 0.98)',
color: 'white'
}
}}
>
<Box
sx={{
p: 2,
borderBottom: 1,
borderColor: 'rgba(148, 163, 184, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<Typography variant="h6">Semantic Indexing Details</Typography>
<IconButton onClick={() => setSifDetailsOpen(false)} sx={{ color: 'rgba(148,163,184,0.9)' }}>
<CloseIcon />
</IconButton>
</Box>
<Box sx={{ p: 2 }}>
{sifHealth ? (
<>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Overall Status
</Typography>
<Typography variant="body1">
{sifHealth.status === 'not_scheduled'
? 'Not scheduled'
: sifHealth.status === 'healthy'
? 'Healthy'
: sifHealth.status === 'warning'
? 'Warning'
: 'Critical'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Next Scheduled Run
</Typography>
<Typography variant="body1">
{sifHealth.task?.next_execution
? new Date(sifHealth.task.next_execution).toLocaleString()
: 'Not scheduled'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Success
</Typography>
<Typography variant="body1">
{sifHealth.task?.last_success
? new Date(sifHealth.task.last_success).toLocaleString()
: 'No successful runs yet'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Failure
</Typography>
<Typography variant="body1">
{sifHealth.task?.last_failure
? new Date(sifHealth.task.last_failure).toLocaleString()
: 'No failures recorded'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Consecutive Failures
</Typography>
<Typography variant="body1">
{sifHealth.task?.consecutive_failures ?? 0}
</Typography>
</Box>
{sifHealth.last_run?.status && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Run Status
</Typography>
<Typography variant="body1">
{sifHealth.last_run.status}
{sifHealth.last_run.time
? `${new Date(sifHealth.last_run.time).toLocaleString()}`
: ''}
</Typography>
</Box>
)}
{sifHealth.last_run?.error_message && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(248,113,113,0.9)', mb: 0.5 }}>
Last Error (snippet)
</Typography>
<Typography
variant="body2"
sx={{
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
fontSize: 12,
bgcolor: 'rgba(15,23,42,0.9)',
borderRadius: 1,
p: 1.5,
border: '1px solid rgba(148,163,184,0.3)'
}}
>
{sifHealth.last_run.error_message}
</Typography>
</Box>
)}
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" sx={{ color: 'white', mb: 1 }}>
Scheduled Jobs For Your Account
</Typography>
{schedulerJobsLoading && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} sx={{ color: 'rgba(148,163,184,0.9)' }} />
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
Loading scheduler jobs
</Typography>
</Box>
)}
{schedulerJobsError && !schedulerJobsLoading && (
<Typography variant="body2" sx={{ color: 'rgba(248,113,113,0.9)' }}>
{schedulerJobsError}
</Typography>
)}
{!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length === 0 && (
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
No scheduled jobs found for this account.
</Typography>
)}
{!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length > 0 && (
<Box
sx={{
mt: 1,
maxHeight: 220,
overflowY: 'auto',
borderRadius: 1,
border: '1px solid rgba(51,65,85,0.9)',
bgcolor: 'rgba(15,23,42,0.9)'
}}
>
{schedulerJobs
.slice()
.sort((a, b) => {
const aTime = a.next_run_time ? new Date(a.next_run_time).getTime() : Number.MAX_SAFE_INTEGER;
const bTime = b.next_run_time ? new Date(b.next_run_time).getTime() : Number.MAX_SAFE_INTEGER;
return aTime - bTime;
})
.map((job) => {
const label =
job.task_category === 'website_analysis'
? 'Website Analysis'
: job.task_category === 'platform_insights'
? 'Platform Insights'
: job.task_category === 'deep_website_crawl'
? 'Deep Website Crawl'
: job.function_name || job.id;
const subtitle =
job.website_url ||
(job.platform ? `${job.platform.toUpperCase()} insights` : job.job_store);
const nextRun = job.next_run_time
? new Date(job.next_run_time).toLocaleString()
: 'Not scheduled';
return (
<Box
key={job.id}
sx={{
px: 1.5,
py: 1,
borderBottom: '1px solid rgba(30,41,59,0.9)',
'&:last-of-type': { borderBottom: 'none' }
}}
>
<Typography
variant="body2"
sx={{ color: 'rgba(248,250,252,0.95)', fontWeight: 500 }}
>
{label}
</Typography>
<Typography
variant="caption"
sx={{ display: 'block', color: 'rgba(148,163,184,0.9)' }}
>
{subtitle}
</Typography>
<Typography
variant="caption"
sx={{ display: 'block', color: 'rgba(148,163,184,0.7)', mt: 0.25 }}
>
Next run: {nextRun}
{job.frequency ? `${job.frequency}` : ''}
</Typography>
</Box>
);
})}
</Box>
)}
</Box>
</>
) : (
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
No semantic indexing information available.
</Typography>
)}
</Box>
</Drawer>
</DashboardContainer>
);
};