From 936dd14e0d5b1190a104748e460561db462a1aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Sun, 8 Mar 2026 23:09:02 +0530 Subject: [PATCH] Add consolidated onboarding SEO task health API and dashboard panel --- backend/api/seo_dashboard.py | 81 ++-- backend/app.py | 10 +- backend/services/seo/dashboard_service.py | 125 +++++- frontend/src/api/seoDashboard.ts | 40 ++ .../components/SEODashboard/SEODashboard.tsx | 405 ++++-------------- 5 files changed, 298 insertions(+), 363 deletions(-) diff --git a/backend/api/seo_dashboard.py b/backend/api/seo_dashboard.py index 46eeafb9..a7f8f8fd 100644 --- a/backend/api/seo_dashboard.py +++ b/backend/api/seo_dashboard.py @@ -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: """ diff --git a/backend/app.py b/backend/app.py index d3dde111..aa01751a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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.""" diff --git a/backend/services/seo/dashboard_service.py b/backend/services/seo/dashboard_service.py index 41a8ad07..e101cceb 100644 --- a/backend/services/seo/dashboard_service.py +++ b/backend/services/seo/dashboard_service.py @@ -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.""" diff --git a/frontend/src/api/seoDashboard.ts b/frontend/src/api/seoDashboard.ts index 6c62c743..650557e0 100644 --- a/frontend/src/api/seoDashboard.ts +++ b/frontend/src/api/seoDashboard.ts @@ -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; + 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 { + 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; + } } + }; diff --git a/frontend/src/components/SEODashboard/SEODashboard.tsx b/frontend/src/components/SEODashboard/SEODashboard.tsx index de9dee17..75a41853 100644 --- a/frontend/src/components/SEODashboard/SEODashboard.tsx +++ b/frontend/src/components/SEODashboard/SEODashboard.tsx @@ -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(null); const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false); const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState(null); - const [sifHealth, setSifHealth] = useState(null); - const [sifDetailsOpen, setSifDetailsOpen] = useState(false); - const [schedulerJobs, setSchedulerJobs] = useState(null); - const [schedulerJobsLoading, setSchedulerJobsLoading] = useState(false); - const [schedulerJobsError, setSchedulerJobsError] = useState(null); + const [onboardingTaskHealth, setOnboardingTaskHealth] = useState(null); // PlatformAnalytics refresh handle const platformRefreshRef = useRef<(() => Promise) | 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 = { + 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 ; } @@ -622,67 +612,18 @@ const SEODashboard: React.FC = () => { }} /> - - {sifHealth && ( - - setSifDetailsOpen(true)} + {onboardingTaskHealth && ( + + 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)' }} - > - - - Semantic Indexing:{' '} - {sifHealth.status === 'not_scheduled' - ? 'Not scheduled yet' - : sifHealth.status === 'healthy' - ? 'Up to date' - : sifHealth.status === 'warning' - ? 'Issues detected' - : 'Needs intervention'} - - + /> )} @@ -1524,6 +1465,61 @@ const SEODashboard: React.FC = () => { )} + + + {onboardingTaskHealth && ( + + + + + Onboarding Scheduled SEO Tasks + + + Unified health view for onboarding automation jobs. + + + + {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 ( + + + + + {task.label || taskKey} + + + + + Next: {task.next_execution ? new Date(task.next_execution).toLocaleString() : 'Not scheduled'} + + + Last success: {task.last_success ? new Date(task.last_success).toLocaleString() : 'No successful runs yet'} + + + Last failure: {task.last_failure ? new Date(task.last_failure).toLocaleString() : 'No failure recorded'} + + + Consecutive failures: {task.consecutive_failures ?? 0} + + + {ui.action} + {task.latest_execution?.error_message ? ` Latest error: ${task.latest_execution.error_message}` : ''} + + + + ); + })} + + + + + )} + {/* SEO Analyzer Panel */} { - setSifDetailsOpen(false)} - PaperProps={{ - sx: { - width: { xs: '100%', sm: 360 }, - maxWidth: '100vw', - bgcolor: 'rgba(15, 23, 42, 0.98)', - color: 'white' - } - }} - > - - Semantic Indexing Details - setSifDetailsOpen(false)} sx={{ color: 'rgba(148,163,184,0.9)' }}> - - - - - {sifHealth ? ( - <> - - - Overall Status - - - {sifHealth.status === 'not_scheduled' - ? 'Not scheduled' - : sifHealth.status === 'healthy' - ? 'Healthy' - : sifHealth.status === 'warning' - ? 'Warning' - : 'Critical'} - - - - - - Next Scheduled Run - - - {sifHealth.task?.next_execution - ? new Date(sifHealth.task.next_execution).toLocaleString() - : 'Not scheduled'} - - - - - - Last Success - - - {sifHealth.task?.last_success - ? new Date(sifHealth.task.last_success).toLocaleString() - : 'No successful runs yet'} - - - - - - Last Failure - - - {sifHealth.task?.last_failure - ? new Date(sifHealth.task.last_failure).toLocaleString() - : 'No failures recorded'} - - - - - - Consecutive Failures - - - {sifHealth.task?.consecutive_failures ?? 0} - - - - {sifHealth.last_run?.status && ( - - - Last Run Status - - - {sifHealth.last_run.status} - {sifHealth.last_run.time - ? ` • ${new Date(sifHealth.last_run.time).toLocaleString()}` - : ''} - - - )} - - {sifHealth.last_run?.error_message && ( - - - Last Error (snippet) - - - {sifHealth.last_run.error_message} - - - )} - - - - Scheduled Jobs For Your Account - - - {schedulerJobsLoading && ( - - - - Loading scheduler jobs… - - - )} - - {schedulerJobsError && !schedulerJobsLoading && ( - - {schedulerJobsError} - - )} - - {!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length === 0 && ( - - No scheduled jobs found for this account. - - )} - - {!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length > 0 && ( - - {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 ( - - - {label} - - - {subtitle} - - - Next run: {nextRun} - {job.frequency ? ` • ${job.frequency}` : ''} - - - ); - })} - - )} - - - ) : ( - - No semantic indexing information available. - - )} - - ); };