ALwrity Backend and Frontend - Stability and Error Handling Improvements

This commit is contained in:
ajaysi
2025-10-14 10:57:16 +05:30
parent b6debd80b7
commit 40fb6ac95b
31 changed files with 1491 additions and 592 deletions

View File

@@ -1,108 +0,0 @@
# Stability AI Configuration Example
# Copy this file to .env and fill in your actual values
# Required: Your Stability AI API Key
# Get your API key from: https://platform.stability.ai/account/keys
STABILITY_API_KEY=your_stability_api_key_here
# Optional: Stability AI API Base URL (default: https://api.stability.ai)
STABILITY_BASE_URL=https://api.stability.ai
# Optional: Request timeout in seconds (default: 300)
STABILITY_TIMEOUT=300
# Optional: Maximum retries for failed requests (default: 3)
STABILITY_MAX_RETRIES=3
# Optional: Maximum file size for uploads in bytes (default: 10MB)
STABILITY_MAX_FILE_SIZE=10485760
# Optional: Enable debug mode for detailed logging (default: false)
STABILITY_DEBUG=false
# Optional: Enable caching for responses (default: true)
STABILITY_ENABLE_CACHE=true
# Optional: Cache duration in seconds (default: 3600)
STABILITY_CACHE_DURATION=3600
# Optional: Enable rate limiting (default: true)
STABILITY_ENABLE_RATE_LIMIT=true
# Optional: Rate limit - requests per window (default: 150)
STABILITY_RATE_LIMIT_REQUESTS=150
# Optional: Rate limit window in seconds (default: 10)
STABILITY_RATE_LIMIT_WINDOW=10
# Optional: Enable content moderation (default: true)
STABILITY_ENABLE_MODERATION=true
# Optional: Enable request logging (default: true)
STABILITY_ENABLE_LOGGING=true
# Optional: Maximum log entries to keep in memory (default: 1000)
STABILITY_MAX_LOG_ENTRIES=1000
# Optional: Enable experimental features (default: false)
STABILITY_ENABLE_EXPERIMENTAL=false
# Optional: Default output format for images (default: png)
STABILITY_DEFAULT_IMAGE_FORMAT=png
# Optional: Default output format for audio (default: mp3)
STABILITY_DEFAULT_AUDIO_FORMAT=mp3
# Optional: Enable webhook support (default: false)
STABILITY_ENABLE_WEBHOOKS=false
# Optional: Webhook URL for generation completion notifications
STABILITY_WEBHOOK_URL=
# Optional: Webhook secret for signature validation
STABILITY_WEBHOOK_SECRET=
# Optional: Enable batch processing (default: true)
STABILITY_ENABLE_BATCH=true
# Optional: Maximum batch size (default: 10)
STABILITY_MAX_BATCH_SIZE=10
# Optional: Enable quality analysis features (default: true)
STABILITY_ENABLE_QUALITY_ANALYSIS=true
# Optional: Enable prompt optimization features (default: true)
STABILITY_ENABLE_PROMPT_OPTIMIZATION=true
# Optional: Default creativity level for upscaling (default: 0.35)
STABILITY_DEFAULT_CREATIVITY=0.35
# Optional: Default control strength for control operations (default: 0.7)
STABILITY_DEFAULT_CONTROL_STRENGTH=0.7
# Optional: Default style fidelity for style operations (default: 0.5)
STABILITY_DEFAULT_STYLE_FIDELITY=0.5
# Optional: Enable automatic image format optimization (default: true)
STABILITY_AUTO_OPTIMIZE_FORMAT=true
# Optional: Enable automatic parameter optimization (default: true)
STABILITY_AUTO_OPTIMIZE_PARAMS=true
# Optional: Default model for generate operations (default: core)
STABILITY_DEFAULT_GENERATE_MODEL=core
# Optional: Default model for upscale operations (default: fast)
STABILITY_DEFAULT_UPSCALE_MODEL=fast
# Optional: Enable cost tracking and warnings (default: true)
STABILITY_ENABLE_COST_TRACKING=true
# Optional: Credit warning threshold (default: 10)
STABILITY_CREDIT_WARNING_THRESHOLD=10
# Optional: Enable performance monitoring (default: true)
STABILITY_ENABLE_MONITORING=true
# Optional: Performance monitoring interval in seconds (default: 60)
STABILITY_MONITORING_INTERVAL=60

View File

@@ -16,72 +16,98 @@ class DatabaseSetup:
def setup_essential_tables(self) -> bool: def setup_essential_tables(self) -> bool:
"""Set up essential database tables.""" """Set up essential database tables."""
print("📊 Setting up essential database tables...") import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose:
print("📊 Setting up essential database tables...")
try: try:
from services.database import init_database, engine from services.database import init_database, engine
# Initialize database connection # Initialize database connection
init_database() init_database()
print(" ✅ Database connection initialized") if verbose:
print(" ✅ Database connection initialized")
# Create essential tables # Create essential tables
self._create_monitoring_tables() self._create_monitoring_tables()
self._create_subscription_tables() self._create_subscription_tables()
self._create_persona_tables() self._create_persona_tables()
print("✅ Essential database tables created") if verbose:
print("✅ Essential database tables created")
return True return True
except Exception as e: except Exception as e:
print(f"⚠️ Warning: Database setup failed: {e}") if verbose:
if self.production_mode: print(f"⚠️ Warning: Database setup failed: {e}")
print(" Continuing in production mode...") if self.production_mode:
return True print(" Continuing in production mode...")
else: else:
print(" This may affect functionality") print(" This may affect functionality")
return True # Don't fail startup for database issues return True # Don't fail startup for database issues
def _create_monitoring_tables(self) -> bool: def _create_monitoring_tables(self) -> bool:
"""Create API monitoring tables.""" """Create API monitoring tables."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
try: try:
from models.api_monitoring import Base as MonitoringBase from models.api_monitoring import Base as MonitoringBase
MonitoringBase.metadata.create_all(bind=engine) MonitoringBase.metadata.create_all(bind=engine)
print(" ✅ Monitoring tables created") if verbose:
print(" ✅ Monitoring tables created")
return True return True
except Exception as e: except Exception as e:
print(f" ⚠️ Monitoring tables failed: {e}") if verbose:
print(f" ⚠️ Monitoring tables failed: {e}")
return True # Non-critical return True # Non-critical
def _create_subscription_tables(self) -> bool: def _create_subscription_tables(self) -> bool:
"""Create subscription and billing tables.""" """Create subscription and billing tables."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
try: try:
from models.subscription_models import Base as SubscriptionBase from models.subscription_models import Base as SubscriptionBase
SubscriptionBase.metadata.create_all(bind=engine) SubscriptionBase.metadata.create_all(bind=engine)
print(" ✅ Subscription tables created") if verbose:
print(" ✅ Subscription tables created")
return True return True
except Exception as e: except Exception as e:
print(f" ⚠️ Subscription tables failed: {e}") if verbose:
print(f" ⚠️ Subscription tables failed: {e}")
return True # Non-critical return True # Non-critical
def _create_persona_tables(self) -> bool: def _create_persona_tables(self) -> bool:
"""Create persona analysis tables.""" """Create persona analysis tables."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
try: try:
from models.persona_models import Base as PersonaBase from models.persona_models import Base as PersonaBase
PersonaBase.metadata.create_all(bind=engine) PersonaBase.metadata.create_all(bind=engine)
print(" ✅ Persona tables created") if verbose:
print(" ✅ Persona tables created")
return True return True
except Exception as e: except Exception as e:
print(f" ⚠️ Persona tables failed: {e}") if verbose:
print(f" ⚠️ Persona tables failed: {e}")
return True # Non-critical return True # Non-critical
def verify_tables(self) -> bool: def verify_tables(self) -> bool:
"""Verify that essential tables exist.""" """Verify that essential tables exist."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if self.production_mode: if self.production_mode:
print("⚠️ Skipping table verification in production mode") if verbose:
print("⚠️ Skipping table verification in production mode")
return True return True
print("🔍 Verifying database tables...") if verbose:
print("🔍 Verifying database tables...")
try: try:
from services.database import engine from services.database import engine
@@ -97,11 +123,13 @@ class DatabaseSetup:
] ]
existing_tables = [table for table in essential_tables if table in tables] existing_tables = [table for table in essential_tables if table in tables]
print(f" ✅ Found tables: {existing_tables}") if verbose:
print(f" ✅ Found tables: {existing_tables}")
if len(existing_tables) < len(essential_tables): if len(existing_tables) < len(essential_tables):
missing = [table for table in essential_tables if table not in existing_tables] missing = [table for table in essential_tables if table not in existing_tables]
print(f" ⚠️ Missing tables: {missing}") if verbose:
print(f" ⚠️ Missing tables: {missing}")
return True return True
@@ -124,11 +152,11 @@ class DatabaseSetup:
# Set up billing tables # Set up billing tables
self._setup_billing_tables() self._setup_billing_tables()
print("✅ Advanced database features configured") logger.debug("✅ Advanced database features configured")
return True return True
except Exception as e: except Exception as e:
print(f"⚠️ Advanced table setup failed: {e}") logger.warning(f"Advanced table setup failed: {e}")
return True # Non-critical return True # Non-critical
def _setup_monitoring_tables(self) -> bool: def _setup_monitoring_tables(self) -> bool:
@@ -157,16 +185,16 @@ class DatabaseSetup:
# Check if tables already exist # Check if tables already exist
if check_existing_tables(engine): if check_existing_tables(engine):
print(" ✅ Billing tables already exist") logger.debug("✅ Billing tables already exist")
return True return True
if create_billing_tables(): if create_billing_tables():
print(" ✅ Billing tables created") logger.debug("✅ Billing tables created")
return True return True
else: else:
print(" ⚠️ Billing setup failed") logger.warning("Billing setup failed")
return True # Non-critical return True # Non-critical
except Exception as e: except Exception as e:
print(f" ⚠️ Billing setup failed: {e}") logger.warning(f"Billing setup failed: {e}")
return True # Non-critical return True # Non-critical

