From 40fb6ac95be44a62cc2d66a74b5b041246e33ab7 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Tue, 14 Oct 2025 10:57:16 +0530 Subject: [PATCH] ALwrity Backend and Frontend - Stability and Error Handling Improvements --- backend/.env.stability.example | 108 ---- backend/alwrity_utils/database_setup.py | 80 ++- backend/alwrity_utils/dependency_manager.py | 32 +- backend/alwrity_utils/environment_setup.py | 18 +- backend/alwrity_utils/router_manager.py | 15 +- .../onboarding_summary_service.py | 184 +++---- backend/app.py | 4 + backend/env_template.txt | 46 +- backend/logging_config.py | 110 ++++ backend/models/user_business_info.py | 4 +- backend/scripts/create_billing_tables.py | 38 +- backend/services/ai_service_manager.py | 27 +- .../content_gap_analyzer/ai_engine_service.py | 17 +- .../core_persona/core_persona_service.py | 23 +- .../persona/enhanced_linguistic_analyzer.py | 2 +- .../facebook/facebook_persona_service.py | 25 +- .../linkedin/linkedin_persona_service.py | 19 +- .../persona/persona_quality_improver.py | 2 +- backend/services/persona_analysis_service.py | 23 +- backend/services/pricing_service.py | 4 +- backend/start_alwrity_backend.py | 118 +++-- frontend/env.production.example | 18 - frontend/package.json | 3 +- frontend/src/App.tsx | 135 +++-- frontend/src/api/client.ts | 44 +- .../shared/BackendConnectionError.tsx | 498 ++++++++++++++++++ .../components/shared/ConnectionErrorPage.tsx | 317 +++++++++++ .../src/components/shared/ErrorBoundary.tsx | 5 +- frontend/src/contexts/OnboardingContext.tsx | 7 + frontend/src/contexts/SubscriptionContext.tsx | 7 + frontend/test_modal_integration.html | 150 ------ 31 files changed, 1491 insertions(+), 592 deletions(-) delete mode 100644 backend/.env.stability.example create mode 100644 backend/logging_config.py delete mode 100644 frontend/env.production.example create mode 100644 frontend/src/components/shared/BackendConnectionError.tsx create mode 100644 frontend/src/components/shared/ConnectionErrorPage.tsx delete mode 100644 frontend/test_modal_integration.html diff --git a/backend/.env.stability.example b/backend/.env.stability.example deleted file mode 100644 index 126780bf..00000000 --- a/backend/.env.stability.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/alwrity_utils/database_setup.py b/backend/alwrity_utils/database_setup.py index 9981b878..8386e42a 100644 --- a/backend/alwrity_utils/database_setup.py +++ b/backend/alwrity_utils/database_setup.py @@ -16,72 +16,98 @@ class DatabaseSetup: def setup_essential_tables(self) -> bool: """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: from services.database import init_database, engine # Initialize database connection init_database() - print(" āœ… Database connection initialized") + if verbose: + print(" āœ… Database connection initialized") # Create essential tables self._create_monitoring_tables() self._create_subscription_tables() self._create_persona_tables() - print("āœ… Essential database tables created") + if verbose: + print("āœ… Essential database tables created") return True except Exception as e: - print(f"āš ļø Warning: Database setup failed: {e}") - if self.production_mode: - print(" Continuing in production mode...") - return True - else: - print(" This may affect functionality") - return True # Don't fail startup for database issues + if verbose: + print(f"āš ļø Warning: Database setup failed: {e}") + if self.production_mode: + print(" Continuing in production mode...") + else: + print(" This may affect functionality") + return True # Don't fail startup for database issues def _create_monitoring_tables(self) -> bool: """Create API monitoring tables.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + try: from models.api_monitoring import Base as MonitoringBase MonitoringBase.metadata.create_all(bind=engine) - print(" āœ… Monitoring tables created") + if verbose: + print(" āœ… Monitoring tables created") return True except Exception as e: - print(f" āš ļø Monitoring tables failed: {e}") + if verbose: + print(f" āš ļø Monitoring tables failed: {e}") return True # Non-critical def _create_subscription_tables(self) -> bool: """Create subscription and billing tables.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + try: from models.subscription_models import Base as SubscriptionBase SubscriptionBase.metadata.create_all(bind=engine) - print(" āœ… Subscription tables created") + if verbose: + print(" āœ… Subscription tables created") return True except Exception as e: - print(f" āš ļø Subscription tables failed: {e}") + if verbose: + print(f" āš ļø Subscription tables failed: {e}") return True # Non-critical def _create_persona_tables(self) -> bool: """Create persona analysis tables.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + try: from models.persona_models import Base as PersonaBase PersonaBase.metadata.create_all(bind=engine) - print(" āœ… Persona tables created") + if verbose: + print(" āœ… Persona tables created") return True except Exception as e: - print(f" āš ļø Persona tables failed: {e}") + if verbose: + print(f" āš ļø Persona tables failed: {e}") return True # Non-critical def verify_tables(self) -> bool: """Verify that essential tables exist.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + if self.production_mode: - print("āš ļø Skipping table verification in production mode") + if verbose: + print("āš ļø Skipping table verification in production mode") return True - print("šŸ” Verifying database tables...") + if verbose: + print("šŸ” Verifying database tables...") try: from services.database import engine @@ -97,11 +123,13 @@ class DatabaseSetup: ] 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): 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 @@ -124,11 +152,11 @@ class DatabaseSetup: # Set up billing tables self._setup_billing_tables() - print("āœ… Advanced database features configured") + logger.debug("āœ… Advanced database features configured") return True except Exception as e: - print(f"āš ļø Advanced table setup failed: {e}") + logger.warning(f"Advanced table setup failed: {e}") return True # Non-critical def _setup_monitoring_tables(self) -> bool: @@ -157,16 +185,16 @@ class DatabaseSetup: # Check if tables already exist if check_existing_tables(engine): - print(" āœ… Billing tables already exist") + logger.debug("āœ… Billing tables already exist") return True if create_billing_tables(): - print(" āœ… Billing tables created") + logger.debug("āœ… Billing tables created") return True else: - print(" āš ļø Billing setup failed") + logger.warning("Billing setup failed") return True # Non-critical except Exception as e: - print(f" āš ļø Billing setup failed: {e}") + logger.warning(f"Billing setup failed: {e}") return True # Non-critical diff --git a/backend/alwrity_utils/dependency_manager.py b/backend/alwrity_utils/dependency_manager.py index ed9d7a1a..340b30d9 100644 --- a/backend/alwrity_utils/dependency_manager.py +++ b/backend/alwrity_utils/dependency_manager.py @@ -51,40 +51,54 @@ class DependencyManager: def check_critical_dependencies(self) -> Tuple[bool, List[str]]: """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 = [] for package in self.critical_packages: try: __import__(package.replace('-', '_')) - print(f" āœ… {package}") + if verbose: + print(f" āœ… {package}") except ImportError: - print(f" āŒ {package} - MISSING") + if verbose: + print(f" āŒ {package} - MISSING") missing_packages.append(package) 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 - print("āœ… All critical dependencies available!") + if verbose: + print("āœ… All critical dependencies available!") return True, [] def check_optional_dependencies(self) -> Tuple[bool, List[str]]: """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 = [] for package in self.optional_packages: try: __import__(package.replace('-', '_')) - print(f" āœ… {package}") + if verbose: + print(f" āœ… {package}") except ImportError: - print(f" āš ļø {package} - MISSING (optional)") + if verbose: + print(f" āš ļø {package} - MISSING (optional)") missing_packages.append(package) - if missing_packages: + if missing_packages and verbose: print(f"āš ļø Missing optional packages: {', '.join(missing_packages)}") print(" Some features may not be available") diff --git a/backend/alwrity_utils/environment_setup.py b/backend/alwrity_utils/environment_setup.py index 67c49758..404ffbba 100644 --- a/backend/alwrity_utils/environment_setup.py +++ b/backend/alwrity_utils/environment_setup.py @@ -28,21 +28,29 @@ class EnvironmentSetup: def setup_directories(self) -> bool: """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: - print(" āš ļø Skipping directory creation in production mode") + if verbose: + print(" āš ļø Skipping directory creation in production mode") return True for directory in self.required_directories: try: Path(directory).mkdir(parents=True, exist_ok=True) - print(f" āœ… Created: {directory}") + if verbose: + print(f" āœ… Created: {directory}") except Exception as e: - print(f" āŒ Failed to create {directory}: {e}") + if verbose: + print(f" āŒ Failed to create {directory}: {e}") return False - print("āœ… All directories created successfully") + if verbose: + print("āœ… All directories created successfully") return True def setup_environment_variables(self) -> bool: diff --git a/backend/alwrity_utils/router_manager.py b/backend/alwrity_utils/router_manager.py index da1f9c51..3f566e99 100644 --- a/backend/alwrity_utils/router_manager.py +++ b/backend/alwrity_utils/router_manager.py @@ -18,22 +18,31 @@ class RouterManager: def include_router_safely(self, router, router_name: str = None) -> bool: """Include a router safely with error handling.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + try: self.app.include_router(router) router_name = router_name or getattr(router, 'prefix', 'unknown') 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 except Exception as e: router_name = router_name or 'unknown' 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 def include_core_routers(self) -> bool: """Include core application routers.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + try: - logger.info("Including core routers...") + if verbose: + logger.info("Including core routers...") # Component logic router from api.component_logic import router as component_logic_router diff --git a/backend/api/onboarding_utils/onboarding_summary_service.py b/backend/api/onboarding_utils/onboarding_summary_service.py index 04a95ca2..04b8cf7a 100644 --- a/backend/api/onboarding_utils/onboarding_summary_service.py +++ b/backend/api/onboarding_utils/onboarding_summary_service.py @@ -70,129 +70,103 @@ class OnboardingSummaryService: try: db = next(get_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}") - return api_keys + db.close() + + 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: - logger.error(f"Error getting API keys from database: {e}") - return {} + logger.error(f"Error getting API keys: {str(e)}") + 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]]: - """Get website analysis data from database (Step 2).""" + """Get website analysis data from database.""" try: db = next(get_db()) website_data = self.db_service.get_website_analysis(self.user_id, db) - if website_data: - 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}") + db.close() return website_data 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 def _get_research_preferences(self) -> Optional[Dict[str, Any]]: - """Get research preferences data from database (Step 3).""" + """Get research preferences from database.""" try: db = next(get_db()) - research_data = self.db_service.get_research_preferences(self.user_id, db) - if research_data: - logger.info(f"Retrieved research preferences from database for user {self.user_id}") - else: - logger.warning(f"No research preferences found in database for user {self.user_id}") - return research_data + preferences = self.db_service.get_research_preferences(self.user_id, db) + db.close() + return preferences 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 - def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """Get personalization settings from Step 4 (Persona) database.""" - try: - # Try to get from Step 4 (Persona) in database - db = next(get_db()) - persona_data = self.db_service.get_persona_data(self.user_id, db) - - if persona_data: - logger.info(f"Retrieved persona data from database for user {self.user_id}") - # Extract personalization settings from persona data - if 'corePersona' in persona_data: - core_persona = persona_data.get('corePersona', {}) - return { - '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 _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Get personalization settings based on research preferences.""" + if not research_preferences: + return { + "writing_style": "professional", + "target_audience": "general", + "content_focus": "informative" + } + + return { + "writing_style": research_preferences.get('writing_style', 'professional'), + "target_audience": research_preferences.get('target_audience', 'general'), + "content_focus": research_preferences.get('content_focus', 'informative') + } - def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """Check if persona can be generated.""" - try: - persona_service = PersonaAnalysisService() - - # Check if persona can be generated - onboarding_data = persona_service._collect_onboarding_data(self.user_id) - if onboarding_data: - data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data) - return { - "ready": data_sufficiency >= 50.0, - "data_sufficiency": data_sufficiency, - "can_generate": website_analysis is not None - } - return {"ready": False, "data_sufficiency": 0.0, "can_generate": False} - except Exception as e: - logger.warning(f"Could not check persona readiness: {str(e)}") - return {"ready": False, "error": str(e)} + def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Check if persona generation is ready based on available data.""" + if not website_analysis: + return { + "ready": False, + "reason": "Website analysis not completed", + "missing_data": ["website_url", "style_analysis"] + } + + required_fields = ['website_url', 'writing_style', 'target_audience'] + missing_fields = [field for field in required_fields if not website_analysis.get(field)] + + return { + "ready": len(missing_fields) == 0, + "reason": "All required data available" if len(missing_fields) == 0 else f"Missing: {', '.join(missing_fields)}", + "missing_data": missing_fields + } def _determine_capabilities(self, api_keys: Dict[str, Any], website_analysis: Optional[Dict[str, Any]], research_preferences: Optional[Dict[str, Any]], - personalization_settings: Optional[Dict[str, Any]], - persona_readiness: Optional[Dict[str, Any]]) -> Dict[str, bool]: - """Determine user capabilities based on onboarding data.""" - return { - "ai_content": len(api_keys) > 0, - "style_analysis": website_analysis is not None, - "research_tools": research_preferences is not None, - "personalization": personalization_settings is not None, - "persona_generation": persona_readiness.get("ready", False) if persona_readiness else False, - "integrations": False # TODO: Implement + personalization_settings: Dict[str, Any], + persona_readiness: Dict[str, Any]) -> Dict[str, Any]: + """Determine available capabilities based on configured data.""" + capabilities = { + "ai_content_generation": any(key.get("configured") for key in api_keys.values()), + "website_analysis": website_analysis is not None, + "research_capabilities": research_preferences is not None, + "persona_generation": persona_readiness.get("ready", False), + "content_optimization": website_analysis is not None and research_preferences is not None } - - async def get_website_analysis_data(self) -> Optional[Dict[str, Any]]: - """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") + + return capabilities \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index cf649ebc..7246e05c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -19,6 +19,10 @@ from alwrity_utils import HealthChecker, RateLimiter, FrontendServing, RouterMan # Load environment variables load_dotenv() +# Set up clean logging for end users +from logging_config import setup_clean_logging +setup_clean_logging() + # Import middleware from middleware.auth_middleware import get_current_user diff --git a/backend/env_template.txt b/backend/env_template.txt index 59d2e795..3f5e43e9 100644 --- a/backend/env_template.txt +++ b/backend/env_template.txt @@ -1,27 +1,29 @@ -# Clerk Authentication -CLERK_SECRET_KEY=your_clerk_secret_key_here -CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here +# ALwrity Backend Configuration -# Google Search Console -GSC_REDIRECT_URI=your-domain-name/gsc/callback +# API Keys (Configure these in the onboarding process) +# 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) -WIX_CLIENT_ID= -WIX_REDIRECT_URI=your-domain-name/wix/callback +# Research API Keys (Optional) +# TAVILY_API_KEY=your_tavily_api_key_here +# SERPER_API_KEY=your_serper_api_key_here +EXA_API_KEY=your_exa_api_key_here -# WordPress.com OAuth2 Integration -# IMPORTANT: You need to register a WordPress.com application to get valid credentials -# 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= +# Authentication +# CLERK_SECRET_KEY=your_clerk_secret_key_here -# Development Settings -DISABLE_AUTH=false +# OAuth Redirect URIs +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 diff --git a/backend/logging_config.py b/backend/logging_config.py new file mode 100644 index 00000000..906255ae --- /dev/null +++ b/backend/logging_config.py @@ -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" diff --git a/backend/models/user_business_info.py b/backend/models/user_business_info.py index e22084d7..59e2a04a 100644 --- a/backend/models/user_business_info.py +++ b/backend/models/user_business_info.py @@ -6,7 +6,7 @@ from datetime import datetime Base = declarative_base() -logger.info("šŸ”„ Loading UserBusinessInfo model...") +logger.debug("šŸ”„ Loading UserBusinessInfo model...") class UserBusinessInfo(Base): __tablename__ = 'user_business_info' @@ -35,4 +35,4 @@ class UserBusinessInfo(Base): "updated_at": self.updated_at.isoformat() if self.updated_at else None, } -logger.info("āœ… UserBusinessInfo model loaded successfully!") +logger.debug("āœ… UserBusinessInfo model loaded successfully!") diff --git a/backend/scripts/create_billing_tables.py b/backend/scripts/create_billing_tables.py index 60b36726..e0e6401d 100644 --- a/backend/scripts/create_billing_tables.py +++ b/backend/scripts/create_billing_tables.py @@ -26,12 +26,12 @@ def create_billing_tables(): try: # Create engine - engine = create_engine(DATABASE_URL, echo=True) + engine = create_engine(DATABASE_URL, echo=False) # 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) - logger.info("āœ… Billing and subscription tables created successfully") + logger.debug("āœ… Billing and subscription tables created successfully") # Create session for data initialization SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -41,13 +41,13 @@ def create_billing_tables(): # Initialize pricing and plans pricing_service = PricingService(db) - logger.info("Initializing default API pricing...") + logger.debug("Initializing default API 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() - logger.info("āœ… Default subscription plans initialized") + logger.debug("āœ… Default subscription plans initialized") except Exception as e: logger.error(f"Error initializing default data: {e}") @@ -57,7 +57,7 @@ def create_billing_tables(): finally: db.close() - logger.info("šŸŽ‰ Billing system setup completed successfully!") + logger.info("āœ… Billing system setup completed successfully!") # Display summary display_setup_summary(engine) @@ -94,7 +94,7 @@ def display_setup_summary(engine): logger.info(f"\nšŸ“Š Created Tables ({len(tables)}):") for table in tables: - logger.info(f" • {table[0]}") + logger.debug(f" • {table[0]}") # Check subscription plans try: @@ -114,7 +114,7 @@ def display_setup_summary(engine): for plan in plans: 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: logger.warning(f"Could not check subscription plans: {e}") @@ -124,22 +124,22 @@ def display_setup_summary(engine): result = conn.execute(pricing_query) pricing_count = result.fetchone()[0] logger.info(f"\nšŸ’° API Pricing Entries: {pricing_count}") - + if pricing_count > 0: pricing_detail_query = text(""" - SELECT provider, model_name, cost_per_input_token, cost_per_output_token - FROM api_provider_pricing + SELECT provider, model_name, cost_per_input_token, cost_per_output_token + FROM api_provider_pricing WHERE cost_per_input_token > 0 OR cost_per_output_token > 0 ORDER BY provider, model_name LIMIT 10 """) result = conn.execute(pricing_detail_query) pricing_entries = result.fetchall() - + logger.info("\n LLM Pricing (per token) - Top 10:") for entry in pricing_entries: 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: logger.warning(f"Could not check API pricing: {e}") @@ -183,7 +183,7 @@ def check_existing_tables(engine): if 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 True @@ -193,7 +193,7 @@ def check_existing_tables(engine): return True # Proceed anyway if __name__ == "__main__": - logger.info("šŸš€ Starting billing system database migration...") + logger.debug("šŸš€ Starting billing system database migration...") try: # Create engine to check existing tables @@ -201,7 +201,7 @@ if __name__ == "__main__": # Check existing tables 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) # Create tables and initialize data @@ -210,7 +210,7 @@ if __name__ == "__main__": logger.info("āœ… Billing system migration completed successfully!") except KeyboardInterrupt: - logger.info("Migration cancelled by user") + logger.warning("Migration cancelled by user") sys.exit(0) except Exception as e: logger.error(f"āŒ Migration failed: {e}") diff --git a/backend/services/ai_service_manager.py b/backend/services/ai_service_manager.py index a9201b62..4129b800 100644 --- a/backend/services/ai_service_manager.py +++ b/backend/services/ai_service_manager.py @@ -47,15 +47,26 @@ class AIServiceMetrics: class AIServiceManager: """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): - """Initialize AI service manager.""" - self.logger = logger - self.metrics: List[AIServiceMetrics] = [] - self.prompts = self._load_centralized_prompts() - self.schemas = self._load_centralized_schemas() - self.config = self._load_ai_configuration() - - logger.info("AIServiceManager initialized") + """Initialize AI service manager (only once).""" + if not self._initialized: + self.logger = logger + self.metrics: List[AIServiceMetrics] = [] + self.prompts = self._load_centralized_prompts() + self.schemas = self._load_centralized_schemas() + self.config = self._load_ai_configuration() + + logger.debug("AIServiceManager initialized") + self._initialized = True def _load_ai_configuration(self) -> Dict[str, Any]: """Load AI configuration settings.""" diff --git a/backend/services/content_gap_analyzer/ai_engine_service.py b/backend/services/content_gap_analyzer/ai_engine_service.py index 2cd64bbe..4c0f1c26 100644 --- a/backend/services/content_gap_analyzer/ai_engine_service.py +++ b/backend/services/content_gap_analyzer/ai_engine_service.py @@ -24,10 +24,21 @@ from services.database import get_db_session class AIEngineService: """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): - """Initialize the AI engine service.""" - self.ai_service_manager = AIServiceManager() - logger.info("AIEngineService initialized") + """Initialize the AI engine service (only once).""" + if not self._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]: """ diff --git a/backend/services/persona/core_persona/core_persona_service.py b/backend/services/persona/core_persona/core_persona_service.py index 05a555d3..0d191b20 100644 --- a/backend/services/persona/core_persona/core_persona_service.py +++ b/backend/services/persona/core_persona/core_persona_service.py @@ -18,13 +18,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe class CorePersonaService: """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): - """Initialize the core persona service.""" - self.data_collector = OnboardingDataCollector() - self.prompt_builder = PersonaPromptBuilder() - self.linkedin_service = LinkedInPersonaService() - self.facebook_service = FacebookPersonaService() - logger.info("CorePersonaService initialized") + """Initialize the core persona service (only once).""" + if not self._initialized: + self.data_collector = OnboardingDataCollector() + self.prompt_builder = PersonaPromptBuilder() + self.linkedin_service = LinkedInPersonaService() + self.facebook_service = FacebookPersonaService() + logger.debug("CorePersonaService initialized") + self._initialized = True def generate_core_persona(self, onboarding_data: Dict[str, Any]) -> Dict[str, Any]: """Generate core writing persona using Gemini structured response.""" diff --git a/backend/services/persona/enhanced_linguistic_analyzer.py b/backend/services/persona/enhanced_linguistic_analyzer.py index a741c786..94a2d8aa 100644 --- a/backend/services/persona/enhanced_linguistic_analyzer.py +++ b/backend/services/persona/enhanced_linguistic_analyzer.py @@ -26,7 +26,7 @@ class EnhancedLinguisticAnalyzer: import spacy self.nlp = spacy.load("en_core_web_sm") 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: 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 diff --git a/backend/services/persona/facebook/facebook_persona_service.py b/backend/services/persona/facebook/facebook_persona_service.py index 8cf3f85f..624fc687 100644 --- a/backend/services/persona/facebook/facebook_persona_service.py +++ b/backend/services/persona/facebook/facebook_persona_service.py @@ -20,14 +20,25 @@ from services.llm_providers.gemini_provider import gemini_structured_json_respon class FacebookPersonaService: """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): - """Initialize the Facebook persona service.""" - self.schemas = FacebookPersonaSchema - self.constraints = FacebookPersonaConstraints() - self.validation = FacebookPersonaValidation() - self.optimization = FacebookPersonaOptimization() - self.prompts = FacebookPersonaPrompts() - logger.info("FacebookPersonaService initialized") + """Initialize the Facebook persona service (only once).""" + if not self._initialized: + self.schemas = FacebookPersonaSchema + self.constraints = FacebookPersonaConstraints() + self.validation = FacebookPersonaValidation() + self.optimization = FacebookPersonaOptimization() + 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]: """ diff --git a/backend/services/persona/linkedin/linkedin_persona_service.py b/backend/services/persona/linkedin/linkedin_persona_service.py index 212032f1..7deaa55c 100644 --- a/backend/services/persona/linkedin/linkedin_persona_service.py +++ b/backend/services/persona/linkedin/linkedin_persona_service.py @@ -14,11 +14,22 @@ from .linkedin_persona_schemas import LinkedInPersonaSchemas class LinkedInPersonaService: """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): - """Initialize the LinkedIn persona service.""" - self.prompts = LinkedInPersonaPrompts() - self.schemas = LinkedInPersonaSchemas() - logger.info("LinkedInPersonaService initialized") + """Initialize the LinkedIn persona service (only once).""" + if not self._initialized: + self.prompts = LinkedInPersonaPrompts() + 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]: """ diff --git a/backend/services/persona/persona_quality_improver.py b/backend/services/persona/persona_quality_improver.py index 3cfb1c21..f4ca1a37 100644 --- a/backend/services/persona/persona_quality_improver.py +++ b/backend/services/persona/persona_quality_improver.py @@ -24,7 +24,7 @@ class PersonaQualityImprover: def __init__(self): """Initialize the quality improver.""" self.linguistic_analyzer = EnhancedLinguisticAnalyzer() - logger.info("PersonaQualityImprover initialized") + logger.debug("PersonaQualityImprover initialized") def assess_persona_quality_comprehensive( self, diff --git a/backend/services/persona_analysis_service.py b/backend/services/persona_analysis_service.py index bde7c4ac..307b0358 100644 --- a/backend/services/persona_analysis_service.py +++ b/backend/services/persona_analysis_service.py @@ -19,13 +19,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe class PersonaAnalysisService: """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): - """Initialize the persona analysis service.""" - self.core_persona_service = CorePersonaService() - self.data_collector = OnboardingDataCollector() - self.linkedin_service = LinkedInPersonaService() - self.facebook_service = FacebookPersonaService() - logger.info("PersonaAnalysisService initialized") + """Initialize the persona analysis service (only once).""" + if not self._initialized: + self.core_persona_service = CorePersonaService() + self.data_collector = OnboardingDataCollector() + self.linkedin_service = LinkedInPersonaService() + 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]: """ diff --git a/backend/services/pricing_service.py b/backend/services/pricing_service.py index c69a76bc..8da06d76 100644 --- a/backend/services/pricing_service.py +++ b/backend/services/pricing_service.py @@ -215,7 +215,7 @@ class PricingService: self.db.add(pricing) self.db.commit() - logger.info("Default API pricing initialized") + logger.debug("Default API pricing initialized") def initialize_default_plans(self): """Initialize default subscription plans.""" @@ -318,7 +318,7 @@ class PricingService: self.db.add(plan) 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, tokens_input: int = 0, tokens_output: int = 0, diff --git a/backend/start_alwrity_backend.py b/backend/start_alwrity_backend.py index d2e57ee8..1078d0a4 100644 --- a/backend/start_alwrity_backend.py +++ b/backend/start_alwrity_backend.py @@ -17,28 +17,37 @@ def bootstrap_linguistic_models(): This prevents import-time failures when EnhancedLinguisticAnalyzer is loaded. """ 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 try: import spacy try: 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: - print(" āš ļø spaCy model 'en_core_web_sm' not found, downloading...") + if verbose: + print(" āš ļø spaCy model 'en_core_web_sm' not found, downloading...") try: subprocess.check_call([ 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: - print(f" āŒ Failed to download spaCy model: {e}") - print(" Please run: python -m spacy download en_core_web_sm") + if verbose: + print(f" āŒ Failed to download spaCy model: {e}") + print(" Please run: python -m spacy download en_core_web_sm") return False except ImportError: - print(" āš ļø spaCy not installed - skipping") + if verbose: + print(" āš ļø spaCy not installed - skipping") # Check and download NLTK data try: @@ -52,25 +61,32 @@ def bootstrap_linguistic_models(): for data_package, path in essential_data: try: nltk.data.find(path) - print(f" āœ… NLTK {data_package} available") + if verbose: + print(f" āœ… NLTK {data_package} available") except LookupError: - print(f" āš ļø NLTK {data_package} not found, downloading...") + if verbose: + print(f" āš ļø NLTK {data_package} not found, downloading...") try: nltk.download(data_package, quiet=True) - print(f" āœ… NLTK {data_package} downloaded") + if verbose: + print(f" āœ… NLTK {data_package} downloaded") except Exception as e: - print(f" āš ļø Failed to download {data_package}: {e}") + if verbose: + print(f" āš ļø Failed to download {data_package}: {e}") # Try fallback if data_package == 'punkt_tab': try: nltk.download('punkt', quiet=True) - print(f" āœ… NLTK punkt (fallback) downloaded") + if verbose: + print(f" āœ… NLTK punkt (fallback) downloaded") except: pass 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 @@ -127,11 +143,10 @@ def start_backend(enable_reload=False, production_mode=False): import uvicorn # Explicitly initialize database before starting server - print("[DB] Initializing 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(" šŸ” Health Check: http://localhost:8000/health") 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("\n[STOP] Press Ctrl+C to stop the server") - print("=" * 60) - print("\nšŸ’” Usage:") - print(" Production mode: python start_alwrity_backend.py --production") - print(" Development mode: python start_alwrity_backend.py --dev") - print(" With auto-reload: python start_alwrity_backend.py --reload") - print("=" * 60) + print("=" * 50) + + # Set up clean logging for end users + from logging_config import setup_clean_logging, get_uvicorn_log_level + + verbose_mode = setup_clean_logging() + uvicorn_log_level = get_uvicorn_log_level() uvicorn.run( "app:app", @@ -186,7 +202,7 @@ def start_backend(enable_reload=False, production_mode=False): "api/**/*.py", "services/**/*.py" ], - log_level="info" + log_level=uvicorn_log_level ) except KeyboardInterrupt: @@ -205,16 +221,23 @@ def main(): 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("--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() # Determine mode production_mode = args.production 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(f"Mode: {'PRODUCTION' if production_mode else 'DEVELOPMENT'}") print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}") + if verbose_mode: + print("Verbose logging: ENABLED") print("=" * 40) # Check if we're in the right directory @@ -230,39 +253,59 @@ def main(): database_setup = DatabaseSetup(production_mode=production_mode) 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 if production_mode: if not production_optimizer.apply_production_optimizations(): - print("[ERROR] Production optimization failed") + print("āŒ Production optimization failed") 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() if not critical_ok: - print("[ERROR] Critical dependencies missing, installing...") + print("installing...", end=" ", flush=True) if not dependency_manager.install_requirements(): - print("[ERROR] Failed to install dependencies") + print("āŒ Failed") return False + print("āœ… Done") + else: + print("āœ… Done") - # Check optional dependencies (non-critical) - dependency_manager.check_optional_dependencies() + # Check optional dependencies (non-critical) - only in verbose mode + 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(): - print("[ERROR] Directory setup failed") + print("āŒ Directory setup failed") return False if not environment_setup.setup_environment_variables(): - print("[ERROR] Environment variable setup failed") + print("āŒ Environment setup failed") return False # Create .env file only in development if not production_mode: 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(): - print("[WARNING] Database setup had issues, continuing...") + print("āš ļø Issues detected, continuing...") + else: + print("āœ… Done") # Setup advanced features in development, verify in all modes if not production_mode: @@ -274,7 +317,8 @@ def main(): # Note: Linguistic models (spaCy/NLTK) are bootstrapped before imports # 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) diff --git a/frontend/env.production.example b/frontend/env.production.example deleted file mode 100644 index 95ad81e5..00000000 --- a/frontend/env.production.example +++ /dev/null @@ -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 diff --git a/frontend/package.json b/frontend/package.json index cfd8a353..c1f8a9d3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,5 +57,6 @@ "devDependencies": { "typescript": "^4.9.5" }, - "proxy": "http://localhost:8000" + "proxy": "http://localhost:8000", + "homepage": "/" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0e919677..e6f187a9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ 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 { CopilotKit } from "@copilotkit/react-core"; import { ClerkProvider, useAuth } from '@clerk/clerk-react'; @@ -26,6 +26,7 @@ import { SubscriptionProvider } from './contexts/SubscriptionContext'; import { apiClient, setAuthTokenGetter } from './api/client'; import { useOnboarding } from './contexts/OnboardingContext'; import { useState, useEffect } from 'react'; +import ConnectionErrorPage from './components/shared/ConnectionErrorPage'; // interface OnboardingStatus { // onboarding_required: boolean; @@ -37,9 +38,6 @@ import { useState, useEffect } from 'react'; // Conditional CopilotKit wrapper that only shows sidebar on content-planning route 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. return <>{children}; }; @@ -54,6 +52,13 @@ const InitialRouteHandler: React.FC = () => { plan: string; isNewUser: boolean; } | null>(null); + const [connectionError, setConnectionError] = useState<{ + hasError: boolean; + error: Error | null; + }>({ + hasError: false, + error: null, + }); useEffect(() => { const checkSubscription = async () => { @@ -61,18 +66,35 @@ const InitialRouteHandler: React.FC = () => { const userId = localStorage.getItem('user_id') || 'anonymous'; const response = await apiClient.get(`/api/subscription/status/${userId}`); const subscriptionData = response.data.data; - + // Check if user is new (no subscription record at all) const isNewUser = !subscriptionData || subscriptionData.plan === 'none'; - + setSubscriptionStatus({ active: subscriptionData?.active || false, plan: subscriptionData?.plan || 'none', isNewUser }); - } catch (err) { + + // Clear any connection errors + setConnectionError({ + hasError: false, + error: null, + }); + + } catch (err: any) { 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({ active: false, plan: 'none', @@ -86,6 +108,65 @@ const InitialRouteHandler: React.FC = () => { 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 ( + + ); + } + // Loading state - checking both subscription and onboarding if (loading || checkingSubscription) { return ( @@ -200,7 +281,6 @@ const TokenInstaller: React.FC = () => { const App: React.FC = () => { // React Hooks MUST be at the top before any conditionals const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); // Get CopilotKit key from localStorage or .env const [copilotApiKey, setCopilotApiKey] = useState(() => { @@ -208,18 +288,10 @@ const App: React.FC = () => { return savedKey || process.env.REACT_APP_COPILOTKIT_API_KEY || ''; }); + // Initialize app - loading state will be managed by InitialRouteHandler useEffect(() => { - const checkBackendHealth = async () => { - try { - await apiClient.get('/health'); - setLoading(false); - } catch (err) { - setError('Backend service is not available. Please check if the server is running.'); - setLoading(false); - } - }; - - checkBackendHealth(); + // Remove manual health check - connection errors are handled by ErrorBoundary + setLoading(false); }, []); // Listen for CopilotKit key updates @@ -257,29 +329,6 @@ const App: React.FC = () => { ); } - if (error) { - return ( - - - Connection Error - - - {error} - - - Please ensure the backend server is running and try refreshing the page. - - - ); - } // Get environment variables with fallbacks const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || ''; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 0851950c..ee446695 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -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 apiClient.interceptors.response.use( (response) => { @@ -79,11 +94,30 @@ apiClient.interceptors.response.use( }, async (error) => { 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 (error?.response?.status === 401 && !originalRequest._retry && authTokenGetter) { originalRequest._retry = true; - + try { // Get fresh token const newToken = await authTokenGetter(); @@ -96,9 +130,9 @@ apiClient.interceptors.response.use( } catch (retryError) { console.error('Token refresh failed:', retryError); } - + // 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 === '/'; if (!isOnboardingRoute) { try { window.location.assign('/'); } catch {} @@ -106,7 +140,7 @@ apiClient.interceptors.response.use( console.warn('401 Unauthorized - token refresh failed'); } } - + console.error('API Error:', error.response?.status, error.response?.data); return Promise.reject(error); } diff --git a/frontend/src/components/shared/BackendConnectionError.tsx b/frontend/src/components/shared/BackendConnectionError.tsx new file mode 100644 index 00000000..40243a6a --- /dev/null +++ b/frontend/src/components/shared/BackendConnectionError.tsx @@ -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 = ({ + 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([]); + 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 ( + + + + + {/* Error Icon */} + + + + + {/* Error Title */} + + Connection Error + + + {/* Error Message */} + + {error} + + + {/* Troubleshooting Tips */} + + + + + Troubleshooting Steps + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Action Buttons */} + + + + + + + {/* Connection Attempts Summary */} + {connectionAttempts.length > 0 && ( + + + Connection attempts: {connectionAttempts.length} + + + Last attempt: {connectionAttempts[connectionAttempts.length - 1]?.success ? 'Successful' : 'Failed'} + + + )} + + + + + ); + } + + // Show loading state for first 2 minutes + return ( + + + + + {/* Loading Animation */} + + + + + + + + {/* Loading Title */} + + Connecting to Backend + + + {/* Progress Bar */} + + + + + {/* Time and Progress Info */} + + + Attempting to reconnect... {timeString} + + + + + {/* Motivational Messages */} + + } + sx={{ + maxWidth: 500, + textAlign: 'center', + '& .MuiAlert-message': { + width: '100%', + }, + }} + > + + {timeElapsed < 60 + ? "We're working to restore your connection..." + : "Still trying to connect. This may take a moment..." + } + + + + + {/* Connection Attempts */} + {connectionAttempts.length > 0 && ( + + + Connection attempts: {connectionAttempts.length} + + + {connectionAttempts.filter(attempt => attempt.success).length} successful,{' '} + {connectionAttempts.filter(attempt => !attempt.success).length} failed + + + )} + + {/* Action Buttons */} + + + + + + + {/* Help Text */} + + If this issue persists, please check your internet connection and ensure the backend server is running. + + + + + + ); +}; + +export default BackendConnectionError; diff --git a/frontend/src/components/shared/ConnectionErrorPage.tsx b/frontend/src/components/shared/ConnectionErrorPage.tsx new file mode 100644 index 00000000..234b76a4 --- /dev/null +++ b/frontend/src/components/shared/ConnectionErrorPage.tsx @@ -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 = ({ + 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 ( + + + + + + {/* Animated background elements */} + 0 + ? 'linear-gradient(90deg, #667eea, #764ba2)' + : 'linear-gradient(90deg, #f44336, #e91e63)', + }} + /> + + + {/* Animated Icon */} + 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 ? ( + + ) : ( + + )} + + + {/* Title and Status */} + + 0 ? '#1a1a1a' : '#d32f2f', + mb: 1, + }} + > + {title} + + + {countdown > 0 ? ( + + + Attempting to reconnect... + + ) : ( + + + Connection failed + + )} + + + {/* Countdown Timer */} + {showProgress && countdown > 0 && ( + + + + Retrying connection... + + + {formatTime(countdown)} + + + + + )} + + {/* Main Message */} + 0 ? "info" : "error"} + icon={countdown > 0 ? : } + sx={{ + width: '100%', + maxWidth: 600, + textAlign: 'left', + '& .MuiAlert-message': { + width: '100%', + }, + }} + > + + {message} + + + {countdown > 0 ? ( + + Please ensure the backend server is running and try refreshing the page. + We'll keep trying to connect automatically. + + ) : ( + + The backend server appears to be unavailable. Please check if it's running + and try again, or contact support if the issue persists. + + )} + + + {/* Action Buttons */} + + {showRetry && ( + + )} + + + + + {/* Help Text */} + + {countdown > 0 + ? `Automatic retry in ${formatTime(countdown)}. Check your terminal for server status.` + : "Error ID: connection_" + Date.now().toString(36) + " • Timestamp: " + new Date().toLocaleString() + } + + + + + + + + ); +}; + +export default ConnectionErrorPage; diff --git a/frontend/src/components/shared/ErrorBoundary.tsx b/frontend/src/components/shared/ErrorBoundary.tsx index 53b89d4b..cdb6c66c 100644 --- a/frontend/src/components/shared/ErrorBoundary.tsx +++ b/frontend/src/components/shared/ErrorBoundary.tsx @@ -136,13 +136,16 @@ class ErrorBoundary extends Component { render() { if (this.state.hasError) { + const { error } = this.state; + + // Custom fallback UI provided if (this.props.fallback) { return this.props.fallback; } // Default fallback UI - const { error, errorInfo, showDetails } = this.state; + const { errorInfo, showDetails } = this.state; const { context, showDetails: showDetailsDefault } = this.props; return ( diff --git a/frontend/src/contexts/OnboardingContext.tsx b/frontend/src/contexts/OnboardingContext.tsx index 62acceb7..3d5b7ca0 100644 --- a/frontend/src/contexts/OnboardingContext.tsx +++ b/frontend/src/contexts/OnboardingContext.tsx @@ -121,6 +121,13 @@ export const OnboardingProvider: React.FC = ({ children setLoading(false); } catch (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'); setLoading(false); } diff --git a/frontend/src/contexts/SubscriptionContext.tsx b/frontend/src/contexts/SubscriptionContext.tsx index 5902a254..fd5b157c 100644 --- a/frontend/src/contexts/SubscriptionContext.tsx +++ b/frontend/src/contexts/SubscriptionContext.tsx @@ -64,6 +64,13 @@ export const SubscriptionProvider: React.FC = ({ chil setSubscription(subscriptionData); } catch (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'); // Default to free tier on error diff --git a/frontend/test_modal_integration.html b/frontend/test_modal_integration.html deleted file mode 100644 index b3adf80e..00000000 --- a/frontend/test_modal_integration.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - Modal Integration Test - - - -
-

Calendar Generation Modal Integration Test

-

This test verifies that the calendar generation modal can be triggered and displays properly.

- -

Test Steps:

-
    -
  1. Click the "Test Modal Integration" button below
  2. -
  3. Check if the modal opens and displays progress
  4. -
  5. Monitor the logs for any errors
  6. -
- - - - - -
-
Ready to test modal integration...
-
-
- - - -