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."""