View File

@@ -51,40 +51,54 @@ class DependencyManager:
def check_critical_dependencies(self) -> Tuple[bool, List[str]]: def check_critical_dependencies(self) -> Tuple[bool, List[str]]:
"""Check if critical dependencies are available.""" """Check if critical dependencies are available."""
print("🔍 Checking critical dependencies...") import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose:
print("🔍 Checking critical dependencies...")
missing_packages = [] missing_packages = []
for package in self.critical_packages: for package in self.critical_packages:
try: try:
__import__(package.replace('-', '_')) __import__(package.replace('-', '_'))
print(f"{package}") if verbose:
print(f"{package}")
except ImportError: except ImportError:
print(f"{package} - MISSING") if verbose:
print(f"{package} - MISSING")
missing_packages.append(package) missing_packages.append(package)
if missing_packages: if missing_packages:
print(f"❌ Missing critical packages: {', '.join(missing_packages)}") if verbose:
print(f"❌ Missing critical packages: {', '.join(missing_packages)}")
return False, missing_packages return False, missing_packages
print("✅ All critical dependencies available!") if verbose:
print("✅ All critical dependencies available!")
return True, [] return True, []
def check_optional_dependencies(self) -> Tuple[bool, List[str]]: def check_optional_dependencies(self) -> Tuple[bool, List[str]]:
"""Check if optional dependencies are available.""" """Check if optional dependencies are available."""
print("🔍 Checking optional dependencies...") import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose:
print("🔍 Checking optional dependencies...")
missing_packages = [] missing_packages = []
for package in self.optional_packages: for package in self.optional_packages:
try: try:
__import__(package.replace('-', '_')) __import__(package.replace('-', '_'))
print(f"{package}") if verbose:
print(f"{package}")
except ImportError: except ImportError:
print(f" ⚠️ {package} - MISSING (optional)") if verbose:
print(f" ⚠️ {package} - MISSING (optional)")
missing_packages.append(package) missing_packages.append(package)
if missing_packages: if missing_packages and verbose:
print(f"⚠️ Missing optional packages: {', '.join(missing_packages)}") print(f"⚠️ Missing optional packages: {', '.join(missing_packages)}")
print(" Some features may not be available") print(" Some features may not be available")

View File

@@ -28,21 +28,29 @@ class EnvironmentSetup:
def setup_directories(self) -> bool: def setup_directories(self) -> bool:
"""Create necessary directories for ALwrity.""" """Create necessary directories for ALwrity."""
print("📁 Setting up directories...") import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose:
print("📁 Setting up directories...")
if not self.required_directories: if not self.required_directories:
print(" ⚠️ Skipping directory creation in production mode") if verbose:
print(" ⚠️ Skipping directory creation in production mode")
return True return True
for directory in self.required_directories: for directory in self.required_directories:
try: try:
Path(directory).mkdir(parents=True, exist_ok=True) Path(directory).mkdir(parents=True, exist_ok=True)
print(f" ✅ Created: {directory}") if verbose:
print(f" ✅ Created: {directory}")
except Exception as e: except Exception as e:
print(f" ❌ Failed to create {directory}: {e}") if verbose:
print(f" ❌ Failed to create {directory}: {e}")
return False return False
print("✅ All directories created successfully") if verbose:
print("✅ All directories created successfully")
return True return True
def setup_environment_variables(self) -> bool: def setup_environment_variables(self) -> bool:

View File

