From 5965e123b9f6da6a88b681ddbe90bbca1dd02b6a Mon Sep 17 00:00:00 2001 From: ajaysi Date: Mon, 30 Mar 2026 07:43:46 +0530 Subject: [PATCH] Merge PR #446: Add podcast-only demo mode visibility and router status --- backend/app.py | 413 ++++++++++++++++++------------- backend/start_alwrity_backend.py | 17 +- 2 files changed, 251 insertions(+), 179 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9275e242..903bae68 100644 --- a/backend/app.py +++ b/backend/app.py @@ -48,12 +48,21 @@ load_dotenv(backend_dir / '.env') # backend/.env load_dotenv(project_root / '.env') # root .env (fallback) load_dotenv() # CWD .env (fallback) -PODCAST_ONLY_DEMO_MODE = os.getenv("PODCAST_ONLY_DEMO_MODE", "false").lower() in { - "1", - "true", - "yes", - "on", -} + +def _env_flag_enabled(*env_names: str) -> bool: + """Return True when any provided env var is set to a truthy value.""" + truthy_values = {"1", "true", "yes", "on"} + for env_name in env_names: + value = os.getenv(env_name) + if value and value.strip().lower() in truthy_values: + return True + return False + + +PODCAST_ONLY_DEMO_MODE = _env_flag_enabled( + "ALWRITY_PODCAST_ONLY_DEMO_MODE", + "PODCAST_ONLY_DEMO_MODE", +) def is_podcast_only_demo_mode() -> bool: @@ -63,6 +72,7 @@ def is_podcast_only_demo_mode() -> bool: def should_include_non_podcast_features() -> bool: return not is_podcast_only_demo_mode() + # Set up clean logging for end users from logging_config import setup_clean_logging setup_clean_logging() @@ -125,37 +135,36 @@ from services.startup_health import ( # Import OAuth token monitoring routes from api.oauth_token_monitoring_routes import router as oauth_token_monitoring_router -if not PODCAST_ONLY_DEMO_MODE: - # Import SEO Dashboard endpoints only when non-demo features are enabled - from api.seo_dashboard import ( - get_seo_dashboard_data, - get_seo_health_score, - get_seo_metrics, - get_platform_status, - get_ai_insights, - seo_dashboard_health_check, - analyze_seo_comprehensive, - analyze_seo_full, - get_seo_metrics_detailed, - get_analysis_summary, - batch_analyze_urls, - SEOAnalysisRequest, - get_seo_dashboard_overview, - get_gsc_raw_data, - get_bing_raw_data, - get_competitive_insights, - get_deep_competitor_analysis, - run_strategic_insights, - get_strategic_insights_history, - refresh_analytics_data, - analyze_urls_ai, - AnalyzeURLsRequest, - get_analyzed_pages, - get_semantic_health, - get_semantic_cache_stats, - get_sif_indexing_health, - get_onboarding_task_health, - ) +# Import SEO Dashboard endpoints +from api.seo_dashboard import ( + get_seo_dashboard_data, + get_seo_health_score, + get_seo_metrics, + get_platform_status, + get_ai_insights, + seo_dashboard_health_check, + analyze_seo_comprehensive, + analyze_seo_full, + get_seo_metrics_detailed, + get_analysis_summary, + batch_analyze_urls, + SEOAnalysisRequest, + get_seo_dashboard_overview, + get_gsc_raw_data, + get_bing_raw_data, + get_competitive_insights, + get_deep_competitor_analysis, + run_strategic_insights, + get_strategic_insights_history, + refresh_analytics_data, + analyze_urls_ai, + AnalyzeURLsRequest, + get_analyzed_pages, + get_semantic_health, + get_semantic_cache_stats, + get_sif_indexing_health, + get_onboarding_task_health, +) # Initialize FastAPI app @@ -198,6 +207,7 @@ health_checker = HealthChecker() rate_limiter = RateLimiter(window_seconds=60, max_requests=200) frontend_serving = FrontendServing(app) router_manager = RouterManager(app) +router_group_status: Dict[str, Dict[str, Any]] = {} onboarding_manager = None if not PODCAST_ONLY_DEMO_MODE: @@ -224,7 +234,9 @@ app.middleware("http")(api_key_injection_middleware) @app.get("/health") async def health(): """Health check endpoint.""" - return health_checker.basic_health_check() + health_data = health_checker.basic_health_check() + health_data["podcast_only_demo_mode"] = PODCAST_ONLY_DEMO_MODE + return health_data @app.get("/health/database") async def database_health(): @@ -240,6 +252,7 @@ async def comprehensive_health(): async def readiness(current_user: dict = Depends(get_current_user)): """Readiness check that validates tenant DB resolution/session under auth context.""" return { + "podcast_only_demo_mode": PODCAST_ONLY_DEMO_MODE, "startup": get_startup_status(), "tenant": readiness_under_auth_context(current_user), } @@ -268,7 +281,14 @@ async def frontend_status(): @app.get("/api/routers/status") async def router_status(): """Get router inclusion status.""" - return router_manager.get_router_status() + status = router_manager.get_router_status() + status.update( + { + "podcast_only_demo_mode": PODCAST_ONLY_DEMO_MODE, + "router_groups": router_group_status, + } + ) + return status # Onboarding management endpoints @app.get("/api/onboarding/status") @@ -284,145 +304,178 @@ async def onboarding_status(): return onboarding_manager.get_onboarding_status() # Include routers using modular utilities -if not PODCAST_ONLY_DEMO_MODE: - router_manager.include_core_routers() - router_manager.include_optional_routers() +if PODCAST_ONLY_DEMO_MODE: + router_group_status["modular_core"] = { + "mounted": False, + "reason": "Skipped in podcast-only demo mode", + } + router_group_status["modular_optional"] = { + "mounted": False, + "reason": "Skipped in podcast-only demo mode", + } else: - logger.info("PODCAST_ONLY_DEMO_MODE enabled: including only podcast and subscription feature routers.") - router_manager.include_router_safely(subscription_router, "subscription") + router_group_status["modular_core"] = { + "mounted": router_manager.include_core_routers(), + "reason": "Full mode", + } + router_group_status["modular_optional"] = { + "mounted": router_manager.include_optional_routers(), + "reason": "Full mode", + } + # Safety net: keep subscription routes available even if core inclusion flow changes # in special modes (e.g., demo mode). De-dup is handled by RouterManager. router_manager.include_router_safely(subscription_router, "subscription") # Include assets serving router (must be mounted to serve generated images) app.include_router(assets_serving_router) +router_group_status["assets_serving"] = { + "mounted": True, + "reason": "Required for podcast media assets", +} +# SEO Dashboard endpoints +@app.get("/api/seo-dashboard/data") +async def seo_dashboard_data(): + """Get complete SEO dashboard data.""" + return await get_seo_dashboard_data() + +@app.get("/api/seo-dashboard/health-score") +async def seo_health_score(): + """Get SEO health score.""" + return await get_seo_health_score() + +@app.get("/api/seo-dashboard/metrics") +async def seo_metrics(): + """Get SEO metrics.""" + return await get_seo_metrics() + +@app.get("/api/seo-dashboard/platforms") +async def seo_platforms(current_user: dict = Depends(get_current_user)): + """Get platform status.""" + return await get_platform_status(current_user) + +@app.get("/api/seo-dashboard/insights") +async def seo_insights(): + """Get AI insights.""" + return await get_ai_insights() + +# New SEO Dashboard endpoints with real data +@app.get("/api/seo-dashboard/overview") +async def seo_dashboard_overview_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """Get comprehensive SEO dashboard overview with real GSC/Bing data.""" + return await get_seo_dashboard_overview(current_user, site_url) + +@app.get("/api/seo-dashboard/gsc/raw") +async def gsc_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """Get raw GSC data for the specified site.""" + return await get_gsc_raw_data(current_user, site_url) + +@app.get("/api/seo-dashboard/bing/raw") +async def bing_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """Get raw Bing data for the specified site.""" + return await get_bing_raw_data(current_user, site_url) + +@app.get("/api/seo-dashboard/competitive-insights") +async def competitive_insights_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """Get competitive insights from onboarding step 3 data.""" + return await get_competitive_insights(current_user, site_url) + +@app.get("/api/seo-dashboard/deep-competitor-analysis") +async def deep_competitor_analysis_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """Get deep competitor analysis results (auto-scheduled post-onboarding).""" + return await get_deep_competitor_analysis(current_user, site_url) + +@app.post("/api/seo-dashboard/strategic-insights/run") +async def run_strategic_insights_endpoint(current_user: dict = Depends(get_current_user)): + """Run AI-powered strategic insights analysis manually.""" + return await run_strategic_insights(current_user) + +@app.get("/api/seo-dashboard/strategic-insights/history") +async def get_strategic_insights_history_endpoint(current_user: dict = Depends(get_current_user)): + """Fetch the history of strategic insights for the user.""" + return await get_strategic_insights_history(current_user) + +@app.post("/api/seo-dashboard/refresh") +async def refresh_analytics_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): + """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.""" + return await seo_dashboard_health_check() + +# Phase 2B: Semantic health monitoring endpoint (24-hour polling) +@app.get("/api/seo-dashboard/semantic-health") +async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)): + """ + Get real-time semantic health metrics for content and competitors. + This endpoint provides Phase 2B semantic intelligence monitoring data. + + Returns semantic health score, status, and recommendations. + Data is cached and updated every 24 hours via scheduler. + """ + return await get_semantic_health(current_user) + + +@app.get("/api/seo-dashboard/cache-stats") +async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)): + """ + Get semantic cache performance statistics. + Returns hit rate, memory usage, and eviction counts. + """ + return await get_semantic_cache_stats(current_user) + + +@app.get("/api/seo-dashboard/sif-health") +async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)): + """ + Get SIF indexing health summary for the current user. + Used by the Semantic Indexing Status widget on the dashboard. + """ + return await get_sif_indexing_health(current_user) + +# Comprehensive SEO Analysis endpoints +@app.post("/api/seo-dashboard/analyze-comprehensive") +async def analyze_seo_comprehensive_endpoint(request: SEOAnalysisRequest): + """Analyze a URL for comprehensive SEO performance.""" + return await analyze_seo_comprehensive(request) + +@app.post("/api/seo-dashboard/analyze-full") +async def analyze_seo_full_endpoint(request: SEOAnalysisRequest): + """Analyze a URL for comprehensive SEO performance.""" + return await analyze_seo_full(request) + +@app.get("/api/seo-dashboard/metrics-detailed") +async def seo_metrics_detailed(url: str): + """Get detailed SEO metrics for a URL.""" + return await get_seo_metrics_detailed(url) + +@app.get("/api/seo-dashboard/analysis-summary") +async def seo_analysis_summary(url: str): + """Get a quick summary of SEO analysis for a URL.""" + return await get_analysis_summary(url) + +@app.post("/api/seo-dashboard/batch-analyze") +async def batch_analyze_urls_endpoint(urls: list[str]): + """Analyze multiple URLs in batch.""" + return await batch_analyze_urls(urls) + +@app.post("/api/seo-dashboard/analyze-urls-ai") +async def analyze_urls_ai_endpoint(request: AnalyzeURLsRequest, current_user: dict = Depends(get_current_user)): + """Run AI-powered SEO analysis on selected URLs.""" + return await analyze_urls_ai(request, current_user) + +# Include platform analytics router if not PODCAST_ONLY_DEMO_MODE: - # SEO Dashboard endpoints - @app.get("/api/seo-dashboard/data") - async def seo_dashboard_data(): - """Get complete SEO dashboard data.""" - return await get_seo_dashboard_data() - - @app.get("/api/seo-dashboard/health-score") - async def seo_health_score(): - """Get SEO health score.""" - return await get_seo_health_score() - - @app.get("/api/seo-dashboard/metrics") - async def seo_metrics(): - """Get SEO metrics.""" - return await get_seo_metrics() - - @app.get("/api/seo-dashboard/platforms") - async def seo_platforms(current_user: dict = Depends(get_current_user)): - """Get platform status.""" - return await get_platform_status(current_user) - - @app.get("/api/seo-dashboard/insights") - async def seo_insights(): - """Get AI insights.""" - return await get_ai_insights() - - # New SEO Dashboard endpoints with real data - @app.get("/api/seo-dashboard/overview") - async def seo_dashboard_overview_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """Get comprehensive SEO dashboard overview with real GSC/Bing data.""" - return await get_seo_dashboard_overview(current_user, site_url) - - @app.get("/api/seo-dashboard/gsc/raw") - async def gsc_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """Get raw GSC data for the specified site.""" - return await get_gsc_raw_data(current_user, site_url) - - @app.get("/api/seo-dashboard/bing/raw") - async def bing_raw_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """Get raw Bing data for the specified site.""" - return await get_bing_raw_data(current_user, site_url) - - @app.get("/api/seo-dashboard/competitive-insights") - async def competitive_insights_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """Get competitive insights from onboarding step 3 data.""" - return await get_competitive_insights(current_user, site_url) - - @app.get("/api/seo-dashboard/deep-competitor-analysis") - async def deep_competitor_analysis_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """Get deep competitor analysis results (auto-scheduled post-onboarding).""" - return await get_deep_competitor_analysis(current_user, site_url) - - @app.post("/api/seo-dashboard/strategic-insights/run") - async def run_strategic_insights_endpoint(current_user: dict = Depends(get_current_user)): - """Run AI-powered strategic insights analysis manually.""" - return await run_strategic_insights(current_user) - - @app.get("/api/seo-dashboard/strategic-insights/history") - async def get_strategic_insights_history_endpoint(current_user: dict = Depends(get_current_user)): - """Fetch the history of strategic insights for the user.""" - return await get_strategic_insights_history(current_user) - - @app.post("/api/seo-dashboard/refresh") - async def refresh_analytics_data_endpoint(current_user: dict = Depends(get_current_user), site_url: str = None): - """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.""" - return await seo_dashboard_health_check() - - # Phase 2B: Semantic health monitoring endpoint (24-hour polling) - @app.get("/api/seo-dashboard/semantic-health") - async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)): - """Get real-time semantic health metrics for content and competitors.""" - return await get_semantic_health(current_user) - - @app.get("/api/seo-dashboard/cache-stats") - async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)): - """Get semantic cache performance statistics.""" - return await get_semantic_cache_stats(current_user) - - @app.get("/api/seo-dashboard/sif-health") - async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)): - """Get SIF indexing health summary for the current user.""" - return await get_sif_indexing_health(current_user) - - # Comprehensive SEO Analysis endpoints - @app.post("/api/seo-dashboard/analyze-comprehensive") - async def analyze_seo_comprehensive_endpoint(request: SEOAnalysisRequest): - """Analyze a URL for comprehensive SEO performance.""" - return await analyze_seo_comprehensive(request) - - @app.post("/api/seo-dashboard/analyze-full") - async def analyze_seo_full_endpoint(request: SEOAnalysisRequest): - """Analyze a URL for comprehensive SEO performance.""" - return await analyze_seo_full(request) - - @app.get("/api/seo-dashboard/metrics-detailed") - async def seo_metrics_detailed(url: str): - """Get detailed SEO metrics for a URL.""" - return await get_seo_metrics_detailed(url) - - @app.get("/api/seo-dashboard/analysis-summary") - async def seo_analysis_summary(url: str): - """Get a quick summary of SEO analysis for a URL.""" - return await get_analysis_summary(url) - - @app.post("/api/seo-dashboard/batch-analyze") - async def batch_analyze_urls_endpoint(urls: list[str]): - """Analyze multiple URLs in batch.""" - return await batch_analyze_urls(urls) - - @app.post("/api/seo-dashboard/analyze-urls-ai") - async def analyze_urls_ai_endpoint(request: AnalyzeURLsRequest, current_user: dict = Depends(get_current_user)): - """Run AI-powered SEO analysis on selected URLs.""" - return await analyze_urls_ai(request, current_user) - - # Include platform analytics router from routers.platform_analytics import router as platform_analytics_router app.include_router(platform_analytics_router) # Include Bing Analytics Storage router to expose storage-backed endpoints @@ -436,10 +489,23 @@ if not PODCAST_ONLY_DEMO_MODE: # Include content assets router from api.content_assets.router import router as content_assets_router app.include_router(content_assets_router) + router_group_status["platform_extensions"] = { + "mounted": True, + "reason": "Full mode", + } +else: + router_group_status["platform_extensions"] = { + "mounted": False, + "reason": "Skipped in podcast-only demo mode", + } # Include Podcast Maker router from api.podcast.router import router as podcast_router app.include_router(podcast_router) +router_group_status["podcast_maker"] = { + "mounted": True, + "reason": "Always mounted", +} if not PODCAST_ONLY_DEMO_MODE: # Include YouTube Creator Studio router @@ -465,6 +531,15 @@ if not PODCAST_ONLY_DEMO_MODE: # Today workflow routes from api.today_workflow import router as today_workflow_router app.include_router(today_workflow_router) + router_group_status["advanced_workflows"] = { + "mounted": True, + "reason": "Full mode", + } +else: + router_group_status["advanced_workflows"] = { + "mounted": False, + "reason": "Skipped in podcast-only demo mode", + } # Setup frontend serving using modular utilities frontend_serving.setup_frontend_serving() diff --git a/backend/start_alwrity_backend.py b/backend/start_alwrity_backend.py index 260c667a..a2a00766 100644 --- a/backend/start_alwrity_backend.py +++ b/backend/start_alwrity_backend.py @@ -160,6 +160,13 @@ from alwrity_utils import ( def start_backend(enable_reload=False, production_mode=False): """Start the backend server.""" print("🚀 Starting ALwrity Backend...") + podcast_only_demo_mode = os.getenv("ALWRITY_PODCAST_ONLY_DEMO_MODE", os.getenv("PODCAST_ONLY_DEMO_MODE", "false")).lower() in {"1", "true", "yes", "on"} + + if podcast_only_demo_mode: + print("\n" + "=" * 60) + print("🎙️ PODCAST-ONLY DEMO MODE ACTIVE") + print(" Non-podcast router groups are intentionally skipped.") + print("=" * 60) # Set host based on environment and mode # Use 127.0.0.1 for local production testing on Windows @@ -192,7 +199,6 @@ def start_backend(enable_reload=False, production_mode=False): print(f" 📍 Host: {host}") print(f" 🔌 Port: {port}") print(f" 🔄 Reload: {reload}") - print(f" 🔄 Reload: {reload}") try: # Import and run the app @@ -298,11 +304,6 @@ def main(): parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)") parser.add_argument("--production", action="store_true", help="Enable production mode (optimized for deployment)") parser.add_argument("--verbose", action="store_true", help="Enable verbose logging for debugging") - parser.add_argument( - "--podcast-only-demo", - action="store_true", - help="Enable podcast-only demo mode (sets PODCAST_ONLY_DEMO_MODE=true before app startup)", - ) args = parser.parse_args() # Determine mode @@ -312,8 +313,6 @@ def main(): # Set global verbose flag for utilities os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false" - if args.podcast_only_demo: - os.environ["PODCAST_ONLY_DEMO_MODE"] = "true" print("[*] ALwrity Backend Server") print("=" * 40) @@ -321,8 +320,6 @@ def main(): print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}") if verbose_mode: print("Verbose logging: ENABLED") - if args.podcast_only_demo: - print("Podcast-only demo mode: ENABLED") print("=" * 40) # Check if we're in the right directory