Compare commits

..

1 Commits

Author SHA1 Message Date
ي
ef7874dcdc Fail demo startup when required API routes are missing 2026-03-30 07:56:05 +05:30
2 changed files with 238 additions and 174 deletions

View File

@@ -48,8 +48,6 @@ load_dotenv(backend_dir / '.env') # backend/.env
load_dotenv(project_root / '.env') # root .env (fallback) load_dotenv(project_root / '.env') # root .env (fallback)
load_dotenv() # CWD .env (fallback) load_dotenv() # CWD .env (fallback)
PODCAST_ONLY_DEMO_MODE = os.getenv("PODCAST_ONLY_DEMO_MODE", "false").lower() in {"1", "true", "yes", "on"}
# Set up clean logging for end users # Set up clean logging for end users
from logging_config import setup_clean_logging from logging_config import setup_clean_logging
setup_clean_logging() setup_clean_logging()
@@ -112,8 +110,7 @@ from services.startup_health import (
# Import OAuth token monitoring routes # Import OAuth token monitoring routes
from api.oauth_token_monitoring_routes import router as oauth_token_monitoring_router from api.oauth_token_monitoring_routes import router as oauth_token_monitoring_router
if not PODCAST_ONLY_DEMO_MODE: # Import SEO Dashboard endpoints
# Import SEO Dashboard endpoints only when non-demo features are enabled
from api.seo_dashboard import ( from api.seo_dashboard import (
get_seo_dashboard_data, get_seo_dashboard_data,
get_seo_health_score, get_seo_health_score,
@@ -262,17 +259,12 @@ async def onboarding_status():
return onboarding_manager.get_onboarding_status() return onboarding_manager.get_onboarding_status()
# Include routers using modular utilities # Include routers using modular utilities
if not PODCAST_ONLY_DEMO_MODE:
router_manager.include_core_routers() router_manager.include_core_routers()
router_manager.include_optional_routers() router_manager.include_optional_routers()
else:
logger.info("PODCAST_ONLY_DEMO_MODE enabled: including only podcast and subscription feature routers.")
app.include_router(subscription_router)
# Include assets serving router (must be mounted to serve generated images) # Include assets serving router (must be mounted to serve generated images)
app.include_router(assets_serving_router) app.include_router(assets_serving_router)
if not PODCAST_ONLY_DEMO_MODE:
# SEO Dashboard endpoints # SEO Dashboard endpoints
@app.get("/api/seo-dashboard/data") @app.get("/api/seo-dashboard/data")
async def seo_dashboard_data(): async def seo_dashboard_data():
@@ -340,6 +332,8 @@ if not PODCAST_ONLY_DEMO_MODE:
"""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") @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): 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.""" """Get consolidated health for onboarding-scheduled SEO tasks."""
@@ -353,17 +347,31 @@ if not PODCAST_ONLY_DEMO_MODE:
# Phase 2B: Semantic health monitoring endpoint (24-hour polling) # Phase 2B: Semantic health monitoring endpoint (24-hour polling)
@app.get("/api/seo-dashboard/semantic-health") @app.get("/api/seo-dashboard/semantic-health")
async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)): async def semantic_health_endpoint(current_user: dict = Depends(get_current_user)):
"""Get real-time semantic health metrics for content and competitors.""" """
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) return await get_semantic_health(current_user)
@app.get("/api/seo-dashboard/cache-stats") @app.get("/api/seo-dashboard/cache-stats")
async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)): async def semantic_cache_stats_endpoint(current_user: dict = Depends(get_current_user)):
"""Get semantic cache performance statistics.""" """
Get semantic cache performance statistics.
Returns hit rate, memory usage, and eviction counts.
"""
return await get_semantic_cache_stats(current_user) return await get_semantic_cache_stats(current_user)
@app.get("/api/seo-dashboard/sif-health") @app.get("/api/seo-dashboard/sif-health")
async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)): async def sif_indexing_health_endpoint(current_user: dict = Depends(get_current_user)):
"""Get SIF indexing health summary for the 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) return await get_sif_indexing_health(current_user)
# Comprehensive SEO Analysis endpoints # Comprehensive SEO Analysis endpoints
@@ -416,7 +424,6 @@ if not PODCAST_ONLY_DEMO_MODE:
from api.podcast.router import router as podcast_router from api.podcast.router import router as podcast_router
app.include_router(podcast_router) app.include_router(podcast_router)
if not PODCAST_ONLY_DEMO_MODE:
# Include YouTube Creator Studio router # Include YouTube Creator Studio router
from api.youtube.router import router as youtube_router from api.youtube.router import router as youtube_router
app.include_router(youtube_router, prefix="/api") app.include_router(youtube_router, prefix="/api")
@@ -455,7 +462,7 @@ async def serve_frontend():
async def startup_event(): async def startup_event():
"""Initialize services on startup.""" """Initialize services on startup."""
try: try:
startup_report = run_startup_health_routine() startup_report = run_startup_health_routine(app)
if startup_report.get("status") != "healthy": if startup_report.get("status") != "healthy":
logger.error(f"Startup readiness finished with failures: {startup_report.get('errors', [])}") logger.error(f"Startup readiness finished with failures: {startup_report.get('errors', [])}")

