Merge branch 'pr-399'
This commit is contained in:
@@ -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]:
|
async def get_sif_indexing_health(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
from models.website_analysis_monitoring_models import SIFIndexingTask, SIFIndexingExecutionLog
|
|
||||||
|
|
||||||
user_id = str(current_user.get("id"))
|
user_id = str(current_user.get("id"))
|
||||||
db = get_session_for_user(user_id)
|
db_session = get_session_for_user(user_id)
|
||||||
if not db:
|
if not db_session:
|
||||||
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
raise HTTPException(status_code=500, detail="Database connection unavailable")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tasks = (
|
dashboard_service = SEODashboardService(db_session)
|
||||||
db.query(SIFIndexingTask)
|
onboarding_task_health = await dashboard_service.get_onboarding_scheduled_task_health(user_id)
|
||||||
.filter(SIFIndexingTask.user_id == user_id)
|
sif_health = onboarding_task_health.get("tasks", {}).get("SIFIndexingTask", {})
|
||||||
.order_by(SIFIndexingTask.created_at.desc())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not tasks:
|
if sif_health.get("status") == "not_scheduled":
|
||||||
return {
|
return {
|
||||||
"has_task": False,
|
"has_task": False,
|
||||||
"status": "not_scheduled",
|
"status": "not_scheduled",
|
||||||
"message": "SIF indexing task not yet scheduled for this website.",
|
"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"
|
overall_status = "healthy"
|
||||||
if latest.consecutive_failures and latest.consecutive_failures > 0:
|
if (sif_health.get("consecutive_failures") or 0) > 0:
|
||||||
overall_status = "warning"
|
overall_status = "warning"
|
||||||
if latest.status in {"needs_intervention"}:
|
if sif_health.get("status") in {"failed", "needs_intervention"}:
|
||||||
overall_status = "critical"
|
overall_status = "critical"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"has_task": True,
|
"has_task": True,
|
||||||
"status": overall_status,
|
"status": overall_status,
|
||||||
"task": {
|
"task": {
|
||||||
"id": latest.id,
|
"raw_status": sif_health.get("status"),
|
||||||
"website_url": latest.website_url,
|
"next_execution": sif_health.get("next_execution"),
|
||||||
"raw_status": latest.status,
|
"last_success": sif_health.get("last_success"),
|
||||||
"next_execution": latest.next_execution.isoformat() if latest.next_execution else None,
|
"last_failure": sif_health.get("last_failure"),
|
||||||
"last_success": latest.last_success.isoformat() if latest.last_success else None,
|
"consecutive_failures": sif_health.get("consecutive_failures") or 0,
|
||||||
"last_failure": latest.last_failure.isoformat() if latest.last_failure else None,
|
|
||||||
"consecutive_failures": latest.consecutive_failures or 0,
|
|
||||||
"failure_pattern": latest.failure_pattern,
|
|
||||||
},
|
},
|
||||||
"last_run": {
|
"last_run": {
|
||||||
"status": last_run_status,
|
"status": sif_health.get("latest_execution", {}).get("status"),
|
||||||
"time": last_run_time,
|
"time": sif_health.get("latest_execution", {}).get("execution_date"),
|
||||||
"error_message": last_error,
|
"error_message": sif_health.get("latest_execution", {}).get("error_message"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db_session.close()
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get SIF indexing health: {e}")
|
logger.error(f"Failed to get SIF indexing health: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to get SIF indexing health")
|
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
|
# New comprehensive SEO analysis endpoints
|
||||||
async def analyze_seo_comprehensive(request: SEOAnalysisRequest) -> SEOAnalysisResponse:
|
async def analyze_seo_comprehensive(request: SEOAnalysisRequest) -> SEOAnalysisResponse:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ from api.seo_dashboard import (
|
|||||||
analyze_urls_ai,
|
analyze_urls_ai,
|
||||||
AnalyzeURLsRequest,
|
AnalyzeURLsRequest,
|
||||||
get_analyzed_pages,
|
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."""
|
"""Refresh analytics data by invalidating cache and fetching fresh data."""
|
||||||
return await refresh_analytics_data(current_user, site_url)
|
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")
|
@app.get("/api/seo-dashboard/health")
|
||||||
async def seo_dashboard_health():
|
async def seo_dashboard_health():
|
||||||
"""Health check for SEO dashboard."""
|
"""Health check for SEO dashboard."""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ and other analytics sources for the SEO dashboard. Leverages existing
|
|||||||
OAuth connections from onboarding step 5.
|
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 datetime import datetime, timedelta
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
@@ -21,7 +21,16 @@ from api.content_planning.services.content_strategy.onboarding.data_integration
|
|||||||
from .analytics_aggregator import AnalyticsAggregator
|
from .analytics_aggregator import AnalyticsAggregator
|
||||||
from .competitive_analyzer import CompetitiveAnalyzer
|
from .competitive_analyzer import CompetitiveAnalyzer
|
||||||
from models.onboarding import SEOPageAudit, WebsiteAnalysis, OnboardingSession
|
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
|
from models.advertools_monitoring_models import AdvertoolsTask
|
||||||
|
|
||||||
logger = get_service_logger("seo_dashboard")
|
logger = get_service_logger("seo_dashboard")
|
||||||
@@ -209,6 +218,118 @@ class SEODashboardService:
|
|||||||
"fix_scheduled_pages": 0,
|
"fix_scheduled_pages": 0,
|
||||||
"worst_pages": []
|
"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]:
|
async def get_gsc_data(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""Get GSC data for the specified site."""
|
"""Get GSC data for the specified site."""
|
||||||
|
|||||||
@@ -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 {
|
export interface SEODashboardData {
|
||||||
health_score: SEOHealthScore;
|
health_score: SEOHealthScore;
|
||||||
key_insight: string;
|
key_insight: string;
|
||||||
@@ -172,5 +199,18 @@ export const seoDashboardAPI = {
|
|||||||
console.error('Error fetching SIF indexing health:', error);
|
console.error('Error fetching SIF indexing health:', error);
|
||||||
throw 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Drawer
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useAuth, useUser, SignOutButton, useClerk } from '@clerk/clerk-react';
|
import { useAuth, useUser, SignOutButton, useClerk } from '@clerk/clerk-react';
|
||||||
@@ -33,7 +32,6 @@ import {
|
|||||||
Schedule as ScheduleIcon,
|
Schedule as ScheduleIcon,
|
||||||
Info as InfoIcon,
|
Info as InfoIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
ExpandMore as ExpandMoreIcon,
|
||||||
Close as CloseIcon,
|
|
||||||
AutoAwesome as AIIcon
|
AutoAwesome as AIIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
@@ -50,8 +48,7 @@ import { useSEODashboardStore } from '../../stores/seoDashboardStore';
|
|||||||
|
|
||||||
// API
|
// API
|
||||||
import { userDataAPI } from '../../api/userData';
|
import { userDataAPI } from '../../api/userData';
|
||||||
import { SIFIndexingHealth } from '../../api/seoDashboard';
|
import { OnboardingScheduledTaskHealthResponse, OnboardingTaskStatus } from '../../api/seoDashboard';
|
||||||
import { getSchedulerDashboard, SchedulerJob } from '../../api/schedulerDashboard';
|
|
||||||
|
|
||||||
// Shared components
|
// Shared components
|
||||||
import PlatformAnalytics from '../shared/PlatformAnalytics';
|
import PlatformAnalytics from '../shared/PlatformAnalytics';
|
||||||
@@ -121,11 +118,7 @@ const SEODashboard: React.FC = () => {
|
|||||||
const [competitiveSitemapBenchmarkingReport, setCompetitiveSitemapBenchmarkingReport] = useState<any>(null);
|
const [competitiveSitemapBenchmarkingReport, setCompetitiveSitemapBenchmarkingReport] = useState<any>(null);
|
||||||
const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false);
|
const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false);
|
||||||
const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState<string | null>(null);
|
const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState<string | null>(null);
|
||||||
const [sifHealth, setSifHealth] = useState<SIFIndexingHealth | null>(null);
|
const [onboardingTaskHealth, setOnboardingTaskHealth] = useState<OnboardingScheduledTaskHealthResponse | 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);
|
|
||||||
|
|
||||||
// PlatformAnalytics refresh handle
|
// PlatformAnalytics refresh handle
|
||||||
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
|
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
|
||||||
@@ -147,26 +140,6 @@ const SEODashboard: React.FC = () => {
|
|||||||
fetchStrategicInsightsHistory();
|
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 () => {
|
const fetchStrategicInsightsHistory = async () => {
|
||||||
setStrategicInsightsLoading(true);
|
setStrategicInsightsLoading(true);
|
||||||
@@ -260,16 +233,16 @@ const SEODashboard: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Fetch platform status and user data in parallel
|
// 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'),
|
apiClient.get('/api/seo-dashboard/platforms'),
|
||||||
userDataAPI.getUserData(),
|
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 response:', platformResponse.status, platformResponse.statusText);
|
||||||
console.log('Platform status data:', platformResponse.data);
|
console.log('Platform status data:', platformResponse.data);
|
||||||
setPlatformStatus(platformResponse.data);
|
setPlatformStatus(platformResponse.data);
|
||||||
setSifHealth(sifHealthResponse.data);
|
setOnboardingTaskHealth(onboardingTaskHealthResponse.data);
|
||||||
|
|
||||||
websiteUrl = userData?.website_url || 'https://alwrity.com';
|
websiteUrl = userData?.website_url || 'https://alwrity.com';
|
||||||
|
|
||||||
@@ -499,6 +472,23 @@ const SEODashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show sign-in prompt if not authenticated
|
// 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) {
|
if (!isLoaded) {
|
||||||
return <Skeleton variant="rectangular" height={200} />;
|
return <Skeleton variant="rectangular" height={200} />;
|
||||||
}
|
}
|
||||||
@@ -622,67 +612,18 @@ const SEODashboard: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{onboardingTaskHealth && (
|
||||||
{sifHealth && (
|
<Tooltip title="Onboarding Scheduled SEO Tasks">
|
||||||
<Tooltip title="Semantic Indexing Status (SIF)">
|
<Chip
|
||||||
<Box
|
label={`Onboarding Tasks: ${Object.values(onboardingTaskHealth.tasks || {}).filter((task: any) => task?.status === 'active').length} active`}
|
||||||
onClick={() => setSifDetailsOpen(true)}
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
ml: 2,
|
ml: 2,
|
||||||
px: 2,
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||||
py: 0.75,
|
color: 'white',
|
||||||
borderRadius: 999,
|
border: '1px solid rgba(255, 255, 255, 0.2)'
|
||||||
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)',
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<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>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1524,6 +1465,61 @@ const SEODashboard: React.FC = () => {
|
|||||||
</Box>
|
</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 */}
|
{/* SEO Analyzer Panel */}
|
||||||
<SEOAnalyzerPanel
|
<SEOAnalyzerPanel
|
||||||
analysisData={analysisData}
|
analysisData={analysisData}
|
||||||
@@ -1543,236 +1539,7 @@ const SEODashboard: React.FC = () => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Container>
|
</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>
|
</DashboardContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user