@@ -18,22 +18,31 @@ class RouterManager:
def include_router_safely(self, router, router_name: str = None) -> bool: def include_router_safely(self, router, router_name: str = None) -> bool:
"""Include a router safely with error handling.""" """Include a router safely with error handling."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
try: try:
self.app.include_router(router) self.app.include_router(router)
router_name = router_name or getattr(router, 'prefix', 'unknown') router_name = router_name or getattr(router, 'prefix', 'unknown')
self.included_routers.append(router_name) self.included_routers.append(router_name)
logger.info(f"✅ Router included successfully: {router_name}") if verbose:
logger.info(f"✅ Router included successfully: {router_name}")
return True return True
except Exception as e: except Exception as e:
router_name = router_name or 'unknown' router_name = router_name or 'unknown'
self.failed_routers.append({"name": router_name, "error": str(e)}) self.failed_routers.append({"name": router_name, "error": str(e)})
logger.warning(f"❌ Router inclusion failed: {router_name} - {e}") if verbose:
logger.warning(f"❌ Router inclusion failed: {router_name} - {e}")
return False return False
def include_core_routers(self) -> bool: def include_core_routers(self) -> bool:
"""Include core application routers.""" """Include core application routers."""
import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
try: try:
logger.info("Including core routers...") if verbose:
logger.info("Including core routers...")
# Component logic router # Component logic router
from api.component_logic import router as component_logic_router from api.component_logic import router as component_logic_router

View File

@@ -70,129 +70,103 @@ class OnboardingSummaryService:
try: try:
db = next(get_db()) db = next(get_db())
api_keys = self.db_service.get_api_keys(self.user_id, db) api_keys = self.db_service.get_api_keys(self.user_id, db)
logger.info(f"Retrieved {len(api_keys)} API keys from database for user {self.user_id}") db.close()
return api_keys
if not api_keys:
return {
"openai": {"configured": False, "value": None},
"anthropic": {"configured": False, "value": None},
"google": {"configured": False, "value": None}
}
return {
"openai": {
"configured": bool(api_keys.get('openai_api_key')),
"value": api_keys.get('openai_api_key')[:8] + "..." if api_keys.get('openai_api_key') else None
},
"anthropic": {
"configured": bool(api_keys.get('anthropic_api_key')),
"value": api_keys.get('anthropic_api_key')[:8] + "..." if api_keys.get('anthropic_api_key') else None
},
"google": {
"configured": bool(api_keys.get('google_api_key')),
"value": api_keys.get('google_api_key')[:8] + "..." if api_keys.get('google_api_key') else None
}
}
except Exception as e: except Exception as e:
logger.error(f"Error getting API keys from database: {e}") logger.error(f"Error getting API keys: {str(e)}")
return {} return {
"openai": {"configured": False, "value": None},
"anthropic": {"configured": False, "value": None},
"google": {"configured": False, "value": None}
}
def _get_website_analysis(self) -> Optional[Dict[str, Any]]: def _get_website_analysis(self) -> Optional[Dict[str, Any]]:
"""Get website analysis data from database (Step 2).""" """Get website analysis data from database."""
try: try:
db = next(get_db()) db = next(get_db())
website_data = self.db_service.get_website_analysis(self.user_id, db) website_data = self.db_service.get_website_analysis(self.user_id, db)
if website_data: db.close()
logger.info(f"Retrieved website analysis from database for user {self.user_id}")
else:
logger.warning(f"No website analysis found in database for user {self.user_id}")
return website_data return website_data
except Exception as e: except Exception as e:
logger.error(f"Error getting website analysis from database: {e}") logger.error(f"Error getting website analysis: {str(e)}")
return None return None
def _get_research_preferences(self) -> Optional[Dict[str, Any]]: def _get_research_preferences(self) -> Optional[Dict[str, Any]]:
"""Get research preferences data from database (Step 3).""" """Get research preferences from database."""
try: try:
db = next(get_db()) db = next(get_db())
research_data = self.db_service.get_research_preferences(self.user_id, db) preferences = self.db_service.get_research_preferences(self.user_id, db)
if research_data: db.close()
logger.info(f"Retrieved research preferences from database for user {self.user_id}") return preferences
else:
logger.warning(f"No research preferences found in database for user {self.user_id}")
return research_data
except Exception as e: except Exception as e:
logger.error(f"Error getting research preferences from database: {e}") logger.error(f"Error getting research preferences: {str(e)}")
return None return None
def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Get personalization settings from Step 4 (Persona) database.""" """Get personalization settings based on research preferences."""
try: if not research_preferences:
# Try to get from Step 4 (Persona) in database return {
db = next(get_db()) "writing_style": "professional",
persona_data = self.db_service.get_persona_data(self.user_id, db) "target_audience": "general",
"content_focus": "informative"
if persona_data: }
logger.info(f"Retrieved persona data from database for user {self.user_id}")
# Extract personalization settings from persona data return {
if 'corePersona' in persona_data: "writing_style": research_preferences.get('writing_style', 'professional'),
core_persona = persona_data.get('corePersona', {}) "target_audience": research_preferences.get('target_audience', 'general'),
return { "content_focus": research_preferences.get('content_focus', 'informative')
'writing_style': core_persona.get('linguistic_fingerprint', {}).get('tone', 'Professional'), }
'tone': core_persona.get('tonal_range', {}).get('primary_tone', 'Formal'),
'brand_voice': core_persona.get('identity', {}).get('voice', 'Trustworthy and Expert')
}
# Fallback to research preferences if persona data not available
if research_preferences:
logger.info(f"Using research preferences as fallback for personalization")
return {
'writing_style': research_preferences.get('writing_style', {}).get('tone', 'Professional'),
'tone': research_preferences.get('writing_style', {}).get('voice', 'Formal'),
'brand_voice': research_preferences.get('writing_style', {}).get('complexity', 'Trustworthy and Expert')
}
return None
except Exception as e:
logger.error(f"Error getting personalization settings from database: {e}")
return None
def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Check if persona can be generated.""" """Check if persona generation is ready based on available data."""
try: if not website_analysis:
persona_service = PersonaAnalysisService() return {
"ready": False,
# Check if persona can be generated "reason": "Website analysis not completed",
onboarding_data = persona_service._collect_onboarding_data(self.user_id) "missing_data": ["website_url", "style_analysis"]
if onboarding_data: }
data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data)
return { required_fields = ['website_url', 'writing_style', 'target_audience']
"ready": data_sufficiency >= 50.0, missing_fields = [field for field in required_fields if not website_analysis.get(field)]
"data_sufficiency": data_sufficiency,
"can_generate": website_analysis is not None return {
} "ready": len(missing_fields) == 0,
return {"ready": False, "data_sufficiency": 0.0, "can_generate": False} "reason": "All required data available" if len(missing_fields) == 0 else f"Missing: {', '.join(missing_fields)}",
except Exception as e: "missing_data": missing_fields
logger.warning(f"Could not check persona readiness: {str(e)}") }
return {"ready": False, "error": str(e)}
def _determine_capabilities(self, api_keys: Dict[str, Any], website_analysis: Optional[Dict[str, Any]], def _determine_capabilities(self, api_keys: Dict[str, Any], website_analysis: Optional[Dict[str, Any]],
research_preferences: Optional[Dict[str, Any]], research_preferences: Optional[Dict[str, Any]],
personalization_settings: Optional[Dict[str, Any]], personalization_settings: Dict[str, Any],
persona_readiness: Optional[Dict[str, Any]]) -> Dict[str, bool]: persona_readiness: Dict[str, Any]) -> Dict[str, Any]:
"""Determine user capabilities based on onboarding data.""" """Determine available capabilities based on configured data."""
return { capabilities = {
"ai_content": len(api_keys) > 0, "ai_content_generation": any(key.get("configured") for key in api_keys.values()),
"style_analysis": website_analysis is not None, "website_analysis": website_analysis is not None,
"research_tools": research_preferences is not None, "research_capabilities": research_preferences is not None,
"personalization": personalization_settings is not None, "persona_generation": persona_readiness.get("ready", False),
"persona_generation": persona_readiness.get("ready", False) if persona_readiness else False, "content_optimization": website_analysis is not None and research_preferences is not None
"integrations": False # TODO: Implement
} }
async def get_website_analysis_data(self) -> Optional[Dict[str, Any]]: return capabilities
"""Get website analysis data for FinalStep."""
try:
analysis = self._get_website_analysis()
if analysis:
return {
"website_url": analysis.get('website_url'),
"style_analysis": analysis.get('style_analysis'),
"style_patterns": analysis.get('style_patterns'),
"style_guidelines": analysis.get('style_guidelines'),
"status": analysis.get('status'),
"completed_at": analysis.get('created_at')
}
else:
return None
except Exception as e:
logger.error(f"Error getting website analysis data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_research_preferences_data(self) -> Optional[Dict[str, Any]]:
"""Get research preferences data for FinalStep."""
try:
return self._get_research_preferences()
except Exception as e:
logger.error(f"Error getting research preferences data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -19,6 +19,10 @@ from alwrity_utils import HealthChecker, RateLimiter, FrontendServing, RouterMan
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
# Set up clean logging for end users
from logging_config import setup_clean_logging
setup_clean_logging()
# Import middleware # Import middleware
from middleware.auth_middleware import get_current_user from middleware.auth_middleware import get_current_user

View File

@@ -1,27 +1,29 @@
# Clerk Authentication # ALwrity Backend Configuration
CLERK_SECRET_KEY=your_clerk_secret_key_here
CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here
# Google Search Console # API Keys (Configure these in the onboarding process)
GSC_REDIRECT_URI=your-domain-name/gsc/callback # OPENAI_API_KEY=your_openai_api_key_here
GEMINI_API_KEY=your_gemini_api_key_here
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
# MISTRAL_API_KEY=your_mistral_api_key_here
# Wix Integration (Headless OAuth - Client ID only, no Client Secret required) # Research API Keys (Optional)
WIX_CLIENT_ID= # TAVILY_API_KEY=your_tavily_api_key_here
WIX_REDIRECT_URI=your-domain-name/wix/callback # SERPER_API_KEY=your_serper_api_key_here
EXA_API_KEY=your_exa_api_key_here
# WordPress.com OAuth2 Integration # Authentication
# IMPORTANT: You need to register a WordPress.com application to get valid credentials # CLERK_SECRET_KEY=your_clerk_secret_key_here
# 1. Go to https://developer.wordpress.com/apps/
# 2. Create a new application
# 3. Set the redirect URI to: https://your-domain.com/wp/callback
# 4. Copy the Client ID and Client Secret below
# For development, these are placeholder values that may not work
WORDPRESS_CLIENT_ID=your_wordpress_com_client_id_here
WORDPRESS_CLIENT_SECRET=your_wordpress_com_client_secret_here
WORDPRESS_REDIRECT_URI=
# Development Settings # OAuth Redirect URIs
DISABLE_AUTH=false GSC_REDIRECT_URI=https://your-frontend.vercel.app/gsc/callback
WORDPRESS_REDIRECT_URI=https://your-frontend.vercel.app/wp/callback
WIX_REDIRECT_URI=https://your-frontend.vercel.app/wix/callback
# local development
DEPLOY_ENV=local # Server Configuration
HOST=0.0.0.0
PORT=8000
DEBUG=true
# Logging
LOG_LEVEL=INFO

110
backend/logging_config.py Normal file
View File

@@ -0,0 +1,110 @@
"""
Logging configuration for ALwrity backend.
Provides clean logging setup for end users vs developers.
"""
import logging
import os
import sys
from loguru import logger
def setup_clean_logging():
"""Set up clean logging for end users."""
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if not verbose_mode:
# Suppress verbose logging for end users - be more aggressive
logging.getLogger('sqlalchemy.engine').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.pool').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.orm').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy').setLevel(logging.CRITICAL)
logging.getLogger('sqlalchemy.engine.Engine').setLevel(logging.CRITICAL)
# Suppress service initialization logs
logging.getLogger('services').setLevel(logging.WARNING)
logging.getLogger('api').setLevel(logging.WARNING)
logging.getLogger('models').setLevel(logging.WARNING)
# Suppress specific noisy loggers
noisy_loggers = [
'linkedin_persona_service',
'facebook_persona_service',
'core_persona_service',
'persona_analysis_service',
'ai_service_manager',
'ai_engine_service',
'website_analyzer',
'competitor_analyzer',
'keyword_researcher',
'content_gap_analyzer',
'onboarding_data_service',
'comprehensive_user_data',
'strategy_data',
'gap_analysis_data',
'phase1_steps',
'daily_schedule_generator',
'gsc_service',
'wordpress_oauth',
'data_filter',
'source_mapper',
'grounding_engine',
'blog_content_seo_analyzer',
'linkedin_service',
'citation_manager',
'content_analyzer',
'linkedin_prompt_generator',
'linkedin_image_storage',
'hallucination_detector',
'writing_assistant',
'onboarding_data_service',
'enhanced_linguistic_analyzer',
'persona_quality_improver',
'logging_middleware',
'exa_service',
'step3_research_service',
'sitemap_service',
'linkedin_image_generator',
'linkedin_prompt_generator',
'linkedin_image_storage',
'router_manager',
'frontend_serving',
'database',
'user_business_info',
'auth_middleware',
'pricing_service',
'create_billing_tables'
]
for logger_name in noisy_loggers:
logging.getLogger(logger_name).setLevel(logging.WARNING)
# Configure loguru to be less verbose (only show warnings and errors)
logger.remove() # Remove default handler
def warning_only_filter(record):
return record["level"].name in ["WARNING", "ERROR", "CRITICAL"]
logger.add(
sys.stdout.write,
level="WARNING",
format="{time:HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n",
filter=warning_only_filter
)
else:
# In verbose mode, show all log levels with detailed formatting
logger.remove() # Remove default handler
logger.add(
sys.stdout.write,
level="DEBUG",
format="{time:HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n"
)
return verbose_mode
def get_uvicorn_log_level():
"""Get appropriate uvicorn log level based on verbose mode."""
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
return "debug" if verbose_mode else "warning"

View File

@@ -6,7 +6,7 @@ from datetime import datetime
Base = declarative_base() Base = declarative_base()
logger.info("🔄 Loading UserBusinessInfo model...") logger.debug("🔄 Loading UserBusinessInfo model...")
class UserBusinessInfo(Base): class UserBusinessInfo(Base):
__tablename__ = 'user_business_info' __tablename__ = 'user_business_info'
@@ -35,4 +35,4 @@ class UserBusinessInfo(Base):
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
} }
logger.info("✅ UserBusinessInfo model loaded successfully!") logger.debug("✅ UserBusinessInfo model loaded successfully!")

View File

@@ -26,12 +26,12 @@ def create_billing_tables():
try: try:
# Create engine # Create engine
engine = create_engine(DATABASE_URL, echo=True) engine = create_engine(DATABASE_URL, echo=False)
# Create all tables # Create all tables
logger.info("Creating billing and subscription system tables...") logger.debug("Creating billing and subscription system tables...")
SubscriptionBase.metadata.create_all(bind=engine) SubscriptionBase.metadata.create_all(bind=engine)
logger.info("✅ Billing and subscription tables created successfully") logger.debug("✅ Billing and subscription tables created successfully")
# Create session for data initialization # Create session for data initialization
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@@ -41,13 +41,13 @@ def create_billing_tables():
# Initialize pricing and plans # Initialize pricing and plans
pricing_service = PricingService(db) pricing_service = PricingService(db)
logger.info("Initializing default API pricing...") logger.debug("Initializing default API pricing...")
pricing_service.initialize_default_pricing() pricing_service.initialize_default_pricing()
logger.info("✅ Default API pricing initialized") logger.debug("✅ Default API pricing initialized")
logger.info("Initializing default subscription plans...") logger.debug("Initializing default subscription plans...")
pricing_service.initialize_default_plans() pricing_service.initialize_default_plans()
logger.info("✅ Default subscription plans initialized") logger.debug("✅ Default subscription plans initialized")
except Exception as e: except Exception as e:
logger.error(f"Error initializing default data: {e}") logger.error(f"Error initializing default data: {e}")
@@ -57,7 +57,7 @@ def create_billing_tables():
finally: finally:
db.close() db.close()
logger.info("🎉 Billing system setup completed successfully!") logger.info(" Billing system setup completed successfully!")
# Display summary # Display summary
display_setup_summary(engine) display_setup_summary(engine)
@@ -94,7 +94,7 @@ def display_setup_summary(engine):
logger.info(f"\n📊 Created Tables ({len(tables)}):") logger.info(f"\n📊 Created Tables ({len(tables)}):")
for table in tables: for table in tables:
logger.info(f"{table[0]}") logger.debug(f"{table[0]}")
# Check subscription plans # Check subscription plans
try: try:
@@ -114,7 +114,7 @@ def display_setup_summary(engine):
for plan in plans: for plan in plans:
name, tier, monthly, yearly = plan name, tier, monthly, yearly = plan
logger.info(f"{name} ({tier}): ${monthly}/month, ${yearly}/year") logger.debug(f"{name} ({tier}): ${monthly}/month, ${yearly}/year")
except Exception as e: except Exception as e:
logger.warning(f"Could not check subscription plans: {e}") logger.warning(f"Could not check subscription plans: {e}")
@@ -124,22 +124,22 @@ def display_setup_summary(engine):
result = conn.execute(pricing_query) result = conn.execute(pricing_query)
pricing_count = result.fetchone()[0] pricing_count = result.fetchone()[0]
logger.info(f"\n💰 API Pricing Entries: {pricing_count}") logger.info(f"\n💰 API Pricing Entries: {pricing_count}")
if pricing_count > 0: if pricing_count > 0:
pricing_detail_query = text(""" pricing_detail_query = text("""
SELECT provider, model_name, cost_per_input_token, cost_per_output_token SELECT provider, model_name, cost_per_input_token, cost_per_output_token
FROM api_provider_pricing FROM api_provider_pricing
WHERE cost_per_input_token > 0 OR cost_per_output_token > 0 WHERE cost_per_input_token > 0 OR cost_per_output_token > 0
ORDER BY provider, model_name ORDER BY provider, model_name
LIMIT 10 LIMIT 10
""") """)
result = conn.execute(pricing_detail_query) result = conn.execute(pricing_detail_query)
pricing_entries = result.fetchall() pricing_entries = result.fetchall()
logger.info("\n LLM Pricing (per token) - Top 10:") logger.info("\n LLM Pricing (per token) - Top 10:")
for entry in pricing_entries: for entry in pricing_entries:
provider, model, input_cost, output_cost = entry provider, model, input_cost, output_cost = entry
logger.info(f"{provider}/{model}: ${input_cost:.8f} in, ${output_cost:.8f} out") logger.debug(f"{provider}/{model}: ${input_cost:.8f} in, ${output_cost:.8f} out")
except Exception as e: except Exception as e:
logger.warning(f"Could not check API pricing: {e}") logger.warning(f"Could not check API pricing: {e}")
@@ -183,7 +183,7 @@ def check_existing_tables(engine):
if existing_tables: if existing_tables:
logger.warning(f"Found existing billing tables: {[t[0] for t in existing_tables]}") logger.warning(f"Found existing billing tables: {[t[0] for t in existing_tables]}")
logger.info("Tables already exist. Skipping creation to preserve data.") logger.debug("Tables already exist. Skipping creation to preserve data.")
return False return False
return True return True
@@ -193,7 +193,7 @@ def check_existing_tables(engine):
return True # Proceed anyway return True # Proceed anyway
if __name__ == "__main__": if __name__ == "__main__":
logger.info("🚀 Starting billing system database migration...") logger.debug("🚀 Starting billing system database migration...")
try: try:
# Create engine to check existing tables # Create engine to check existing tables
@@ -201,7 +201,7 @@ if __name__ == "__main__":
# Check existing tables # Check existing tables
if not check_existing_tables(engine): if not check_existing_tables(engine):
logger.info("✅ Billing tables already exist, skipping creation") logger.debug("✅ Billing tables already exist, skipping creation")
sys.exit(0) sys.exit(0)
# Create tables and initialize data # Create tables and initialize data
@@ -210,7 +210,7 @@ if __name__ == "__main__":
logger.info("✅ Billing system migration completed successfully!") logger.info("✅ Billing system migration completed successfully!")
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Migration cancelled by user") logger.warning("Migration cancelled by user")
sys.exit(0) sys.exit(0)
except Exception as e: except Exception as e:
logger.error(f"❌ Migration failed: {e}") logger.error(f"❌ Migration failed: {e}")

View File

@@ -47,15 +47,26 @@ class AIServiceMetrics:
class AIServiceManager: class AIServiceManager:
"""Centralized AI service management for content planning system.""" """Centralized AI service management for content planning system."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(AIServiceManager, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize AI service manager.""" """Initialize AI service manager (only once)."""
self.logger = logger if not self._initialized:
self.metrics: List[AIServiceMetrics] = [] self.logger = logger
self.prompts = self._load_centralized_prompts() self.metrics: List[AIServiceMetrics] = []
self.schemas = self._load_centralized_schemas() self.prompts = self._load_centralized_prompts()
self.config = self._load_ai_configuration() self.schemas = self._load_centralized_schemas()
self.config = self._load_ai_configuration()
logger.info("AIServiceManager initialized")
logger.debug("AIServiceManager initialized")
self._initialized = True
def _load_ai_configuration(self) -> Dict[str, Any]: def _load_ai_configuration(self) -> Dict[str, Any]:
"""Load AI configuration settings.""" """Load AI configuration settings."""