View File

@@ -3,6 +3,8 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from fastapi import FastAPI
from fastapi.routing import APIRoute
from loguru import logger from loguru import logger
from sqlalchemy import inspect, text from sqlalchemy import inspect, text
@@ -49,6 +51,60 @@ def _record_check(checks: List[Dict[str, Any]], name: str, ok: bool, detail: str
checks.append({"name": name, "ok": ok, "detail": detail}) checks.append({"name": name, "ok": ok, "detail": detail})
def _is_demo_mode() -> bool:
app_env = os.getenv("APP_ENV", os.getenv("ENV", os.getenv("DEPLOY_ENV", ""))).strip().lower()
if app_env == "demo":
return True
return _env_true("ALWRITY_DEMO_MODE", default=False)
def _check_required_demo_routes(
app: Optional[FastAPI],
checks: List[Dict[str, Any]],
errors: List[str],
) -> None:
if not _is_demo_mode():
_record_check(
checks,
"demo_required_routes",
True,
"Skipped (not in demo mode). Set APP_ENV=demo or ALWRITY_DEMO_MODE=true to enforce.",
)
return
if app is None:
errors.append(
"Demo startup route check could not run because FastAPI app context was not provided to startup health routine."
)
_record_check(checks, "demo_required_routes_context", False, "missing app context")
return
required_routes = {
"/api/subscription/plans": "GET",
"/api/podcast/projects": "GET",
}
available_routes = {
(route.path, method)
for route in app.router.routes
if isinstance(route, APIRoute)
for method in route.methods
}
missing: List[str] = []
for path, method in required_routes.items():
if (path, method) in available_routes:
_record_check(checks, f"demo_route_{path}_{method}", True, "route registered")
else:
missing.append(f"{method} {path}")
_record_check(checks, f"demo_route_{path}_{method}", False, "route missing")
if missing:
errors.append(
"Demo mode startup check failed. Missing required API endpoints: "
f"{', '.join(missing)}. Ensure subscription and podcast routers are imported and included during app setup."
)
def _check_workspace_root(checks: List[Dict[str, Any]], errors: List[str]) -> None: def _check_workspace_root(checks: List[Dict[str, Any]], errors: List[str]) -> None:
workspace = Path(WORKSPACE_DIR) workspace = Path(WORKSPACE_DIR)
if not workspace.exists(): if not workspace.exists():
@@ -144,7 +200,7 @@ def _check_db_access(checks: List[Dict[str, Any]], errors: List[str], warnings:
return candidate_user return candidate_user
def run_startup_health_routine() -> Dict[str, Any]: def run_startup_health_routine(app: Optional[FastAPI] = None) -> Dict[str, Any]:
checks: List[Dict[str, Any]] = [] checks: List[Dict[str, Any]] = []
errors: List[str] = [] errors: List[str] = []
warnings: List[str] = [] warnings: List[str] = []
@@ -152,6 +208,7 @@ def run_startup_health_routine() -> Dict[str, Any]:
_check_workspace_root(checks, errors) _check_workspace_root(checks, errors)
if not errors: if not errors:
_check_db_access(checks, errors, warnings) _check_db_access(checks, errors, warnings)
_check_required_demo_routes(app, checks, errors)
status = "healthy" if not errors else "failed" status = "healthy" if not errors else "failed"
report = { report = {