chore: bulk commit of local changes across blog writer, SEO dashboard, scheduler, docs-site, and frontend
This commit is contained in:
@@ -1238,7 +1238,7 @@ async def save_complete_blog_asset(
|
||||
user_id=user_id,
|
||||
content=full_content,
|
||||
source_module="blog_writer",
|
||||
title=f"Published Blog: {request.title[:60]}",
|
||||
title=request.title[:100],
|
||||
description=request.meta_description or f"Complete published blog post: {request.title}",
|
||||
prompt=f"SEO Title: {request.seo_title or request.title}\nFocus Keyword: {request.focus_keyword or ''}",
|
||||
tags=["blog", "published"] + [t for t in (request.tags or []) if t],
|
||||
@@ -1413,7 +1413,11 @@ async def update_blog_asset(
|
||||
if val is not None:
|
||||
meta[field] = val
|
||||
|
||||
if meta.get("selected_title"):
|
||||
# Prefer seo_title from publish_data, then selected_title, then topic, then existing title
|
||||
publish_data = meta.get("publish_data") or {}
|
||||
if isinstance(publish_data, dict) and publish_data.get("seo_title"):
|
||||
new_title = publish_data["seo_title"]
|
||||
elif meta.get("selected_title"):
|
||||
new_title = meta["selected_title"]
|
||||
elif meta.get("topic"):
|
||||
new_title = meta["topic"]
|
||||
|
||||
@@ -344,6 +344,43 @@ async def update_asset(
|
||||
raise HTTPException(status_code=500, detail=f"Error updating asset: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{asset_id}/content")
|
||||
async def get_asset_content(
|
||||
asset_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Serve the raw text content of a text asset by reading its file from disk."""
|
||||
try:
|
||||
user_id = current_user.get("user_id") or current_user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found")
|
||||
|
||||
service = ContentAssetService(db)
|
||||
asset = service.get_asset_by_id(asset_id, user_id)
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Asset not found")
|
||||
|
||||
if asset.asset_type != AssetType.TEXT:
|
||||
raise HTTPException(status_code=400, detail="Asset is not a text file")
|
||||
|
||||
if not asset.file_path:
|
||||
raise HTTPException(status_code=404, detail="Asset file path not recorded")
|
||||
|
||||
from pathlib import Path
|
||||
file_path = Path(asset.file_path)
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Asset file not found on disk")
|
||||
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
return {"success": True, "content": content}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading asset content: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=Dict[str, Any])
|
||||
async def get_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -19,7 +19,11 @@ from models.monitoring_models import TaskExecutionLog, MonitoringTask
|
||||
from models.scheduler_models import SchedulerEventLog
|
||||
from models.oauth_token_monitoring_models import OAuthTokenMonitoringTask
|
||||
from models.platform_insights_monitoring_models import PlatformInsightsTask, PlatformInsightsExecutionLog
|
||||
from models.website_analysis_monitoring_models import WebsiteAnalysisTask, WebsiteAnalysisExecutionLog, DeepWebsiteCrawlTask
|
||||
from models.website_analysis_monitoring_models import (
|
||||
WebsiteAnalysisTask, WebsiteAnalysisExecutionLog, DeepWebsiteCrawlTask,
|
||||
OnboardingFullWebsiteAnalysisTask, DeepCompetitorAnalysisTask,
|
||||
SIFIndexingTask, MarketTrendsTask, AdvertoolsTask,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/scheduler", tags=["scheduler-dashboard"])
|
||||
|
||||
@@ -309,6 +313,198 @@ async def get_scheduler_dashboard(
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading deep website crawl tasks: {e}", exc_info=True)
|
||||
|
||||
# Load onboarding full website analysis tasks
|
||||
try:
|
||||
onboarding_tasks = db.query(OnboardingFullWebsiteAnalysisTask).filter(
|
||||
OnboardingFullWebsiteAnalysisTask.status.in_(['active', 'failed', 'needs_intervention'])
|
||||
).all()
|
||||
|
||||
if user_id_str:
|
||||
onboarding_tasks = [t for t in onboarding_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in onboarding_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception:
|
||||
user_job_store = 'default'
|
||||
|
||||
job_info = {
|
||||
'id': f"onboarding_full_website_analysis_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'DateTrigger' if task.status != 'active' else 'CronTrigger',
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'onboarding_full_website_analysis_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': 'One-time' if task.status == 'completed' else 'Once',
|
||||
'task_category': 'onboarding_full_website_analysis',
|
||||
'status': task.status,
|
||||
'last_success': task.last_success.isoformat() if task.last_success else None,
|
||||
'last_failure': task.last_failure.isoformat() if task.last_failure else None,
|
||||
'failure_reason': task.failure_reason,
|
||||
'consecutive_failures': task.consecutive_failures,
|
||||
}
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading onboarding full website analysis tasks: {e}", exc_info=True)
|
||||
|
||||
# Load deep competitor analysis tasks
|
||||
try:
|
||||
competitor_tasks = db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.status.in_(['active', 'failed', 'needs_intervention'])
|
||||
).all()
|
||||
|
||||
if user_id_str:
|
||||
competitor_tasks = [t for t in competitor_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in competitor_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception:
|
||||
user_job_store = 'default'
|
||||
|
||||
payload = task.payload or {}
|
||||
frequency_label = 'Weekly' if payload.get('mode') == 'strategic_insights' else 'One-time'
|
||||
job_info = {
|
||||
'id': f"deep_competitor_analysis_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'CronTrigger' if frequency_label == 'Weekly' else 'DateTrigger',
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'deep_competitor_analysis_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': frequency_label,
|
||||
'task_category': 'deep_competitor_analysis',
|
||||
'status': task.status,
|
||||
'last_success': task.last_success.isoformat() if task.last_success else None,
|
||||
'last_failure': task.last_failure.isoformat() if task.last_failure else None,
|
||||
'failure_reason': task.failure_reason,
|
||||
'consecutive_failures': task.consecutive_failures,
|
||||
}
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading deep competitor analysis tasks: {e}", exc_info=True)
|
||||
|
||||
# Load SIF indexing tasks
|
||||
try:
|
||||
sif_tasks = db.query(SIFIndexingTask).filter(
|
||||
SIFIndexingTask.status.in_(['active', 'failed', 'needs_intervention'])
|
||||
).all()
|
||||
|
||||
if user_id_str:
|
||||
sif_tasks = [t for t in sif_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in sif_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception:
|
||||
user_job_store = 'default'
|
||||
|
||||
job_info = {
|
||||
'id': f"sif_indexing_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'CronTrigger',
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'sif_indexing_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': f'Every {task.frequency_hours}h' if task.frequency_hours else 'Every 48h',
|
||||
'task_category': 'sif_indexing',
|
||||
'status': task.status,
|
||||
'last_success': task.last_success.isoformat() if task.last_success else None,
|
||||
'last_failure': task.last_failure.isoformat() if task.last_failure else None,
|
||||
'failure_reason': task.failure_reason,
|
||||
'consecutive_failures': task.consecutive_failures,
|
||||
}
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading SIF indexing tasks: {e}", exc_info=True)
|
||||
|
||||
# Load market trends tasks
|
||||
try:
|
||||
trends_tasks = db.query(MarketTrendsTask).filter(
|
||||
MarketTrendsTask.status.in_(['active', 'failed', 'needs_intervention'])
|
||||
).all()
|
||||
|
||||
if user_id_str:
|
||||
trends_tasks = [t for t in trends_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in trends_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception:
|
||||
user_job_store = 'default'
|
||||
|
||||
job_info = {
|
||||
'id': f"market_trends_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'CronTrigger',
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'market_trends_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': f'Every {task.frequency_hours}h' if task.frequency_hours else 'Every 72h',
|
||||
'task_category': 'market_trends',
|
||||
'status': task.status,
|
||||
'last_success': task.last_success.isoformat() if task.last_success else None,
|
||||
'last_failure': task.last_failure.isoformat() if task.last_failure else None,
|
||||
'failure_reason': task.failure_reason,
|
||||
'consecutive_failures': task.consecutive_failures,
|
||||
}
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading market trends tasks: {e}", exc_info=True)
|
||||
|
||||
# Load advertools tasks
|
||||
try:
|
||||
advertools_tasks = db.query(AdvertoolsTask).filter(
|
||||
AdvertoolsTask.status.in_(['active', 'failed', 'paused'])
|
||||
).all()
|
||||
|
||||
if user_id_str:
|
||||
advertools_tasks = [t for t in advertools_tasks if t.user_id == user_id_str]
|
||||
|
||||
for task in advertools_tasks:
|
||||
try:
|
||||
user_job_store = get_user_job_store_name(task.user_id, db)
|
||||
except Exception:
|
||||
user_job_store = 'default'
|
||||
|
||||
job_info = {
|
||||
'id': f"advertools_{task.user_id}_{task.id}",
|
||||
'trigger_type': 'CronTrigger',
|
||||
'next_run_time': task.next_execution.isoformat() if task.next_execution else None,
|
||||
'user_id': task.user_id,
|
||||
'job_store': 'default',
|
||||
'user_job_store': user_job_store,
|
||||
'function_name': 'advertools_executor.execute_task',
|
||||
'website_url': task.website_url,
|
||||
'task_id': task.id,
|
||||
'is_database_task': True,
|
||||
'frequency': f'Every {task.frequency_days}d' if task.frequency_days else 'Weekly',
|
||||
'task_category': 'advertools',
|
||||
'status': task.status,
|
||||
'last_success': task.last_success.isoformat() if task.last_success else None,
|
||||
'last_failure': task.last_failure.isoformat() if task.last_failure else None,
|
||||
'failure_reason': task.failure_reason,
|
||||
'consecutive_failures': task.consecutive_failures,
|
||||
}
|
||||
formatted_jobs.append(job_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading advertools tasks: {e}", exc_info=True)
|
||||
|
||||
# Get active strategies count
|
||||
active_strategies = stats.get('active_strategies_count', 0)
|
||||
|
||||
@@ -1237,7 +1433,9 @@ async def manual_trigger_task(
|
||||
This bypasses the cool-off check and executes the task immediately.
|
||||
|
||||
Args:
|
||||
task_type: Task type (oauth_token_monitoring, website_analysis, gsc_insights, bing_insights)
|
||||
task_type: Task type (oauth_token_monitoring, website_analysis, gsc_insights, bing_insights,
|
||||
onboarding_full_website_analysis, deep_competitor_analysis, sif_indexing,
|
||||
market_trends, advertools)
|
||||
task_id: Task ID
|
||||
|
||||
Returns:
|
||||
@@ -1261,6 +1459,30 @@ async def manual_trigger_task(
|
||||
task = db.query(PlatformInsightsTask).filter(
|
||||
PlatformInsightsTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "onboarding_full_website_analysis":
|
||||
task = db.query(OnboardingFullWebsiteAnalysisTask).filter(
|
||||
OnboardingFullWebsiteAnalysisTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "deep_competitor_analysis":
|
||||
task = db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "sif_indexing":
|
||||
task = db.query(SIFIndexingTask).filter(
|
||||
SIFIndexingTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "market_trends":
|
||||
task = db.query(MarketTrendsTask).filter(
|
||||
MarketTrendsTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "advertools":
|
||||
task = db.query(AdvertoolsTask).filter(
|
||||
AdvertoolsTask.id == task_id
|
||||
).first()
|
||||
elif task_type == "deep_website_crawl":
|
||||
task = db.query(DeepWebsiteCrawlTask).filter(
|
||||
DeepWebsiteCrawlTask.id == task_id
|
||||
).first()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown task type: {task_type}")
|
||||
|
||||
@@ -1363,3 +1585,219 @@ async def get_platform_insights_logs(
|
||||
logger.error(f"Error getting platform insights logs for user {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get platform insights logs: {str(e)}")
|
||||
|
||||
|
||||
TASK_DISPLAY_INFO = {
|
||||
"onboarding_full_website_analysis": {"label": "Full-Site SEO Audit", "description": "Crawls your entire website and generates per-page SEO audit results.", "frequency": "One-time"},
|
||||
"deep_competitor_analysis": {"label": "Deep Competitor Analysis", "description": "Analyzes competitors' content strategy, keywords, and positioning.", "frequency": "Weekly (strategic insights) or One-time"},
|
||||
"sif_indexing": {"label": "SIF Content Indexing", "description": "Indexes your website content into the Semantic Intelligence Framework for agent-powered recommendations.", "frequency": "Every 48 hours"},
|
||||
"market_trends": {"label": "Market Trends", "description": "Monitors search trends and surfaces high-impact content opportunities.", "frequency": "Every 72 hours"},
|
||||
"advertools": {"label": "Advertools Analysis", "description": "Runs brand analysis and site health audits using Advertools.", "frequency": "Weekly"},
|
||||
"oauth_token_monitoring": {"label": "OAuth Token Health", "description": "Monitors and refreshes OAuth tokens for connected platforms (GSC, Bing, WordPress, Wix).", "frequency": "Weekly"},
|
||||
"website_analysis": {"label": "Website Analysis", "description": "Periodically re-crawls your website and updates style analysis, content pillars, and SEO data.", "frequency": "Every 10 days"},
|
||||
"gsc_insights": {"label": "Google Search Console Insights", "description": "Pulls search performance data from Google Search Console.", "frequency": "Weekly"},
|
||||
"bing_insights": {"label": "Bing Insights", "description": "Pulls search performance data from Bing Webmaster Tools.", "frequency": "Weekly"},
|
||||
"deep_website_crawl": {"label": "Deep Website Crawl", "description": "Performs deep crawl of your website for technical SEO issues.", "frequency": "Weekly"},
|
||||
"platform_insights": {"label": "Platform Insights", "description": "Aggregates search performance data from connected platforms.", "frequency": "Weekly"},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/onboarding-tasks/{user_id}")
|
||||
async def get_onboarding_tasks(
|
||||
user_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all tasks created during onboarding for a user, with status and human-readable descriptions.
|
||||
"""
|
||||
try:
|
||||
if str(current_user.get('id')) != user_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
tasks = []
|
||||
|
||||
def _fmt_status(s):
|
||||
return s.replace('_', ' ').title() if s else 'Unknown'
|
||||
|
||||
def _fmt_dt(dt):
|
||||
return dt.isoformat() if dt else None
|
||||
|
||||
# Onboarding full-site SEO audit
|
||||
for t in db.query(OnboardingFullWebsiteAnalysisTask).filter(
|
||||
OnboardingFullWebsiteAnalysisTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("onboarding_full_website_analysis", {})
|
||||
tasks.append({
|
||||
"task_type": "onboarding_full_website_analysis",
|
||||
"label": info.get("label", "Full-Site SEO Audit"),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": info.get("frequency", "One-time"),
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_execution),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
# Deep competitor analysis
|
||||
for t in db.query(DeepCompetitorAnalysisTask).filter(
|
||||
DeepCompetitorAnalysisTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("deep_competitor_analysis", {})
|
||||
payload = t.payload or {}
|
||||
freq_label = info.get("frequency", "One-time")
|
||||
if payload.get("mode") == "strategic_insights":
|
||||
freq_label = "Weekly"
|
||||
tasks.append({
|
||||
"task_type": "deep_competitor_analysis",
|
||||
"label": info.get("label", "Deep Competitor Analysis"),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": freq_label,
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_execution),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
# SIF indexing
|
||||
for t in db.query(SIFIndexingTask).filter(
|
||||
SIFIndexingTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("sif_indexing", {})
|
||||
tasks.append({
|
||||
"task_type": "sif_indexing",
|
||||
"label": info.get("label", "SIF Content Indexing"),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": f"Every {t.frequency_hours or 48}h",
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_execution),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
# Market trends
|
||||
for t in db.query(MarketTrendsTask).filter(
|
||||
MarketTrendsTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("market_trends", {})
|
||||
tasks.append({
|
||||
"task_type": "market_trends",
|
||||
"label": info.get("label", "Market Trends"),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": f"Every {t.frequency_hours or 72}h",
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_execution),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
# Advertools
|
||||
for t in db.query(AdvertoolsTask).filter(
|
||||
AdvertoolsTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("advertools", {})
|
||||
tasks.append({
|
||||
"task_type": "advertools",
|
||||
"label": info.get("label", "Advertools Analysis"),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": f"Every {t.frequency_days or 7}d",
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_execution),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
# Also include website analysis & OAuth tasks created during onboarding
|
||||
for t in db.query(WebsiteAnalysisTask).filter(
|
||||
WebsiteAnalysisTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("website_analysis", {})
|
||||
tasks.append({
|
||||
"task_type": "website_analysis",
|
||||
"label": info.get("label", "Website Analysis") + (f" ({t.task_type})" if t.task_type == 'competitor' else ""),
|
||||
"description": info.get("description", ""),
|
||||
"frequency": f"Every {t.frequency_days or 10}d",
|
||||
"task_id": t.id,
|
||||
"website_url": t.website_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_check),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
for t in db.query(OAuthTokenMonitoringTask).filter(
|
||||
OAuthTokenMonitoringTask.user_id == user_id
|
||||
).all():
|
||||
info = TASK_DISPLAY_INFO.get("oauth_token_monitoring", {})
|
||||
tasks.append({
|
||||
"task_type": "oauth_token_monitoring",
|
||||
"label": info.get("label", "OAuth Token Health") + f" ({t.platform})",
|
||||
"description": info.get("description", ""),
|
||||
"frequency": info.get("frequency", "Weekly"),
|
||||
"task_id": t.id,
|
||||
"website_url": None,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_check),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
for t in db.query(PlatformInsightsTask).filter(
|
||||
PlatformInsightsTask.user_id == user_id
|
||||
).all():
|
||||
task_key = f"{t.platform}_insights"
|
||||
info = TASK_DISPLAY_INFO.get(task_key, {})
|
||||
tasks.append({
|
||||
"task_type": task_key,
|
||||
"label": info.get("label", "Platform Insights") + f" ({t.platform})",
|
||||
"description": info.get("description", ""),
|
||||
"frequency": info.get("frequency", "Weekly"),
|
||||
"task_id": t.id,
|
||||
"website_url": t.site_url,
|
||||
"status": t.status,
|
||||
"status_label": _fmt_status(t.status),
|
||||
"last_success": _fmt_dt(t.last_success),
|
||||
"last_failure": _fmt_dt(t.last_failure),
|
||||
"next_execution": _fmt_dt(t.next_check),
|
||||
"failure_reason": t.failure_reason,
|
||||
"consecutive_failures": t.consecutive_failures,
|
||||
})
|
||||
|
||||
return {"success": True, "tasks": tasks, "count": len(tasks)}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding tasks for user {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get onboarding tasks: {str(e)}")
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ class SEODashboardData(BaseModel):
|
||||
platforms: Dict[str, PlatformStatus]
|
||||
ai_insights: List[AIInsight]
|
||||
last_updated: str
|
||||
website_url: Optional[str] = None # User's website URL from onboarding
|
||||
website_url: Optional[str] = None
|
||||
advertools_insights: Optional[Dict[str, Any]] = None
|
||||
technical_seo_audit: Optional[Dict[str, Any]] = None
|
||||
|
||||
# New models for comprehensive SEO analysis
|
||||
class SEOAnalysisRequest(BaseModel):
|
||||
@@ -378,7 +380,9 @@ async def get_seo_dashboard_data(current_user: dict = Depends(get_current_user))
|
||||
platforms=_convert_platforms(overview_data.get("platforms", {})),
|
||||
ai_insights=[AIInsight(**insight) for insight in overview_data.get("ai_insights", [])],
|
||||
last_updated=overview_data.get("last_updated", datetime.now().isoformat()),
|
||||
website_url=overview_data.get("website_url")
|
||||
website_url=overview_data.get("website_url"),
|
||||
advertools_insights=overview_data.get("advertools_insights"),
|
||||
technical_seo_audit=overview_data.get("technical_seo_audit"),
|
||||
)
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@@ -167,10 +167,10 @@ class SceneVideoRenderResponse(BaseModel):
|
||||
|
||||
class CombineVideosRequest(BaseModel):
|
||||
"""Request model for combining multiple scene videos."""
|
||||
video_urls: List[str] = Field(..., description="List of scene video URLs to combine in order")
|
||||
scene_video_urls: List[str] = Field(..., description="List of scene video URLs to combine in order")
|
||||
video_plan: Optional[Dict[str, Any]] = Field(None, description="Original video plan (for metadata)")
|
||||
resolution: str = Field("720p", pattern="^(480p|720p|1080p)$", description="Target resolution for output")
|
||||
title: Optional[str] = Field(None, description="Optional title for the final video")
|
||||
title: Optional[str] = Field(None, description="Optional title for the combined video")
|
||||
|
||||
|
||||
class CombineVideosResponse(BaseModel):
|
||||
@@ -187,13 +187,6 @@ class VideoListResponse(BaseModel):
|
||||
message: str = "Videos fetched successfully"
|
||||
|
||||
|
||||
class CombineVideosRequest(BaseModel):
|
||||
"""Request model for combining multiple scene videos."""
|
||||
scene_video_urls: List[str] = Field(..., description="List of scene video URLs to combine")
|
||||
resolution: str = Field("720p", pattern="^(480p|720p|1080p)$", description="Output video resolution")
|
||||
title: Optional[str] = Field(None, description="Optional title for the combined video")
|
||||
|
||||
|
||||
class VideoRenderResponse(BaseModel):
|
||||
"""Response model for video rendering."""
|
||||
success: bool
|
||||
@@ -721,85 +714,6 @@ async def get_render_status(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/render/combine", response_model=VideoRenderResponse)
|
||||
async def combine_videos(
|
||||
request: CombineVideosRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> VideoRenderResponse:
|
||||
"""
|
||||
Combine multiple scene videos into a final video.
|
||||
Returns task_id for polling.
|
||||
"""
|
||||
try:
|
||||
user_id = require_authenticated_user(current_user)
|
||||
|
||||
# Subscription validation
|
||||
pricing_service = PricingService(db)
|
||||
validate_scene_animation_operation(
|
||||
pricing_service=pricing_service,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if not request.scene_video_urls or len(request.scene_video_urls) < 2:
|
||||
return VideoRenderResponse(
|
||||
success=False,
|
||||
message="At least two scene videos are required to combine."
|
||||
)
|
||||
|
||||
task_id = task_manager.create_task("youtube_combine_video")
|
||||
logger.info(
|
||||
f"[YouTubeAPI] Created combine task {task_id} for user {user_id}, videos={len(request.scene_video_urls)}, resolution={request.resolution}"
|
||||
)
|
||||
|
||||
initial_status = task_manager.get_task_status(task_id)
|
||||
if not initial_status:
|
||||
logger.error(f"[YouTubeAPI] Failed to create combine task {task_id} - task not found immediately after creation")
|
||||
return VideoRenderResponse(
|
||||
success=False,
|
||||
message="Failed to create combine task. Please try again."
|
||||
)
|
||||
|
||||
try:
|
||||
background_tasks.add_task(
|
||||
_execute_combine_video_task,
|
||||
task_id=task_id,
|
||||
scene_video_urls=request.scene_video_urls,
|
||||
user_id=user_id,
|
||||
resolution=request.resolution,
|
||||
title=request.title,
|
||||
)
|
||||
logger.info(f"[YouTubeAPI] Background combine task added for {task_id}")
|
||||
except Exception as bg_error:
|
||||
logger.error(f"[YouTubeAPI] Failed to add combine background task for {task_id}: {bg_error}", exc_info=True)
|
||||
task_manager.update_task_status(
|
||||
task_id,
|
||||
"failed",
|
||||
error=str(bg_error),
|
||||
message="Failed to start combine task"
|
||||
)
|
||||
return VideoRenderResponse(
|
||||
success=False,
|
||||
message=f"Failed to start combine task: {str(bg_error)}"
|
||||
)
|
||||
|
||||
return VideoRenderResponse(
|
||||
success=True,
|
||||
task_id=task_id,
|
||||
message="Video combination started."
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[YouTubeAPI] Error starting combine: {e}", exc_info=True)
|
||||
return VideoRenderResponse(
|
||||
success=False,
|
||||
message=f"Failed to start combine: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def _execute_video_render_task(
|
||||
task_id: str,
|
||||
scenes: List[Dict[str, Any]],
|
||||
@@ -1270,20 +1184,21 @@ async def combine_scene_videos(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if not request.video_urls or len(request.video_urls) < 2:
|
||||
if not request.scene_video_urls or len(request.scene_video_urls) < 2:
|
||||
return CombineVideosResponse(
|
||||
success=False,
|
||||
task_id=None,
|
||||
message="At least two videos are required to combine."
|
||||
message="At least two scene videos are required to combine."
|
||||
)
|
||||
|
||||
# Pre-validate that referenced video files exist and are within youtube_videos dir
|
||||
user_workspace = UserWorkspaceManager(db)
|
||||
workspace_info = user_workspace.get_user_workspace(user_id)
|
||||
youtube_video_dir = Path(workspace_info['workspace_path']) / "content" / "videos" if workspace_info and workspace_info.get('workspace_path') else YOUTUBE_VIDEO_DIR
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
youtube_video_dir = base_dir / "youtube_videos"
|
||||
legacy_video_dir = base_dir / "youtube_videos"
|
||||
missing_files = []
|
||||
for url in request.video_urls:
|
||||
filename = Path(url).name # strips query params if present
|
||||
video_path = youtube_video_dir / filename
|
||||
for url in request.scene_video_urls:
|
||||
filename = Path(url).name
|
||||
# prevent directory traversal
|
||||
if ".." in filename or "/" in filename or "\\" in filename:
|
||||
return CombineVideosResponse(
|
||||
@@ -1291,8 +1206,13 @@ async def combine_scene_videos(
|
||||
task_id=None,
|
||||
message=f"Invalid video filename: {filename}"
|
||||
)
|
||||
video_path = youtube_video_dir / filename
|
||||
if not video_path.exists():
|
||||
missing_files.append(filename)
|
||||
legacy_path = legacy_video_dir / filename
|
||||
if legacy_path.exists():
|
||||
video_path = legacy_path
|
||||
else:
|
||||
missing_files.append(filename)
|
||||
if missing_files:
|
||||
return CombineVideosResponse(
|
||||
success=False,
|
||||
@@ -1303,7 +1223,7 @@ async def combine_scene_videos(
|
||||
# Create task
|
||||
task_id = task_manager.create_task("youtube_video_combine")
|
||||
logger.info(
|
||||
f"[YouTubeAPI] Created combine task {task_id} for user {user_id}, videos={len(request.video_urls)}, resolution={request.resolution}"
|
||||
f"[YouTubeAPI] Created combine task {task_id} for user {user_id}, videos={len(request.scene_video_urls)}, resolution={request.resolution}"
|
||||
)
|
||||
|
||||
initial_status = task_manager.get_task_status(task_id)
|
||||
@@ -1320,7 +1240,7 @@ async def combine_scene_videos(
|
||||
background_tasks.add_task(
|
||||
_execute_combine_video_task,
|
||||
task_id=task_id,
|
||||
scene_video_urls=request.video_urls,
|
||||
scene_video_urls=request.scene_video_urls,
|
||||
user_id=user_id,
|
||||
resolution=request.resolution,
|
||||
title=request.title,
|
||||
@@ -1343,7 +1263,7 @@ async def combine_scene_videos(
|
||||
return CombineVideosResponse(
|
||||
success=True,
|
||||
task_id=task_id,
|
||||
message=f"Combining {len(request.video_urls)} videos...",
|
||||
message=f"Combining {len(request.scene_video_urls)} videos...",
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""
|
||||
Task Manager for YouTube Creator Studio
|
||||
|
||||
Reuses the Story Writer task manager pattern for async video rendering.
|
||||
Delegates to the hybrid DB-backed + in-memory YouTubeTaskManager.
|
||||
Maintains backward compatibility with the Story Writer TaskManager API.
|
||||
"""
|
||||
|
||||
from api.story_writer.task_manager import TaskManager
|
||||
|
||||
# Shared task manager instance
|
||||
task_manager = TaskManager()
|
||||
from services.youtube.youtube_task_manager import task_manager
|
||||
|
||||
__all__ = ["task_manager"]
|
||||
Reference in New Issue
Block a user