View File

@@ -24,10 +24,21 @@ from services.database import get_db_session
class AIEngineService: class AIEngineService:
"""AI engine for content planning insights and analysis.""" """AI engine for content planning insights and analysis."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(AIEngineService, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize the AI engine service.""" """Initialize the AI engine service (only once)."""
self.ai_service_manager = AIServiceManager() if not self._initialized:
logger.info("AIEngineService initialized") self.ai_service_manager = AIServiceManager()
logger.debug("AIEngineService initialized")
self._initialized = True
async def analyze_content_gaps(self, analysis_summary: Dict[str, Any]) -> Dict[str, Any]: async def analyze_content_gaps(self, analysis_summary: Dict[str, Any]) -> Dict[str, Any]:
""" """

View File

@@ -18,13 +18,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe
class CorePersonaService: class CorePersonaService:
"""Core service for generating writing personas using Gemini AI.""" """Core service for generating writing personas using Gemini AI."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(CorePersonaService, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize the core persona service.""" """Initialize the core persona service (only once)."""
self.data_collector = OnboardingDataCollector() if not self._initialized:
self.prompt_builder = PersonaPromptBuilder() self.data_collector = OnboardingDataCollector()
self.linkedin_service = LinkedInPersonaService() self.prompt_builder = PersonaPromptBuilder()
self.facebook_service = FacebookPersonaService() self.linkedin_service = LinkedInPersonaService()
logger.info("CorePersonaService initialized") self.facebook_service = FacebookPersonaService()
logger.debug("CorePersonaService initialized")
self._initialized = True
def generate_core_persona(self, onboarding_data: Dict[str, Any]) -> Dict[str, Any]: def generate_core_persona(self, onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
"""Generate core writing persona using Gemini structured response.""" """Generate core writing persona using Gemini structured response."""

View File

@@ -26,7 +26,7 @@ class EnhancedLinguisticAnalyzer:
import spacy import spacy
self.nlp = spacy.load("en_core_web_sm") self.nlp = spacy.load("en_core_web_sm")
self.spacy_available = True self.spacy_available = True
logger.info("SUCCESS: spaCy model loaded successfully - Enhanced linguistic analysis available") logger.debug("SUCCESS: spaCy model loaded successfully - Enhanced linguistic analysis available")
except ImportError as e: except ImportError as e:
logger.error(f"ERROR: spaCy is REQUIRED for persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm") logger.error(f"ERROR: spaCy is REQUIRED for persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm")
raise ImportError("spaCy is required for enhanced persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm") from e raise ImportError("spaCy is required for enhanced persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm") from e

View File

@@ -20,14 +20,25 @@ from services.llm_providers.gemini_provider import gemini_structured_json_respon
class FacebookPersonaService: class FacebookPersonaService:
"""Facebook-specific persona generation and optimization service.""" """Facebook-specific persona generation and optimization service."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(FacebookPersonaService, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize the Facebook persona service.""" """Initialize the Facebook persona service (only once)."""
self.schemas = FacebookPersonaSchema if not self._initialized:
self.constraints = FacebookPersonaConstraints() self.schemas = FacebookPersonaSchema
self.validation = FacebookPersonaValidation() self.constraints = FacebookPersonaConstraints()
self.optimization = FacebookPersonaOptimization() self.validation = FacebookPersonaValidation()
self.prompts = FacebookPersonaPrompts() self.optimization = FacebookPersonaOptimization()
logger.info("FacebookPersonaService initialized") self.prompts = FacebookPersonaPrompts()
logger.debug("FacebookPersonaService initialized")
self._initialized = True
def generate_facebook_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]: def generate_facebook_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
""" """

View File

@@ -14,11 +14,22 @@ from .linkedin_persona_schemas import LinkedInPersonaSchemas
class LinkedInPersonaService: class LinkedInPersonaService:
"""Service for generating LinkedIn-specific persona adaptations.""" """Service for generating LinkedIn-specific persona adaptations."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(LinkedInPersonaService, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize the LinkedIn persona service.""" """Initialize the LinkedIn persona service (only once)."""
self.prompts = LinkedInPersonaPrompts() if not self._initialized:
self.schemas = LinkedInPersonaSchemas() self.prompts = LinkedInPersonaPrompts()
logger.info("LinkedInPersonaService initialized") self.schemas = LinkedInPersonaSchemas()
logger.debug("LinkedInPersonaService initialized")
self._initialized = True
def generate_linkedin_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]: def generate_linkedin_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
""" """

View File

