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]:
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user