@@ -24,7 +24,7 @@ class PersonaQualityImprover:
def __init__(self): def __init__(self):
"""Initialize the quality improver.""" """Initialize the quality improver."""
self.linguistic_analyzer = EnhancedLinguisticAnalyzer() self.linguistic_analyzer = EnhancedLinguisticAnalyzer()
logger.info("PersonaQualityImprover initialized") logger.debug("PersonaQualityImprover initialized")
def assess_persona_quality_comprehensive( def assess_persona_quality_comprehensive(
self, self,

View File

@@ -19,13 +19,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe
class PersonaAnalysisService: class PersonaAnalysisService:
"""Service for analyzing onboarding data and generating writing personas using Gemini AI.""" """Service for analyzing onboarding data and generating writing personas using Gemini AI."""
_instance = None
_initialized = False
def __new__(cls):
"""Implement singleton pattern to prevent multiple initializations."""
if cls._instance is None:
cls._instance = super(PersonaAnalysisService, cls).__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
"""Initialize the persona analysis service.""" """Initialize the persona analysis service (only once)."""
self.core_persona_service = CorePersonaService() if not self._initialized:
self.data_collector = OnboardingDataCollector() self.core_persona_service = CorePersonaService()
self.linkedin_service = LinkedInPersonaService() self.data_collector = OnboardingDataCollector()
self.facebook_service = FacebookPersonaService() self.linkedin_service = LinkedInPersonaService()
logger.info("PersonaAnalysisService initialized") self.facebook_service = FacebookPersonaService()
logger.debug("PersonaAnalysisService initialized")
self._initialized = True
def generate_persona_from_onboarding(self, user_id: int, onboarding_session_id: int = None) -> Dict[str, Any]: def generate_persona_from_onboarding(self, user_id: int, onboarding_session_id: int = None) -> Dict[str, Any]:
""" """

View File

@@ -215,7 +215,7 @@ class PricingService:
self.db.add(pricing) self.db.add(pricing)
self.db.commit() self.db.commit()
logger.info("Default API pricing initialized") logger.debug("Default API pricing initialized")
def initialize_default_plans(self): def initialize_default_plans(self):
"""Initialize default subscription plans.""" """Initialize default subscription plans."""
@@ -318,7 +318,7 @@ class PricingService:
self.db.add(plan) self.db.add(plan)
self.db.commit() self.db.commit()
logger.info("Default subscription plans initialized") logger.debug("Default subscription plans initialized")
def calculate_api_cost(self, provider: APIProvider, model_name: str, def calculate_api_cost(self, provider: APIProvider, model_name: str,
tokens_input: int = 0, tokens_output: int = 0, tokens_input: int = 0, tokens_output: int = 0,

View File

@@ -17,28 +17,37 @@ def bootstrap_linguistic_models():
This prevents import-time failures when EnhancedLinguisticAnalyzer is loaded. This prevents import-time failures when EnhancedLinguisticAnalyzer is loaded.
""" """
import subprocess import subprocess
import os
print("🔍 Bootstrapping linguistic models...") verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose:
print("🔍 Bootstrapping linguistic models...")
# Check and download spaCy model # Check and download spaCy model
try: try:
import spacy import spacy
try: try:
nlp = spacy.load("en_core_web_sm") nlp = spacy.load("en_core_web_sm")
print(" ✅ spaCy model 'en_core_web_sm' available") if verbose:
print(" ✅ spaCy model 'en_core_web_sm' available")
except OSError: except OSError:
print(" ⚠️ spaCy model 'en_core_web_sm' not found, downloading...") if verbose:
print(" ⚠️ spaCy model 'en_core_web_sm' not found, downloading...")
try: try:
subprocess.check_call([ subprocess.check_call([
sys.executable, "-m", "spacy", "download", "en_core_web_sm" sys.executable, "-m", "spacy", "download", "en_core_web_sm"
]) ])
print(" ✅ spaCy model downloaded successfully") if verbose:
print(" ✅ spaCy model downloaded successfully")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f" ❌ Failed to download spaCy model: {e}") if verbose:
print(" Please run: python -m spacy download en_core_web_sm") print(f" ❌ Failed to download spaCy model: {e}")
print(" Please run: python -m spacy download en_core_web_sm")
return False return False
except ImportError: except ImportError:
print(" ⚠️ spaCy not installed - skipping") if verbose:
print(" ⚠️ spaCy not installed - skipping")
# Check and download NLTK data # Check and download NLTK data
try: try:
@@ -52,25 +61,32 @@ def bootstrap_linguistic_models():
for data_package, path in essential_data: for data_package, path in essential_data:
try: try:
nltk.data.find(path) nltk.data.find(path)
print(f" ✅ NLTK {data_package} available") if verbose:
print(f" ✅ NLTK {data_package} available")
except LookupError: except LookupError:
print(f" ⚠️ NLTK {data_package} not found, downloading...") if verbose:
print(f" ⚠️ NLTK {data_package} not found, downloading...")
try: try:
nltk.download(data_package, quiet=True) nltk.download(data_package, quiet=True)
print(f" ✅ NLTK {data_package} downloaded") if verbose:
print(f" ✅ NLTK {data_package} downloaded")
except Exception as e: except Exception as e:
print(f" ⚠️ Failed to download {data_package}: {e}") if verbose:
print(f" ⚠️ Failed to download {data_package}: {e}")
# Try fallback # Try fallback
if data_package == 'punkt_tab': if data_package == 'punkt_tab':
try: try:
nltk.download('punkt', quiet=True) nltk.download('punkt', quiet=True)
print(f" ✅ NLTK punkt (fallback) downloaded") if verbose:
print(f" ✅ NLTK punkt (fallback) downloaded")
except: except:
pass pass
except ImportError: except ImportError:
print(" ⚠️ NLTK not installed - skipping") if verbose:
print(" ⚠️ NLTK not installed - skipping")
print("✅ Linguistic model bootstrap complete") if verbose:
print("✅ Linguistic model bootstrap complete")
return True return True
@@ -127,11 +143,10 @@ def start_backend(enable_reload=False, production_mode=False):
import uvicorn import uvicorn
# Explicitly initialize database before starting server # Explicitly initialize database before starting server
print("[DB] Initializing database...")
init_database() init_database()
print("[OK] Database initialized successfully")
print("\n🌐 Backend is starting...") print("\n🌐 ALwrity Backend Server")
print("=" * 50)
print(" 📖 API Documentation: http://localhost:8000/api/docs") print(" 📖 API Documentation: http://localhost:8000/api/docs")
print(" 🔍 Health Check: http://localhost:8000/health") print(" 🔍 Health Check: http://localhost:8000/health")
print(" 📊 ReDoc: http://localhost:8000/api/redoc") print(" 📊 ReDoc: http://localhost:8000/api/redoc")
@@ -142,12 +157,13 @@ def start_backend(enable_reload=False, production_mode=False):
print(" 📊 Usage Tracking: http://localhost:8000/api/subscription/usage/demo") print(" 📊 Usage Tracking: http://localhost:8000/api/subscription/usage/demo")
print("\n[STOP] Press Ctrl+C to stop the server") print("\n[STOP] Press Ctrl+C to stop the server")
print("=" * 60) print("=" * 50)
print("\n💡 Usage:")
print(" Production mode: python start_alwrity_backend.py --production") # Set up clean logging for end users
print(" Development mode: python start_alwrity_backend.py --dev") from logging_config import setup_clean_logging, get_uvicorn_log_level
print(" With auto-reload: python start_alwrity_backend.py --reload")
print("=" * 60) verbose_mode = setup_clean_logging()
uvicorn_log_level = get_uvicorn_log_level()
uvicorn.run( uvicorn.run(
"app:app", "app:app",
@@ -186,7 +202,7 @@ def start_backend(enable_reload=False, production_mode=False):
"api/**/*.py", "api/**/*.py",
"services/**/*.py" "services/**/*.py"
], ],
log_level="info" log_level=uvicorn_log_level
) )
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -205,16 +221,23 @@ def main():
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)") 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("--production", action="store_true", help="Enable production mode (optimized for deployment)")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging for debugging")
args = parser.parse_args() args = parser.parse_args()
# Determine mode # Determine mode
production_mode = args.production production_mode = args.production
enable_reload = (args.reload or args.dev) and not production_mode enable_reload = (args.reload or args.dev) and not production_mode
verbose_mode = args.verbose
print("ALwrity Backend Server") # Set global verbose flag for utilities
os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false"
print("🚀 ALwrity Backend Server")
print("=" * 40) print("=" * 40)
print(f"Mode: {'PRODUCTION' if production_mode else 'DEVELOPMENT'}") print(f"Mode: {'PRODUCTION' if production_mode else 'DEVELOPMENT'}")
print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}") print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}")
if verbose_mode:
print("Verbose logging: ENABLED")
print("=" * 40) print("=" * 40)
# Check if we're in the right directory # Check if we're in the right directory
@@ -230,39 +253,59 @@ def main():
database_setup = DatabaseSetup(production_mode=production_mode) database_setup = DatabaseSetup(production_mode=production_mode)
production_optimizer = ProductionOptimizer() production_optimizer = ProductionOptimizer()
# Setup progress tracking
setup_steps = [
"Checking dependencies",
"Setting up environment",
"Configuring database",
"Starting server"
]
print("🔧 Initializing ALwrity...")
# Apply production optimizations if needed # Apply production optimizations if needed
if production_mode: if production_mode:
if not production_optimizer.apply_production_optimizations(): if not production_optimizer.apply_production_optimizations():
print("[ERROR] Production optimization failed") print(" Production optimization failed")
return False return False
# Check and install dependencies # Step 1: Dependencies
print(f" 📦 {setup_steps[0]}...", end=" ", flush=True)
critical_ok, missing_critical = dependency_manager.check_critical_dependencies() critical_ok, missing_critical = dependency_manager.check_critical_dependencies()
if not critical_ok: if not critical_ok:
print("[ERROR] Critical dependencies missing, installing...") print("installing...", end=" ", flush=True)
if not dependency_manager.install_requirements(): if not dependency_manager.install_requirements():
print("[ERROR] Failed to install dependencies") print("❌ Failed")
return False return False
print("✅ Done")
else:
print("✅ Done")
# Check optional dependencies (non-critical) # Check optional dependencies (non-critical) - only in verbose mode
dependency_manager.check_optional_dependencies() if verbose_mode:
dependency_manager.check_optional_dependencies()
# Setup environment # Step 2: Environment
print(f" 🔧 {setup_steps[1]}...", end=" ", flush=True)
if not environment_setup.setup_directories(): if not environment_setup.setup_directories():
print("[ERROR] Directory setup failed") print(" Directory setup failed")
return False return False
if not environment_setup.setup_environment_variables(): if not environment_setup.setup_environment_variables():
print("[ERROR] Environment variable setup failed") print(" Environment setup failed")
return False return False
# Create .env file only in development # Create .env file only in development
if not production_mode: if not production_mode:
environment_setup.create_env_file() environment_setup.create_env_file()
print("✅ Done")
# Setup database # Step 3: Database
print(f" 📊 {setup_steps[2]}...", end=" ", flush=True)
if not database_setup.setup_essential_tables(): if not database_setup.setup_essential_tables():
print("[WARNING] Database setup had issues, continuing...") print("⚠️ Issues detected, continuing...")
else:
print("✅ Done")
# Setup advanced features in development, verify in all modes # Setup advanced features in development, verify in all modes
if not production_mode: if not production_mode:
@@ -274,7 +317,8 @@ def main():
# Note: Linguistic models (spaCy/NLTK) are bootstrapped before imports # Note: Linguistic models (spaCy/NLTK) are bootstrapped before imports
# See bootstrap_linguistic_models() at the top of this file # See bootstrap_linguistic_models() at the top of this file
# Start backend # Step 4: Start backend
print(f" 🚀 {setup_steps[3]}...")
return start_backend(enable_reload=enable_reload, production_mode=production_mode) return start_backend(enable_reload=enable_reload, production_mode=production_mode)

View File

@@ -1,18 +0,0 @@
# Production Environment Variables for Vercel
# Copy this file to .env.production and update with your actual values
# Backend API URL (from your deployment platform)
# Examples:
# REACT_APP_API_URL=https://alwrity.onrender.com
# REACT_APP_API_URL=https://your-app.railway.app
# REACT_APP_API_URL=https://your-app.herokuapp.com
REACT_APP_API_URL=https://alwrity.onrender.com
# Alternative backend URL (fallback)
# REACT_APP_BACKEND_URL=https://alwrity.onrender.com
# Environment
REACT_APP_ENVIRONMENT=production
# Optional: Custom domain for OAuth redirects
# REACT_APP_DOMAIN=your-custom-domain.com

View File

@@ -57,5 +57,6 @@
"devDependencies": { "devDependencies": {
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"proxy": "http://localhost:8000" "proxy": "http://localhost:8000",
"homepage": "/"
} }

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Box, CircularProgress, Typography } from '@mui/material'; import { Box, CircularProgress, Typography } from '@mui/material';
import { CopilotKit } from "@copilotkit/react-core"; import { CopilotKit } from "@copilotkit/react-core";
import { ClerkProvider, useAuth } from '@clerk/clerk-react'; import { ClerkProvider, useAuth } from '@clerk/clerk-react';
@@ -26,6 +26,7 @@ import { SubscriptionProvider } from './contexts/SubscriptionContext';
import { apiClient, setAuthTokenGetter } from './api/client'; import { apiClient, setAuthTokenGetter } from './api/client';
import { useOnboarding } from './contexts/OnboardingContext'; import { useOnboarding } from './contexts/OnboardingContext';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import ConnectionErrorPage from './components/shared/ConnectionErrorPage';
// interface OnboardingStatus { // interface OnboardingStatus {
// onboarding_required: boolean; // onboarding_required: boolean;
@@ -37,9 +38,6 @@ import { useState, useEffect } from 'react';
// Conditional CopilotKit wrapper that only shows sidebar on content-planning route // Conditional CopilotKit wrapper that only shows sidebar on content-planning route
const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ children }) => { const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
// const isContentPlanningRoute = location.pathname === '/content-planning';
// Do not render CopilotSidebar here. Let specific pages/components control it. // Do not render CopilotSidebar here. Let specific pages/components control it.
return <>{children}</>; return <>{children}</>;
}; };
@@ -54,6 +52,13 @@ const InitialRouteHandler: React.FC = () => {
plan: string; plan: string;
isNewUser: boolean; isNewUser: boolean;
} | null>(null); } | null>(null);
const [connectionError, setConnectionError] = useState<{
hasError: boolean;
error: Error | null;
}>({
hasError: false,
error: null,
});
useEffect(() => { useEffect(() => {
const checkSubscription = async () => { const checkSubscription = async () => {
@@ -61,18 +66,35 @@ const InitialRouteHandler: React.FC = () => {
const userId = localStorage.getItem('user_id') || 'anonymous'; const userId = localStorage.getItem('user_id') || 'anonymous';
const response = await apiClient.get(`/api/subscription/status/${userId}`); const response = await apiClient.get(`/api/subscription/status/${userId}`);
const subscriptionData = response.data.data; const subscriptionData = response.data.data;
// Check if user is new (no subscription record at all) // Check if user is new (no subscription record at all)
const isNewUser = !subscriptionData || subscriptionData.plan === 'none'; const isNewUser = !subscriptionData || subscriptionData.plan === 'none';
setSubscriptionStatus({ setSubscriptionStatus({
active: subscriptionData?.active || false, active: subscriptionData?.active || false,
plan: subscriptionData?.plan || 'none', plan: subscriptionData?.plan || 'none',
isNewUser isNewUser
}); });
} catch (err) {
// Clear any connection errors
setConnectionError({
hasError: false,
error: null,
});
} catch (err: any) {
console.error('Error checking subscription:', err); console.error('Error checking subscription:', err);
// On error, treat as new user
// Check if it's a connection error - handle it locally
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
setConnectionError({
hasError: true,
error: err,
});
return; // Don't set subscription status for connection errors
}
// For other errors, treat as new user
setSubscriptionStatus({ setSubscriptionStatus({
active: false, active: false,
plan: 'none', plan: 'none',
@@ -86,6 +108,65 @@ const InitialRouteHandler: React.FC = () => {
checkSubscription(); checkSubscription();
}, []); }, []);
// Handle connection error - show connection error page
if (connectionError.hasError) {
const handleRetry = () => {
setConnectionError({
hasError: false,
error: null,
});
setCheckingSubscription(true);
// Re-trigger the subscription check
const checkSubscription = async () => {
try {
const userId = localStorage.getItem('user_id') || 'anonymous';
const response = await apiClient.get(`/api/subscription/status/${userId}`);
const subscriptionData = response.data.data;
const isNewUser = !subscriptionData || subscriptionData.plan === 'none';
setSubscriptionStatus({
active: subscriptionData?.active || false,
plan: subscriptionData?.plan || 'none',
isNewUser
});
} catch (err: any) {
console.error('Error checking subscription on retry:', err);
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
setConnectionError({
hasError: true,
error: err,
});
} else {
setSubscriptionStatus({
active: false,
plan: 'none',
isNewUser: true
});
}
} finally {
setCheckingSubscription(false);
}
};
checkSubscription();
};
const handleGoHome = () => {
window.location.href = '/';
};
return (
<ConnectionErrorPage
onRetry={handleRetry}
onGoHome={handleGoHome}
message={connectionError.error?.message || "Backend service is not available. Please check if the server is running."}
title="Connection Error"
/>
);
}
// Loading state - checking both subscription and onboarding // Loading state - checking both subscription and onboarding
if (loading || checkingSubscription) { if (loading || checkingSubscription) {
return ( return (
@@ -200,7 +281,6 @@ const TokenInstaller: React.FC = () => {
const App: React.FC = () => { const App: React.FC = () => {
// React Hooks MUST be at the top before any conditionals // React Hooks MUST be at the top before any conditionals
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Get CopilotKit key from localStorage or .env // Get CopilotKit key from localStorage or .env
const [copilotApiKey, setCopilotApiKey] = useState(() => { const [copilotApiKey, setCopilotApiKey] = useState(() => {
@@ -208,18 +288,10 @@ const App: React.FC = () => {
return savedKey || process.env.REACT_APP_COPILOTKIT_API_KEY || ''; return savedKey || process.env.REACT_APP_COPILOTKIT_API_KEY || '';
}); });
// Initialize app - loading state will be managed by InitialRouteHandler
useEffect(() => { useEffect(() => {
const checkBackendHealth = async () => { // Remove manual health check - connection errors are handled by ErrorBoundary
try { setLoading(false);
await apiClient.get('/health');
setLoading(false);
} catch (err) {
setError('Backend service is not available. Please check if the server is running.');
setLoading(false);
}
};
checkBackendHealth();
}, []); }, []);
// Listen for CopilotKit key updates // Listen for CopilotKit key updates
@@ -257,29 +329,6 @@ const App: React.FC = () => {
); );
} }
if (error) {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
minHeight="100vh"
gap={2}
p={3}
>
<Typography variant="h5" color="error" gutterBottom>
Connection Error
</Typography>
<Typography variant="body1" color="textSecondary" textAlign="center">
{error}
</Typography>
<Typography variant="body2" color="textSecondary" textAlign="center">
Please ensure the backend server is running and try refreshing the page.
</Typography>
</Box>
);
}
// Get environment variables with fallbacks // Get environment variables with fallbacks
const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || ''; const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || '';

View File

@@ -72,6 +72,21 @@ apiClient.interceptors.request.use(
} }
); );
// Custom error types for better error handling
export class ConnectionError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConnectionError';
}
}
export class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
// Add response interceptor with automatic token refresh on 401 // Add response interceptor with automatic token refresh on 401
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => { (response) => {
@@ -79,11 +94,30 @@ apiClient.interceptors.response.use(
}, },
async (error) => { async (error) => {
const originalRequest = error.config; const originalRequest = error.config;
// Handle network errors and timeouts (backend not available)
if (!error.response) {
// Network error, timeout, or backend not reachable
const connectionError = new NetworkError(
'Unable to connect to the backend server. Please check if the server is running.'
);
console.error('Network/Connection Error:', error.message || error);
return Promise.reject(connectionError);
}
// Handle server errors (5xx)
if (error.response.status >= 500) {
const connectionError = new ConnectionError(
'Backend server is experiencing issues. Please try again later.'
);
console.error('Server Error:', error.response.status, error.response.data);
return Promise.reject(connectionError);
}
// If 401 and we haven't retried yet, try to refresh token and retry // If 401 and we haven't retried yet, try to refresh token and retry
if (error?.response?.status === 401 && !originalRequest._retry && authTokenGetter) { if (error?.response?.status === 401 && !originalRequest._retry && authTokenGetter) {
originalRequest._retry = true; originalRequest._retry = true;
try { try {
// Get fresh token // Get fresh token
const newToken = await authTokenGetter(); const newToken = await authTokenGetter();
@@ -96,9 +130,9 @@ apiClient.interceptors.response.use(
} catch (retryError) { } catch (retryError) {
console.error('Token refresh failed:', retryError); console.error('Token refresh failed:', retryError);
} }
// If retry failed and not in onboarding, redirect // If retry failed and not in onboarding, redirect
const isOnboardingRoute = window.location.pathname.includes('/onboarding') || const isOnboardingRoute = window.location.pathname.includes('/onboarding') ||
window.location.pathname === '/'; window.location.pathname === '/';
if (!isOnboardingRoute) { if (!isOnboardingRoute) {
try { window.location.assign('/'); } catch {} try { window.location.assign('/'); } catch {}
@@ -106,7 +140,7 @@ apiClient.interceptors.response.use(
console.warn('401 Unauthorized - token refresh failed'); console.warn('401 Unauthorized - token refresh failed');
} }
} }
console.error('API Error:', error.response?.status, error.response?.data); console.error('API Error:', error.response?.status, error.response?.data);
return Promise.reject(error); return Promise.reject(error);
} }

View File

@@ -0,0 +1,498 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Paper,
Typography,
Button,
Stack,
CircularProgress,
Alert,
LinearProgress,
Fade,
Chip,
Card,
CardContent,
List,
ListItem,
ListItemIcon,
ListItemText
} from '@mui/material';
import {
WifiOff as ConnectionIcon,
Refresh as RefreshIcon,
Home as HomeIcon,
Settings as SettingsIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Schedule as ScheduleIcon,
CloudQueue as CloudIcon
} from '@mui/icons-material';
interface BackendConnectionErrorProps {
error?: string;
onRetry?: () => void;
onGoHome?: () => void;
}
interface ConnectionAttempt {
timestamp: number;
success: boolean;
responseTime?: number;
}
/**
* Enhanced Backend Connection Error Component
*
* Shows a loading state for 2 minutes while attempting to reconnect,
* then gracefully shows error if backend remains unresponsive.
*/
const BackendConnectionError: React.FC<BackendConnectionErrorProps> = ({
error = 'Backend service is not available. Please check if the server is running.',
onRetry,
onGoHome
}) => {
const [isRetrying, setIsRetrying] = useState(true);
const [timeElapsed, setTimeElapsed] = useState(0);
const [connectionAttempts, setConnectionAttempts] = useState<ConnectionAttempt[]>([]);
const [showError, setShowError] = useState(false);
const MAX_RETRY_TIME = 120; // 2 minutes in seconds
const RETRY_INTERVAL = 5000; // 5 seconds
// Timer for elapsed time
useEffect(() => {
const timer = setInterval(() => {
setTimeElapsed(prev => {
const newTime = prev + 1;
if (newTime >= MAX_RETRY_TIME) {
setIsRetrying(false);
setShowError(true);
return MAX_RETRY_TIME;
}
return newTime;
});
}, 1000);
return () => clearInterval(timer);
}, []);
// Retry attempts
useEffect(() => {
if (!isRetrying) return;
const retryTimer = setInterval(async () => {
const startTime = Date.now();
try {
// Try to connect to a simple health check endpoint with short timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
const response = await fetch('/health', {
method: 'GET',
signal: controller.signal
});
clearTimeout(timeoutId);
const responseTime = Date.now() - startTime;
setConnectionAttempts(prev => [...prev, {
timestamp: Date.now(),
success: response.ok,
responseTime
}]);
if (response.ok) {
// Backend is back online, trigger retry callback
setIsRetrying(false);
setShowError(false);
if (onRetry) {
onRetry();
}
return;
}
} catch (error: any) {
setConnectionAttempts(prev => [...prev, {
timestamp: Date.now(),
success: false,
responseTime: Date.now() - startTime
}]);
}
}, RETRY_INTERVAL);
return () => clearInterval(retryTimer);
}, [isRetrying, onRetry]);
const progress = (timeElapsed / MAX_RETRY_TIME) * 100;
const minutes = Math.floor(timeElapsed / 60);
const seconds = timeElapsed % 60;
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`;
const handleManualRetry = () => {
setIsRetrying(true);
setShowError(false);
setTimeElapsed(0);
setConnectionAttempts([]);
};
const handleGoHome = () => {
if (onGoHome) {
onGoHome();
} else {
window.location.href = '/';
}
};
if (showError) {
// Show final error state after 2 minutes
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
p: { xs: 2, md: 4 },
}}
>
<Container maxWidth="md">
<Paper
elevation={24}
sx={{
p: { xs: 3, md: 5 },
borderRadius: 4,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
}}
>
<Stack spacing={4} alignItems="center">
{/* Error Icon */}
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
}}
>
<ConnectionIcon sx={{ fontSize: 48, color: 'white' }} />
</Box>
{/* Error Title */}
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
color: '#1a1a1a',
textAlign: 'center',
}}
>
Connection Error
</Typography>
{/* Error Message */}
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center', maxWidth: 600 }}
>
{error}
</Typography>
{/* Troubleshooting Tips */}
<Card sx={{ width: '100%', maxWidth: 600 }}>
<CardContent>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SettingsIcon />
Troubleshooting Steps
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Check if the backend server is running" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Verify the backend is accessible on the correct port" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Check firewall and network settings" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Review backend server logs for errors" />
</ListItem>
</List>
</CardContent>
</Card>
{/* Action Buttons */}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ width: '100%', maxWidth: 400 }}
>
<Button
variant="contained"
size="large"
startIcon={<RefreshIcon />}
onClick={handleManualRetry}
sx={{
flex: 1,
py: 1.5,
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
'&:hover': {
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
},
}}
>
Try Again
</Button>
<Button
variant="outlined"
size="large"
startIcon={<HomeIcon />}
onClick={handleGoHome}
sx={{
flex: 1,
py: 1.5,
borderColor: '#667eea',
color: '#667eea',
'&:hover': {
borderColor: '#5568d3',
background: 'rgba(102, 126, 234, 0.05)',
},
}}
>
Go Home
</Button>
</Stack>
{/* Connection Attempts Summary */}
{connectionAttempts.length > 0 && (
<Box sx={{ width: '100%', maxWidth: 400 }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Connection attempts: {connectionAttempts.length}
</Typography>
<Typography variant="caption" color="text.secondary">
Last attempt: {connectionAttempts[connectionAttempts.length - 1]?.success ? 'Successful' : 'Failed'}
</Typography>
</Box>
)}
</Stack>
</Paper>
</Container>
</Box>
);
}
// Show loading state for first 2 minutes
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
p: { xs: 2, md: 4 },
}}
>
<Container maxWidth="md">
<Paper
elevation={24}
sx={{
p: { xs: 3, md: 5 },
borderRadius: 4,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
}}
>
<Stack spacing={4} alignItems="center">
{/* Loading Animation */}
<Box sx={{ position: 'relative' }}>
<CircularProgress
size={80}
thickness={4}
sx={{
color: '#667eea',
animation: 'pulse 2s ease-in-out infinite',
'@keyframes pulse': {
'0%': { opacity: 1 },
'50%': { opacity: 0.5 },
'100%': { opacity: 1 },
},
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CloudIcon sx={{ fontSize: 32, color: '#667eea' }} />
</Box>
</Box>
{/* Loading Title */}
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
color: '#1a1a1a',
textAlign: 'center',
}}
>
Connecting to Backend
</Typography>
{/* Progress Bar */}
<Box sx={{ width: '100%', maxWidth: 400 }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(102, 126, 234, 0.2)',
'& .MuiLinearProgress-bar': {
backgroundColor: '#667eea',
borderRadius: 4,
},
}}
/>
</Box>
{/* Time and Progress Info */}
<Stack spacing={1} alignItems="center">
<Typography variant="body1" color="text.secondary">
Attempting to reconnect... {timeString}
</Typography>
<Chip
label={`${Math.round(progress)}% complete`}
color="primary"
variant="outlined"
size="small"
/>
</Stack>
{/* Motivational Messages */}
<Fade in timeout={1000}>
<Alert
severity="info"
icon={<ScheduleIcon />}
sx={{
maxWidth: 500,
textAlign: 'center',
'& .MuiAlert-message': {
width: '100%',
},
}}
>
<Typography variant="body2">
{timeElapsed < 60
? "We're working to restore your connection..."
: "Still trying to connect. This may take a moment..."
}
</Typography>
</Alert>
</Fade>
{/* Connection Attempts */}
{connectionAttempts.length > 0 && (
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Connection attempts: {connectionAttempts.length}
</Typography>
<Typography variant="caption" color="text.secondary">
{connectionAttempts.filter(attempt => attempt.success).length} successful,{' '}
{connectionAttempts.filter(attempt => !attempt.success).length} failed
</Typography>
</Box>
)}
{/* Action Buttons */}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ width: '100%', maxWidth: 400 }}
>
<Button
variant="outlined"
size="large"
startIcon={<RefreshIcon />}
onClick={handleManualRetry}
sx={{
flex: 1,
py: 1.5,
borderColor: '#667eea',
color: '#667eea',
'&:hover': {
borderColor: '#5568d3',
background: 'rgba(102, 126, 234, 0.05)',
},
}}
>
Retry Now
</Button>
<Button
variant="text"
size="large"
startIcon={<HomeIcon />}
onClick={handleGoHome}
sx={{
flex: 1,
py: 1.5,
color: '#666',
'&:hover': {
background: 'rgba(0, 0, 0, 0.05)',
},
}}
>
Go Home
</Button>
</Stack>
{/* Help Text */}
<Typography
variant="caption"
color="text.secondary"
sx={{ textAlign: 'center', maxWidth: 400 }}
>
If this issue persists, please check your internet connection and ensure the backend server is running.
</Typography>
</Stack>
</Paper>
</Container>
</Box>
);
};
export default BackendConnectionError;

View File

@@ -0,0 +1,317 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Paper,
Typography,
Button,
Stack,
LinearProgress,
Alert,
Fade,
Slide,
} from '@mui/material';
import {
WifiOff as WifiOffIcon,
Refresh as RefreshIcon,
Home as HomeIcon,
CheckCircle as CheckCircleIcon,
ErrorOutline as ErrorOutlineIcon,
CloudQueue as CloudIcon,
Schedule as ScheduleIcon,
} from '@mui/icons-material';
import { keyframes } from '@mui/system';
interface ConnectionErrorPageProps {
onRetry?: () => void;
onGoHome?: () => void;
showRetry?: boolean;
message?: string;
title?: string;
}
const pulse = keyframes`
0% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.6; transform: scale(1); }
`;
const ConnectionErrorPage: React.FC<ConnectionErrorPageProps> = ({
onRetry,
onGoHome,
showRetry = true,
message = "Backend service is not available. Please check if the server is running.",
title = "Connection Error"
}) => {
const [countdown, setCountdown] = useState(120); // 2 minutes
const [isRetrying, setIsRetrying] = useState(false);
const [showProgress, setShowProgress] = useState(true);
useEffect(() => {
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
setShowProgress(false);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
const handleRetry = async () => {
if (!onRetry || isRetrying) return;
setIsRetrying(true);
try {
await onRetry();
} finally {
setIsRetrying(false);
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<Fade in timeout={800}>
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
p: { xs: 2, md: 4 },
}}
>
<Container maxWidth="md">
<Slide in timeout={600} direction="up">
<Paper
elevation={24}
sx={{
p: { xs: 3, md: 5 },
borderRadius: 4,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Animated background elements */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 4,
background: countdown > 0
? 'linear-gradient(90deg, #667eea, #764ba2)'
: 'linear-gradient(90deg, #f44336, #e91e63)',
}}
/>
<Stack spacing={4} alignItems="center">
{/* Animated Icon */}
<Box
sx={{
width: 100,
height: 100,
borderRadius: '50%',
background: countdown > 0
? 'linear-gradient(45deg, #ff9800 30%, #f44336 90%)'
: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
animation: countdown > 0 ? `${pulse} 2s ease-in-out infinite` : 'none',
}}
>
{countdown > 0 ? (
<CloudIcon sx={{ fontSize: 60, color: 'white' }} />
) : (
<WifiOffIcon sx={{ fontSize: 60, color: 'white' }} />
)}
</Box>
{/* Title and Status */}
<Box textAlign="center">
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
color: countdown > 0 ? '#1a1a1a' : '#d32f2f',
mb: 1,
}}
>
{title}
</Typography>
{countdown > 0 ? (
<Typography
variant="h6"
color="primary"
sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
}}
>
<CheckCircleIcon color="success" />
Attempting to reconnect...
</Typography>
) : (
<Typography
variant="h6"
color="error"
sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
}}
>
<ErrorOutlineIcon />
Connection failed
</Typography>
)}
</Box>
{/* Countdown Timer */}
{showProgress && countdown > 0 && (
<Box sx={{ width: '100%', maxWidth: 400 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Retrying connection...
</Typography>
<Typography variant="body2" color="primary" fontWeight={600}>
{formatTime(countdown)}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={((120 - countdown) / 120) * 100}
sx={{
height: 8,
borderRadius: 4,
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(45deg, #667eea, #764ba2)',
},
}}
/>
</Box>
)}
{/* Main Message */}
<Alert
severity={countdown > 0 ? "info" : "error"}
icon={countdown > 0 ? <ScheduleIcon /> : <ErrorOutlineIcon />}
sx={{
width: '100%',
maxWidth: 600,
textAlign: 'left',
'& .MuiAlert-message': {
width: '100%',
},
}}
>
<Typography variant="body1" gutterBottom>
{message}
</Typography>
{countdown > 0 ? (
<Typography variant="body2" color="text.secondary">
Please ensure the backend server is running and try refreshing the page.
We'll keep trying to connect automatically.
</Typography>
) : (
<Typography variant="body2" color="text.secondary">
The backend server appears to be unavailable. Please check if it's running
and try again, or contact support if the issue persists.
</Typography>
)}
</Alert>
{/* Action Buttons */}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ width: '100%', maxWidth: 400 }}
>
{showRetry && (
<Button
variant="contained"
size="large"
startIcon={<RefreshIcon />}
onClick={handleRetry}
disabled={isRetrying}
sx={{
flex: 1,
py: 1.5,
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
'&:hover': {
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
},
'&:disabled': {
background: 'rgba(102, 126, 234, 0.5)',
},
}}
>
{isRetrying ? 'Retrying...' : 'Retry Connection'}
</Button>
)}
<Button
variant="outlined"
size="large"
startIcon={<HomeIcon />}
onClick={onGoHome}
sx={{
flex: 1,
py: 1.5,
borderColor: '#667eea',
color: '#667eea',
'&:hover': {
borderColor: '#5568d3',
background: 'rgba(102, 126, 234, 0.05)',
},
}}
>
Go Home
</Button>
</Stack>
{/* Help Text */}
<Typography
variant="caption"
color="text.secondary"
sx={{ textAlign: 'center', maxWidth: 500 }}
>
{countdown > 0
? `Automatic retry in ${formatTime(countdown)}. Check your terminal for server status.`
: "Error ID: connection_" + Date.now().toString(36) + " • Timestamp: " + new Date().toLocaleString()
}
</Typography>
</Stack>
</Paper>
</Slide>
</Container>
</Box>
</Fade>
);
};
export default ConnectionErrorPage;

View File

@@ -136,13 +136,16 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
const { error } = this.state;
// Custom fallback UI provided // Custom fallback UI provided
if (this.props.fallback) { if (this.props.fallback) {
return this.props.fallback; return this.props.fallback;
} }
// Default fallback UI // Default fallback UI
const { error, errorInfo, showDetails } = this.state; const { errorInfo, showDetails } = this.state;
const { context, showDetails: showDetailsDefault } = this.props; const { context, showDetails: showDetailsDefault } = this.props;
return ( return (

View File

@@ -121,6 +121,13 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({ children
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
console.error('OnboardingContext: Error fetching data:', err); console.error('OnboardingContext: Error fetching data:', err);
// Check if it's a connection error that should be handled at the app level
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
// Re-throw connection errors to be handled by the app-level error boundary
throw err;
}
setError(err instanceof Error ? err.message : 'Failed to load onboarding data'); setError(err instanceof Error ? err.message : 'Failed to load onboarding data');
setLoading(false); setLoading(false);
} }

View File

@@ -64,6 +64,13 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
setSubscription(subscriptionData); setSubscription(subscriptionData);
} catch (err) { } catch (err) {
console.error('Error checking subscription:', err); console.error('Error checking subscription:', err);
// Check if it's a connection error that should be handled at the app level
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
// Re-throw connection errors to be handled by the app-level error boundary
throw err;
}
setError(err instanceof Error ? err.message : 'Failed to check subscription'); setError(err instanceof Error ? err.message : 'Failed to check subscription');
// Default to free tier on error // Default to free tier on error

View File

@@ -1,150 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modal Integration Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.test-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-button {
background-color: #1976d2;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
.test-button:hover {
background-color: #1565c0;
}
.log-container {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
font-family: monospace;
font-size: 14px;
max-height: 300px;
overflow-y: auto;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-radius: 3px;
}
.log-info { background-color: #d1ecf1; }
.log-success { background-color: #d4edda; }
.log-error { background-color: #f8d7da; }
</style>
</head>
<body>
<div class="test-container">
<h1>Calendar Generation Modal Integration Test</h1>
<p>This test verifies that the calendar generation modal can be triggered and displays properly.</p>
<h2>Test Steps:</h2>
<ol>
<li>Click the "Test Modal Integration" button below</li>
<li>Check if the modal opens and displays progress</li>
<li>Monitor the logs for any errors</li>
</ol>
<button class="test-button" onclick="testModalIntegration()">
Test Modal Integration
</button>
<button class="test-button" onclick="testBackendConnection()">
Test Backend Connection
</button>
<div class="log-container" id="logContainer">
<div class="log-entry log-info">Ready to test modal integration...</div>
</div>
</div>
<script>
function addLog(message, type = 'info') {
const logContainer = document.getElementById('logContainer');
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
async function testBackendConnection() {
addLog('Testing backend connection...', 'info');
try {
const response = await fetch('/api/content-planning/calendar-generation/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: 1,
calendar_type: 'monthly',
industry: 'technology',
business_size: 'sme',
force_refresh: false
}),
});
if (response.ok) {
const data = await response.json();
addLog(`Backend connection successful! Session ID: ${data.session_id}`, 'success');
} else {
addLog(`Backend connection failed: ${response.status} ${response.statusText}`, 'error');
}
} catch (error) {
addLog(`Backend connection error: ${error.message}`, 'error');
}
}
function testModalIntegration() {
addLog('Testing modal integration...', 'info');
// Simulate the calendar generation flow
const mockCalendarConfig = {
calendarType: 'monthly',
startDate: '2024-01-01',
calendarDuration: 30,
postingFrequency: 3,
priorityPlatforms: ['LinkedIn', 'Twitter'],
timeZone: 'UTC',
includeWeekends: true,
autoSchedule: true,
generateTopics: true
};
addLog(`Calendar config prepared: ${JSON.stringify(mockCalendarConfig)}`, 'info');
// Simulate the API call that should trigger the modal
testBackendConnection().then(() => {
addLog('Modal should now be visible if integration is working correctly', 'success');
addLog('Check the main application for the modal display', 'info');
});
}
// Auto-test on page load
window.addEventListener('load', () => {
addLog('Page loaded, ready for testing', 'info');
});
</script>
</body>
</html>