diff --git a/.github/README.md b/.github/README.md index 1fcc8fcc..8a547d93 100644 --- a/.github/README.md +++ b/.github/README.md @@ -29,10 +29,59 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu - **šŸŒ Multi-Modal Content Creation**: Text, images, audio, and video content generation - **šŸ“Š Data-Driven Insights**: Web research, competitor analysis, and predictive analytics - **šŸ¤– AI Agent Teams**: Specialized AI agents for different marketing tasks -- **šŸ”— Platform Integration**: Direct publishing to WordPress, social media, and more +- **šŸ”— Platform Integration**: Direct publishing to WordPress, Wix, Google Search Console, and more - **šŸ“ˆ Performance Optimization**: Continuous learning and strategy refinement - **šŸŽÆ Solopreneur-Focused**: Designed specifically for independent entrepreneurs - **šŸ›”ļø Enterprise Security**: JWT authentication, rate limiting, and comprehensive monitoring +- **✨ Intelligent Onboarding**: AI-powered setup process that analyzes your business and generates personalized strategies + +### šŸš€ **NEW: Complete Onboarding & Integration System** + +**Transform Your Digital Presence in 5 Simple Steps:** + +1. **šŸ“§ Email Setup & Business Analysis** - AI analyzes your business domain and industry +2. **šŸŽ­ AI Persona Generation** - Creates detailed buyer personas and audience insights +3. **šŸ¢ Business Information Collection** - Gathers comprehensive business data for personalized strategies +4. **šŸ” Competitor Analysis** - Real-time competitor research and market positioning +5. **šŸ”— Platform Integrations** - Connect WordPress, Wix, Google Search Console with OAuth security + +**šŸŽÆ User Impact**: Go from zero to fully optimized digital presence in under 15 minutes! + +--- + +## šŸš€ Getting Started (Live Now!) + +### **⚔ Quick Start - 3 Steps to Success** + +**1. Clone & Setup (2 minutes)** +```bash +git clone https://github.com/AJaySi/AI-Writer.git +cd AI-Writer/backend && pip install -r requirements.txt +cd ../frontend && npm install +``` + +**2. Launch Platform (1 minute)** +```bash +# Terminal 1: Backend +cd backend && python start_alwrity_backend.py + +# Terminal 2: Frontend +cd frontend && npm start +``` + +**3. Access & Create (Instant)** +- **Frontend**: http://localhost:3000 +- **API Docs**: http://localhost:8000/api/docs +- **Complete onboarding** → **Generate content** → **Publish everywhere** + +### **šŸŽÆ What You'll Get Immediately:** +- āœ… **AI-powered business analysis** and strategy generation +- āœ… **LinkedIn content creation** with fact-checking and Google grounding +- āœ… **Blog writing** with research, SEO optimization, and metadata +- āœ… **Facebook content generation** with platform-specific optimization +- āœ… **WordPress & Wix integration** with OAuth security +- āœ… **Google Search Console** analytics and insights +- āœ… **Competitor analysis** and market intelligence --- @@ -67,6 +116,9 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu - **Google Search Console Integration**: OAuth2 authentication and real-time analytics - **Hallucination Detection**: AI-powered fact-checking and content verification - **Persona System**: Advanced writing persona generation and management +- **Google Grounding**: Real-time fact verification using Google Search API +- **Exa AI Integration**: Advanced semantic search and content discovery +- **Assistive Writing**: Real-time writing suggestions and optimization ### **āœ… Frontend Development - COMPLETE** - **React Application**: Modern TypeScript-based frontend with Material-UI @@ -75,7 +127,9 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu - **Real-time Updates**: Live progress tracking and notifications - **Blog Writer Interface**: Complete WYSIWYG editor with research integration - **SEO Dashboard**: Comprehensive SEO analysis and metadata generation tools -- **Onboarding System**: Multi-step guided setup with business information collection +- **Complete Onboarding System**: 5-step AI-powered setup with business analysis +- **Platform Integrations**: WordPress, Wix, Google Search Console with OAuth +- **Coming Soon Section**: Interactive preview of upcoming features ### **šŸ“… Launch Timeline** - **Current**: Full platform operational with all core features @@ -100,11 +154,39 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu | **Platform** | **Content Types** | **Status** | |--------------|------------------|------------| -| **LinkedIn** | Posts, Articles, Carousels, Video Scripts, Comments, Fact-Checking | āœ… Complete | +| **LinkedIn** | Posts, Articles, Carousels, Video Scripts, Comments, Fact-Checking, Google Grounding | āœ… Complete | | **Facebook** | Posts, Stories, Ads, Community Content | āœ… Complete | -| **Blog Writer** | Research, Outline, Content Generation, SEO Analysis, Metadata | āœ… Complete | +| **Blog Writer** | Research, Outline, Content Generation, SEO Analysis, Metadata, Exa AI Integration | āœ… Complete | | **SEO Content** | Blog posts, landing pages, technical content | āœ… Complete | | **General Content** | Long-form articles, social media posts | āœ… Complete | +| **Assistive Writing** | Real-time suggestions, grammar checking, tone optimization | āœ… Complete | + +### šŸ” **Advanced Research & Fact-Checking** + +| **Feature** | **AI Capabilities** | **Status** | +|-------------|-------------------|------------| +| **Google Grounding** | Real-time fact verification using Google Search | āœ… Complete | +| **Exa AI Integration** | Semantic search and content discovery | āœ… Complete | +| **Fact-Checking Engine** | AI-powered content verification and source validation | āœ… Complete | +| **Web Research** | Automated competitor analysis and market intelligence | āœ… Complete | +| **Citation Management** | Automatic source tracking and verification | āœ… Complete | + +### šŸš€ **Complete Onboarding System** + +| **Step** | **AI Capabilities** | **User Impact** | +|----------|-------------------|-----------------| +| **šŸ“§ Email & Business Analysis** | AI analyzes your domain, industry, and business model | **Instant business insights** and personalized recommendations | +| **šŸŽ­ AI Persona Generation** | Creates detailed buyer personas with demographic and psychographic data | **Target the right audience** with precision marketing strategies | +| **šŸ¢ Business Information** | Collects comprehensive business data for strategy personalization | **Customized content strategies** that align with your business goals | +| **šŸ” Competitor Analysis** | Real-time competitor research using Exa AI and web scraping | **Stay ahead of competition** with data-driven market positioning | +| **šŸ”— Platform Integrations** | OAuth-secured connections to WordPress, Wix, Google Search Console | **Publish everywhere** with one-click integration and real-time analytics | + +**šŸŽÆ User Benefits:** +- **15-minute setup** from zero to fully optimized digital presence +- **Professional marketing strategy** without hiring agencies +- **Automated competitor intelligence** for strategic advantage +- **One-click publishing** across all major platforms +- **Real-time performance tracking** with actionable insights ### šŸ” **Advanced SEO & Technical Optimization** @@ -117,6 +199,23 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu | **SEO Metadata** | Automated title, description, Open Graph, Twitter Cards | āœ… Complete | | **Google Search Console** | OAuth2 integration, real-time analytics, sitemap analysis | āœ… Complete | +### šŸ”— **Platform Integrations & Publishing** + +| **Integration** | **Features** | **User Impact** | +|-----------------|-------------|-----------------| +| **WordPress OAuth** | Direct publishing, media management, category/tag sync | **One-click publishing** to WordPress sites with full content optimization | +| **Wix Integration** | Blog post creation, media upload, SEO optimization | **Seamless Wix publishing** with automatic SEO metadata generation | +| **Google Search Console** | Real-time analytics, search performance, keyword tracking | **Data-driven optimization** with actual search performance insights | +| **LinkedIn Publishing** | Direct post creation, article publishing, engagement tracking | **Professional content** published directly to LinkedIn with analytics | +| **Facebook Integration** | Post scheduling, media upload, audience targeting | **Social media automation** with platform-specific optimization | + +**šŸŽÆ Integration Benefits:** +- **OAuth security** - No password sharing, enterprise-grade authentication +- **Real-time analytics** - Performance tracking across all platforms +- **Automated SEO** - Every published piece optimized for search engines +- **Content synchronization** - Consistent branding and messaging across platforms +- **Performance insights** - Data-driven content optimization recommendations + ### šŸ–¼ļø **AI Image Generation** | **Feature** | **Capabilities** | **Status** | @@ -136,6 +235,23 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu | **Automated Optimization** | Continuous strategy refinement | āœ… Complete | | **Usage Tracking** | Comprehensive API usage and billing analytics | āœ… Complete | +### šŸš€ **Coming Soon Features** + +| **Feature** | **Status** | **Expected Impact** | +|-------------|------------|-------------------| +| **Social Media OAuth** | šŸ”„ Awaiting Platform Approval | **Automated LinkedIn & Facebook posting** with advanced scheduling | +| **Instagram Integration** | šŸ“… Planned | **Story creation, hashtag optimization, and visual content** | +| **Advanced WordPress Features** | šŸ”„ In Development | **Media library management, advanced SEO tools, auto-publishing** | +| **Mobile Application** | šŸ“… Q2 2025 | **Content creation and management on-the-go** | +| **AI Agent Marketplace** | šŸ“… Q3 2025 | **Specialized AI agents for specific marketing tasks** | +| **Enterprise White-Label** | šŸ“… Q3 2025 | **Customizable platform for agencies and enterprises** | + +**šŸŽÆ Future Benefits:** +- **Complete social media automation** across all major platforms +- **Mobile-first content creation** for busy entrepreneurs +- **AI agent ecosystem** for specialized marketing tasks +- **Enterprise-grade customization** for agencies and large teams + ### šŸ›”ļø **Enterprise Features** | **Feature** | **Capabilities** | **Status** | @@ -195,34 +311,27 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu --- -## šŸš€ Getting Started (Right Now!) +## šŸŽÆ **Start Creating Content Now** -### **1. Backend Setup** -```bash -cd backend -pip install -r requirements.txt -python start_alwrity_backend.py -``` +### **Complete Onboarding Process** +1. **šŸ“§ Email Setup** - Enter your business email for AI analysis +2. **šŸŽ­ Persona Generation** - AI creates detailed buyer personas +3. **šŸ¢ Business Info** - Provide comprehensive business details +4. **šŸ” Competitor Analysis** - AI researches your competition +5. **šŸ”— Platform Integration** - Connect WordPress, Wix, GSC -### **2. Frontend Setup** -```bash -cd frontend -npm install -npm start -``` +### **Immediate Content Creation** +- āœ… **LinkedIn Posts** with fact-checking and Google grounding +- āœ… **Blog Writing** with research and SEO optimization +- āœ… **Facebook Content** with platform-specific optimization +- āœ… **SEO Analysis** with metadata generation +- āœ… **Image Generation** with AI-powered visuals -### **3. Access the Platform** -- **Backend API**: http://localhost:8000/api/docs -- **Frontend**: http://localhost:3000 -- **Health Check**: http://localhost:8000/health - -### **4. Start Creating Content** -- Complete the AI-powered onboarding process -- Generate your personalized content strategy -- Create content across all platforms -- Track performance with real-time analytics -- Use the Blog Writer for complete blog creation -- Leverage SEO analysis and metadata generation +### **Publishing & Analytics** +- **One-click publishing** to WordPress and Wix +- **Real-time analytics** from Google Search Console +- **Performance tracking** across all platforms +- **Data-driven optimization** recommendations --- diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 00000000..8dfd66ca --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,293 @@ +# šŸš€ ALwrity Deployment Guide + +## Vercel Deployment Strategy + +Since Vercel is optimized for frontend applications, we'll deploy the frontend on Vercel and the backend on a separate platform. + +### **Architecture Overview** +``` +Frontend (Vercel) ←→ Backend (Railway/Render) ←→ Database (SQLite/PostgreSQL) +``` + +--- + +## šŸŽÆ **Step-by-Step Deployment** + +### **Part 1: Deploy Backend (Railway - Recommended)** + +#### **Option A: Railway (Recommended)** + +1. **Install Railway CLI:** + ```bash + npm install -g @railway/cli + ``` + +2. **Login to Railway:** + ```bash + railway login + ``` + +3. **Initialize Project:** + ```bash + cd backend + railway init + ``` + +4. **Add Environment Variables:** + ```bash + railway variables set GEMINI_API_KEY=your_gemini_key + railway variables set OPENAI_API_KEY=your_openai_key + railway variables set ANTHROPIC_API_KEY=your_anthropic_key + railway variables set MISTRAL_API_KEY=your_mistral_key + railway variables set TAVILY_API_KEY=your_tavily_key + railway variables set EXA_API_KEY=your_exa_key + railway variables set SERPER_API_KEY=your_serper_key + railway variables set CLERK_SECRET_KEY=your_clerk_secret + railway variables set GSC_REDIRECT_URI=https://your-frontend.vercel.app/gsc/callback + railway variables set WORDPRESS_REDIRECT_URI=https://your-frontend.vercel.app/wp/callback + railway variables set WIX_REDIRECT_URI=https://your-frontend.vercel.app/wix/callback + ``` + +5. **Deploy:** + ```bash + railway up + ``` + +6. **Get Backend URL:** + ```bash + railway domain + ``` + Copy the URL (e.g., `https://your-app.railway.app`) + +#### **Option B: Render** + +1. Go to [render.com](https://render.com) and connect your GitHub +2. Create a new "Web Service" +3. Select your repository +4. Configure: + - **Build Command:** `pip install -r requirements.txt` + - **Start Command:** `python start_alwrity_backend.py` + - **Environment:** Python 3 +5. Add all environment variables in the dashboard +6. Deploy and get your backend URL + +--- + +### **Part 2: Deploy Frontend (Vercel)** + +#### **1. Prepare Frontend for Production** + +Create a `.env.production` file in the `frontend` directory: +```env +REACT_APP_API_URL=https://your-backend-url.railway.app +REACT_APP_ENVIRONMENT=production +``` + +#### **2. Deploy to Vercel** + +1. **Connect to Vercel:** + - Go to [vercel.com](https://vercel.com) + - Click "New Project" + - Import your GitHub repository + +2. **Configure Project:** + - **Framework Preset:** Create React App + - **Root Directory:** `frontend` + - **Build Command:** `npm run build` + - **Output Directory:** `build` + +3. **Add Environment Variables:** + ``` + REACT_APP_API_URL = https://your-backend-url.railway.app + REACT_APP_ENVIRONMENT = production + ``` + +4. **Deploy:** + - Click "Deploy" + - Wait for deployment to complete + - Get your frontend URL (e.g., `https://your-app.vercel.app`) + +#### **3. Update Backend URLs** + +After getting your frontend URL, update the backend environment variables: + +```bash +# Update redirect URIs with your actual Vercel URL +railway variables set GSC_REDIRECT_URI=https://your-app.vercel.app/gsc/callback +railway variables set WORDPRESS_REDIRECT_URI=https://your-app.vercel.app/wp/callback +railway variables set WIX_REDIRECT_URI=https://your-app.vercel.app/wix/callback +``` + +--- + +## šŸ”§ **Alternative: Full-Stack on Vercel (Advanced)** + +If you want to deploy everything on Vercel, you can use Vercel's serverless functions: + +### **1. Create API Routes** + +Create `api/` directory in your project root: + +``` +api/ +ā”œā”€ā”€ auth/ +│ └── [...auth].ts +ā”œā”€ā”€ content/ +│ └── generate.ts +└── seo/ + └── analyze.ts +``` + +### **2. Convert FastAPI to Vercel Functions** + +This requires significant refactoring of your backend code to work with Vercel's serverless functions. + +### **3. Deploy as Monorepo** + +```bash +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel --prod +``` + +--- + +## šŸ› ļø **Environment Variables Checklist** + +### **Backend Environment Variables:** +```bash +# AI API Keys +GEMINI_API_KEY=your_gemini_key +OPENAI_API_KEY=your_openai_key +ANTHROPIC_API_KEY=your_anthropic_key +MISTRAL_API_KEY=your_mistral_key + +# Research APIs +TAVILY_API_KEY=your_tavily_key +EXA_API_KEY=your_exa_key +SERPER_API_KEY=your_serper_key + +# Authentication +CLERK_SECRET_KEY=your_clerk_secret + +# OAuth Redirects (update with your Vercel URL) +GSC_REDIRECT_URI=https://your-app.vercel.app/gsc/callback +WORDPRESS_REDIRECT_URI=https://your-app.vercel.app/wp/callback +WIX_REDIRECT_URI=https://your-app.vercel.app/wix/callback +``` + +### **Frontend Environment Variables:** +```bash +REACT_APP_API_URL=https://your-backend-url.railway.app +REACT_APP_ENVIRONMENT=production +``` + +--- + +## šŸš€ **Post-Deployment Steps** + +### **1. Test Your Deployment** +- Visit your Vercel URL +- Complete the onboarding process +- Test content generation +- Verify OAuth integrations + +### **2. Configure Custom Domain (Optional)** +- In Vercel dashboard, go to your project +- Click "Domains" +- Add your custom domain +- Update redirect URIs with custom domain + +### **3. Monitor Performance** +- Check Vercel analytics +- Monitor Railway/Render logs +- Set up error tracking (Sentry, etc.) + +--- + +## šŸ” **Troubleshooting** + +### **Common Issues:** + +1. **CORS Errors:** + - Add your Vercel domain to backend CORS settings + - Update `allowed_origins` in FastAPI CORS middleware + +2. **Environment Variables:** + - Ensure all variables are set in both platforms + - Check variable names match exactly + +3. **OAuth Redirects:** + - Update all redirect URIs with production URLs + - Test OAuth flows end-to-end + +4. **Database Issues:** + - Consider upgrading to PostgreSQL for production + - Set up database backups + +### **Performance Optimization:** + +1. **Frontend:** + - Enable Vercel's edge caching + - Optimize bundle size + - Use CDN for static assets + +2. **Backend:** + - Implement connection pooling + - Add caching layers + - Monitor memory usage + +--- + +## šŸ“Š **Monitoring & Maintenance** + +### **Health Checks:** +- Frontend: `https://your-app.vercel.app/health` +- Backend: `https://your-backend.railway.app/health` + +### **Logs:** +- Vercel: Built-in function logs +- Railway: `railway logs` +- Render: Dashboard logs section + +### **Scaling:** +- Vercel: Automatic scaling +- Railway: Manual scaling in dashboard +- Render: Auto-scaling available + +--- + +## šŸŽÆ **Recommended Architecture** + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Frontend │ │ Backend │ │ Database │ +│ (Vercel) │◄──►│ (Railway) │◄──►│ (PostgreSQL) │ +│ │ │ │ │ │ +│ • React App │ │ • FastAPI │ │ • User Data │ +│ • Static Files │ │ • AI Services │ │ • Content │ +│ • CDN Cached │ │ • OAuth APIs │ │ • Analytics │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +This setup provides: +- āœ… **Fast frontend delivery** via Vercel's global CDN +- āœ… **Scalable backend** with Railway's infrastructure +- āœ… **Reliable database** with PostgreSQL +- āœ… **Easy maintenance** with separate concerns +- āœ… **Cost-effective** scaling based on usage + +--- + +## šŸš€ **Next Steps** + +1. **Deploy backend** to Railway/Render +2. **Deploy frontend** to Vercel +3. **Configure environment variables** +4. **Test all integrations** +5. **Set up monitoring** +6. **Configure custom domain** (optional) + +Your ALwrity platform will be live and accessible worldwide! šŸŒ diff --git a/backend/alwrity_utils/__init__.py b/backend/alwrity_utils/__init__.py new file mode 100644 index 00000000..9edc5227 --- /dev/null +++ b/backend/alwrity_utils/__init__.py @@ -0,0 +1,26 @@ +""" +ALwrity Utilities Package +Modular utilities for ALwrity backend startup and configuration. +""" + +from .dependency_manager import DependencyManager +from .environment_setup import EnvironmentSetup +from .database_setup import DatabaseSetup +from .production_optimizer import ProductionOptimizer +from .health_checker import HealthChecker +from .rate_limiter import RateLimiter +from .frontend_serving import FrontendServing +from .router_manager import RouterManager +from .onboarding_manager import OnboardingManager + +__all__ = [ + 'DependencyManager', + 'EnvironmentSetup', + 'DatabaseSetup', + 'ProductionOptimizer', + 'HealthChecker', + 'RateLimiter', + 'FrontendServing', + 'RouterManager', + 'OnboardingManager' +] diff --git a/backend/alwrity_utils/database_setup.py b/backend/alwrity_utils/database_setup.py new file mode 100644 index 00000000..9981b878 --- /dev/null +++ b/backend/alwrity_utils/database_setup.py @@ -0,0 +1,172 @@ +""" +Database Setup Module +Handles database initialization and table creation. +""" + +from typing import List, Tuple +import sys +from pathlib import Path + + +class DatabaseSetup: + """Manages database setup for ALwrity backend.""" + + def __init__(self, production_mode: bool = False): + self.production_mode = production_mode + + def setup_essential_tables(self) -> bool: + """Set up essential database tables.""" + print("šŸ“Š Setting up essential database tables...") + + try: + from services.database import init_database, engine + + # Initialize database connection + init_database() + print(" āœ… Database connection initialized") + + # Create essential tables + self._create_monitoring_tables() + self._create_subscription_tables() + self._create_persona_tables() + + 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 + + def _create_monitoring_tables(self) -> bool: + """Create API monitoring tables.""" + try: + from models.api_monitoring import Base as MonitoringBase + MonitoringBase.metadata.create_all(bind=engine) + print(" āœ… Monitoring tables created") + return True + except Exception as e: + print(f" āš ļø Monitoring tables failed: {e}") + return True # Non-critical + + def _create_subscription_tables(self) -> bool: + """Create subscription and billing tables.""" + try: + from models.subscription_models import Base as SubscriptionBase + SubscriptionBase.metadata.create_all(bind=engine) + print(" āœ… Subscription tables created") + return True + except Exception as e: + print(f" āš ļø Subscription tables failed: {e}") + return True # Non-critical + + def _create_persona_tables(self) -> bool: + """Create persona analysis tables.""" + try: + from models.persona_models import Base as PersonaBase + PersonaBase.metadata.create_all(bind=engine) + print(" āœ… Persona tables created") + return True + except Exception as e: + print(f" āš ļø Persona tables failed: {e}") + return True # Non-critical + + def verify_tables(self) -> bool: + """Verify that essential tables exist.""" + if self.production_mode: + print("āš ļø Skipping table verification in production mode") + return True + + print("šŸ” Verifying database tables...") + + try: + from services.database import engine + from sqlalchemy import inspect + + inspector = inspect(engine) + tables = inspector.get_table_names() + + essential_tables = [ + 'api_monitoring_logs', + 'subscription_plans', + 'user_subscriptions' + ] + + existing_tables = [table for table in essential_tables if table in tables] + 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}") + + return True + + except Exception as e: + print(f" āš ļø Table verification failed: {e}") + return True # Non-critical + + def setup_advanced_tables(self) -> bool: + """Set up advanced tables (non-critical).""" + if self.production_mode: + print("āš ļø Skipping advanced table setup in production mode") + return True + + print("šŸ”§ Setting up advanced database features...") + + try: + # Set up monitoring tables + self._setup_monitoring_tables() + + # Set up billing tables + self._setup_billing_tables() + + print("āœ… Advanced database features configured") + return True + + except Exception as e: + print(f"āš ļø Advanced table setup failed: {e}") + return True # Non-critical + + def _setup_monitoring_tables(self) -> bool: + """Set up API monitoring tables.""" + try: + sys.path.append(str(Path(__file__).parent.parent)) + from scripts.create_monitoring_tables import create_monitoring_tables + + if create_monitoring_tables(): + print(" āœ… API monitoring tables created") + return True + else: + print(" āš ļø API monitoring setup failed") + return True # Non-critical + + except Exception as e: + print(f" āš ļø Monitoring setup failed: {e}") + return True # Non-critical + + def _setup_billing_tables(self) -> bool: + """Set up billing and subscription tables.""" + try: + sys.path.append(str(Path(__file__).parent.parent)) + from scripts.create_billing_tables import create_billing_tables, check_existing_tables + from services.database import engine + + # Check if tables already exist + if check_existing_tables(engine): + print(" āœ… Billing tables already exist") + return True + + if create_billing_tables(): + print(" āœ… Billing tables created") + return True + else: + print(" āš ļø Billing setup failed") + return True # Non-critical + + except Exception as e: + print(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 new file mode 100644 index 00000000..26c615a8 --- /dev/null +++ b/backend/alwrity_utils/dependency_manager.py @@ -0,0 +1,150 @@ +""" +Dependency Management Module +Handles installation and verification of Python dependencies. +""" + +import sys +import subprocess +from pathlib import Path +from typing import List, Tuple + + +class DependencyManager: + """Manages Python package dependencies for ALwrity backend.""" + + def __init__(self, requirements_file: str = "requirements.txt"): + self.requirements_file = Path(requirements_file) + self.critical_packages = [ + 'fastapi', + 'uvicorn', + 'pydantic', + 'sqlalchemy', + 'loguru' + ] + + self.optional_packages = [ + 'openai', + 'google.generativeai', + 'anthropic', + 'mistralai', + 'spacy', + 'nltk' + ] + + def install_requirements(self) -> bool: + """Install packages from requirements.txt.""" + print("šŸ“¦ Installing required packages...") + + if not self.requirements_file.exists(): + print(f"āŒ Requirements file not found: {self.requirements_file}") + return False + + try: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "-r", str(self.requirements_file) + ]) + print("āœ… All packages installed successfully!") + return True + except subprocess.CalledProcessError as e: + print(f"āŒ Error installing packages: {e}") + return False + + def check_critical_dependencies(self) -> Tuple[bool, List[str]]: + """Check if critical dependencies are available.""" + print("šŸ” Checking critical dependencies...") + + missing_packages = [] + + for package in self.critical_packages: + try: + __import__(package.replace('-', '_')) + print(f" āœ… {package}") + except ImportError: + print(f" āŒ {package} - MISSING") + missing_packages.append(package) + + if missing_packages: + print(f"āŒ Missing critical packages: {', '.join(missing_packages)}") + return False, missing_packages + + 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...") + + missing_packages = [] + + for package in self.optional_packages: + try: + __import__(package.replace('-', '_')) + print(f" āœ… {package}") + except ImportError: + print(f" āš ļø {package} - MISSING (optional)") + missing_packages.append(package) + + if missing_packages: + print(f"āš ļø Missing optional packages: {', '.join(missing_packages)}") + print(" Some features may not be available") + + return len(missing_packages) == 0, missing_packages + + def setup_spacy_model(self) -> bool: + """Set up spaCy English model (production-optimized).""" + print("🧠 Setting up spaCy model...") + + try: + import spacy + + model_name = "en_core_web_sm" + + try: + # Try to load the model + nlp = spacy.load(model_name) + test_doc = nlp("This is a test sentence.") + if test_doc and len(test_doc) > 0: + print(f"āœ… spaCy model '{model_name}' is available") + return True + except OSError: + print(f"āš ļø spaCy model '{model_name}' not found") + print(" Skipping spaCy setup in production mode") + return True # Don't fail for missing spaCy in production + + except ImportError: + print("āš ļø spaCy not installed - skipping model setup") + return True # Don't fail for missing spaCy + + return True + + def setup_nltk_data(self) -> bool: + """Set up NLTK data (production-optimized).""" + print("šŸ“š Setting up NLTK data...") + + try: + import nltk + + # Only download essential data + essential_data = ['punkt', 'stopwords'] + + for data_package in essential_data: + try: + nltk.data.find(f'tokenizers/{data_package}' if data_package == 'punkt' + else f'corpora/{data_package}') + print(f" āœ… {data_package}") + except LookupError: + print(f" āš ļø {data_package} - downloading...") + try: + nltk.download(data_package, quiet=True) + print(f" āœ… {data_package} downloaded") + except Exception as e: + print(f" āš ļø {data_package} download failed: {e}") + + print("āœ… NLTK data setup complete") + return True + + except ImportError: + print("āš ļø NLTK not installed - skipping data setup") + return True # Don't fail for missing NLTK + + return True diff --git a/backend/alwrity_utils/environment_setup.py b/backend/alwrity_utils/environment_setup.py new file mode 100644 index 00000000..67c49758 --- /dev/null +++ b/backend/alwrity_utils/environment_setup.py @@ -0,0 +1,147 @@ +""" +Environment Setup Module +Handles environment configuration and directory setup. +""" + +import os +from pathlib import Path +from typing import List, Dict, Any + + +class EnvironmentSetup: + """Manages environment setup for ALwrity backend.""" + + def __init__(self, production_mode: bool = False): + self.production_mode = production_mode + # Use safer directory paths that don't conflict with deployment platforms + if production_mode: + # In production, use temp directories or skip directory creation + self.required_directories = [] + else: + # In development, use local directories + self.required_directories = [ + "lib/workspace/alwrity_content", + "lib/workspace/alwrity_web_research", + "lib/workspace/alwrity_prompts", + "lib/workspace/alwrity_config" + ] + + def setup_directories(self) -> bool: + """Create necessary directories for ALwrity.""" + print("šŸ“ Setting up directories...") + + if not self.required_directories: + 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}") + except Exception as e: + print(f" āŒ Failed to create {directory}: {e}") + return False + + print("āœ… All directories created successfully") + return True + + def setup_environment_variables(self) -> bool: + """Set up environment variables for the application.""" + print("šŸ”§ Setting up environment variables...") + + # Production environment variables + if self.production_mode: + env_vars = { + "HOST": "0.0.0.0", + "PORT": "8000", + "RELOAD": "false", + "LOG_LEVEL": "INFO", + "DEBUG": "false" + } + else: + env_vars = { + "HOST": "0.0.0.0", + "PORT": "8000", + "RELOAD": "true", + "LOG_LEVEL": "DEBUG", + "DEBUG": "true" + } + + for key, value in env_vars.items(): + os.environ.setdefault(key, value) + print(f" āœ… {key}={value}") + + print("āœ… Environment variables configured") + return True + + def create_env_file(self) -> bool: + """Create .env file with default configuration (development only).""" + if self.production_mode: + print("āš ļø Skipping .env file creation in production mode") + return True + + print("šŸ”§ Creating .env file...") + + env_file = Path(".env") + if env_file.exists(): + print(" āœ… .env file already exists") + return True + + env_content = """# ALwrity Backend Configuration + +# 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 + +# 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 + +# Authentication +# CLERK_SECRET_KEY=your_clerk_secret_key_here + +# 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 + +# Server Configuration +HOST=0.0.0.0 +PORT=8000 +DEBUG=true + +# Logging +LOG_LEVEL=INFO +""" + + try: + with open(env_file, 'w') as f: + f.write(env_content) + print("āœ… .env file created successfully") + return True + except Exception as e: + print(f"āŒ Error creating .env file: {e}") + return False + + def verify_environment(self) -> bool: + """Verify that the environment is properly configured.""" + print("šŸ” Verifying environment setup...") + + # Check required directories + for directory in self.required_directories: + if not Path(directory).exists(): + print(f"āŒ Directory missing: {directory}") + return False + + # Check environment variables + required_vars = ["HOST", "PORT", "LOG_LEVEL"] + for var in required_vars: + if not os.getenv(var): + print(f"āŒ Environment variable missing: {var}") + return False + + print("āœ… Environment verification complete") + return True diff --git a/backend/alwrity_utils/frontend_serving.py b/backend/alwrity_utils/frontend_serving.py new file mode 100644 index 00000000..4d63c08e --- /dev/null +++ b/backend/alwrity_utils/frontend_serving.py @@ -0,0 +1,82 @@ +""" +Frontend Serving Module +Handles React frontend serving and static file mounting. +""" + +import os +from pathlib import Path +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from loguru import logger +from typing import Dict, Any + + +class FrontendServing: + """Manages React frontend serving and static file mounting.""" + + def __init__(self, app: FastAPI): + self.app = app + self.frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "build") + self.static_path = os.path.join(self.frontend_build_path, "static") + + def setup_frontend_serving(self) -> bool: + """Set up React frontend serving and static file mounting.""" + try: + logger.info("Setting up frontend serving...") + + # Mount static files for React app (only if directory exists) + if os.path.exists(self.static_path): + self.app.mount("/static", StaticFiles(directory=self.static_path), name="static") + logger.info("Frontend static files mounted successfully") + return True + else: + logger.info("Frontend build directory not found. Static files not mounted.") + return False + + except Exception as e: + logger.error(f"Could not mount static files: {e}") + return False + + def serve_frontend(self) -> FileResponse | Dict[str, Any]: + """Serve the React frontend.""" + try: + # Check if frontend build exists + index_html = os.path.join(self.frontend_build_path, "index.html") + + if os.path.exists(index_html): + return FileResponse(index_html) + else: + return { + "message": "Frontend not built. Please run 'npm run build' in the frontend directory.", + "api_docs": "/api/docs" + } + + except Exception as e: + logger.error(f"Error serving frontend: {e}") + return { + "message": "Error serving frontend", + "error": str(e), + "api_docs": "/api/docs" + } + + def get_frontend_status(self) -> Dict[str, Any]: + """Get the status of frontend build and serving.""" + try: + index_html = os.path.join(self.frontend_build_path, "index.html") + static_exists = os.path.exists(self.static_path) + + return { + "frontend_build_path": self.frontend_build_path, + "static_path": self.static_path, + "index_html_exists": os.path.exists(index_html), + "static_files_exist": static_exists, + "frontend_ready": os.path.exists(index_html) and static_exists + } + + except Exception as e: + logger.error(f"Error checking frontend status: {e}") + return { + "error": str(e), + "frontend_ready": False + } diff --git a/backend/alwrity_utils/health_checker.py b/backend/alwrity_utils/health_checker.py new file mode 100644 index 00000000..719fe054 --- /dev/null +++ b/backend/alwrity_utils/health_checker.py @@ -0,0 +1,129 @@ +""" +Health Check Module +Handles health check endpoints and database health verification. +""" + +from fastapi import HTTPException +from datetime import datetime +from typing import Dict, Any +from loguru import logger + + +class HealthChecker: + """Manages health check functionality for ALwrity backend.""" + + def __init__(self): + self.startup_time = datetime.utcnow() + + def basic_health_check(self) -> Dict[str, Any]: + """Basic health check endpoint.""" + try: + return { + "status": "healthy", + "message": "ALwrity backend is running", + "timestamp": datetime.utcnow().isoformat(), + "uptime": str(datetime.utcnow() - self.startup_time) + } + except Exception as e: + logger.error(f"Health check failed: {e}") + return { + "status": "error", + "message": f"Health check failed: {str(e)}", + "timestamp": datetime.utcnow().isoformat() + } + + def database_health_check(self) -> Dict[str, Any]: + """Database health check endpoint including persona tables verification.""" + try: + from services.database import get_db_session + from models.persona_models import ( + WritingPersona, + PlatformPersona, + PersonaAnalysisResult, + PersonaValidationResult + ) + + session = get_db_session() + if not session: + return { + "status": "error", + "message": "Could not get database session", + "timestamp": datetime.utcnow().isoformat() + } + + # Test all persona tables + tables_status = {} + try: + session.query(WritingPersona).first() + tables_status["writing_personas"] = "ok" + except Exception as e: + tables_status["writing_personas"] = f"error: {str(e)}" + + try: + session.query(PlatformPersona).first() + tables_status["platform_personas"] = "ok" + except Exception as e: + tables_status["platform_personas"] = f"error: {str(e)}" + + try: + session.query(PersonaAnalysisResult).first() + tables_status["persona_analysis_results"] = "ok" + except Exception as e: + tables_status["persona_analysis_results"] = f"error: {str(e)}" + + try: + session.query(PersonaValidationResult).first() + tables_status["persona_validation_results"] = "ok" + except Exception as e: + tables_status["persona_validation_results"] = f"error: {str(e)}" + + session.close() + + # Check if all tables are ok + all_ok = all(status == "ok" for status in tables_status.values()) + + return { + "status": "healthy" if all_ok else "warning", + "message": "Database connection successful" if all_ok else "Some persona tables may have issues", + "persona_tables": tables_status, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + logger.error(f"Database health check failed: {e}") + return { + "status": "error", + "message": f"Database health check failed: {str(e)}", + "timestamp": datetime.utcnow().isoformat() + } + + def comprehensive_health_check(self) -> Dict[str, Any]: + """Comprehensive health check including all services.""" + try: + # Basic health + basic_health = self.basic_health_check() + + # Database health + db_health = self.database_health_check() + + # Determine overall status + overall_status = "healthy" + if basic_health["status"] != "healthy" or db_health["status"] == "error": + overall_status = "unhealthy" + elif db_health["status"] == "warning": + overall_status = "degraded" + + return { + "status": overall_status, + "basic": basic_health, + "database": db_health, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + logger.error(f"Comprehensive health check failed: {e}") + return { + "status": "error", + "message": f"Comprehensive health check failed: {str(e)}", + "timestamp": datetime.utcnow().isoformat() + } diff --git a/backend/alwrity_utils/onboarding_manager.py b/backend/alwrity_utils/onboarding_manager.py new file mode 100644 index 00000000..c800a334 --- /dev/null +++ b/backend/alwrity_utils/onboarding_manager.py @@ -0,0 +1,455 @@ +""" +Onboarding Manager Module +Handles all onboarding-related endpoints and functionality. +""" + +from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks +from fastapi.responses import FileResponse +from typing import Dict, Any, Optional +from loguru import logger + +# Import onboarding functions +from api.onboarding import ( + health_check, + initialize_onboarding, + get_onboarding_status, + get_onboarding_progress_full, + get_step_data, + complete_step, + skip_step, + validate_step_access, + get_api_keys, + get_api_keys_for_onboarding, + save_api_key, + validate_api_keys, + start_onboarding, + complete_onboarding, + reset_onboarding, + get_resume_info, + get_onboarding_config, + get_provider_setup_info, + get_all_providers_info, + validate_provider_key, + get_enhanced_validation_status, + get_onboarding_summary, + get_website_analysis_data, + get_research_preferences_data, + save_business_info, + get_business_info, + get_business_info_by_user, + update_business_info, + generate_writing_personas, + generate_writing_personas_async, + get_persona_task_status, + assess_persona_quality, + regenerate_persona, + get_persona_generation_options, + get_latest_persona, + save_persona_update, + StepCompletionRequest, + APIKeyRequest +) +from middleware.auth_middleware import get_current_user + + +class OnboardingManager: + """Manages all onboarding-related endpoints and functionality.""" + + def __init__(self, app: FastAPI): + self.app = app + self.setup_onboarding_endpoints() + + def setup_onboarding_endpoints(self): + """Set up all onboarding-related endpoints.""" + + # Onboarding initialization - BATCH ENDPOINT (reduces 4 API calls to 1) + @self.app.get("/api/onboarding/init") + async def onboarding_init(current_user: dict = Depends(get_current_user)): + """ + Batch initialization endpoint - combines user info, status, and progress. + This eliminates 3-4 separate API calls on initial load, reducing latency by 60-75%. + """ + try: + return await initialize_onboarding(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in onboarding_init: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Onboarding status endpoints + @self.app.get("/api/onboarding/status") + async def onboarding_status(current_user: dict = Depends(get_current_user)): + """Get the current onboarding status.""" + try: + return await get_onboarding_status(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in onboarding_status: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/progress") + async def onboarding_progress(current_user: dict = Depends(get_current_user)): + """Get the full onboarding progress data.""" + try: + return await get_onboarding_progress_full(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in onboarding_progress: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Step management endpoints + @self.app.get("/api/onboarding/step/{step_number}") + async def step_data(step_number: int, current_user: dict = Depends(get_current_user)): + """Get data for a specific step.""" + try: + return await get_step_data(step_number, current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in step_data: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step/{step_number}/complete") + async def step_complete(step_number: int, request: StepCompletionRequest, current_user: dict = Depends(get_current_user)): + """Mark a step as completed.""" + try: + return await complete_step(step_number, request, current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in step_complete: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step/{step_number}/skip") + async def step_skip(step_number: int, current_user: dict = Depends(get_current_user)): + """Skip a step (for optional steps).""" + try: + return await skip_step(step_number, current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in step_skip: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/step/{step_number}/validate") + async def step_validate(step_number: int, current_user: dict = Depends(get_current_user)): + """Validate if user can access a specific step.""" + try: + return await validate_step_access(step_number, current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in step_validate: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # API key management endpoints + @self.app.get("/api/onboarding/api-keys") + async def api_keys(): + """Get all configured API keys (masked).""" + try: + return await get_api_keys() + except Exception as e: + logger.error(f"Error in api_keys: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/api-keys/onboarding") + async def api_keys_for_onboarding(): + """Get all configured API keys for onboarding (unmasked).""" + try: + return await get_api_keys_for_onboarding() + except Exception as e: + logger.error(f"Error in api_keys_for_onboarding: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/api-keys") + async def api_key_save(request: APIKeyRequest): + """Save an API key for a provider.""" + try: + return await save_api_key(request) + except Exception as e: + logger.error(f"Error in api_key_save: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/api-keys/validate") + async def api_key_validate(): + """Validate all configured API keys.""" + try: + return await validate_api_keys() + except Exception as e: + logger.error(f"Error in api_key_validate: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Onboarding control endpoints + @self.app.post("/api/onboarding/start") + async def onboarding_start(current_user: dict = Depends(get_current_user)): + """Start a new onboarding session.""" + try: + return await start_onboarding(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in onboarding_start: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/complete") + async def onboarding_complete(current_user: dict = Depends(get_current_user)): + """Complete the onboarding process.""" + try: + return await complete_onboarding(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in onboarding_complete: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/reset") + async def onboarding_reset(): + """Reset the onboarding progress.""" + try: + return await reset_onboarding() + except Exception as e: + logger.error(f"Error in onboarding_reset: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Resume functionality + @self.app.get("/api/onboarding/resume") + async def onboarding_resume(): + """Get information for resuming onboarding.""" + try: + return await get_resume_info() + except Exception as e: + logger.error(f"Error in onboarding_resume: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Configuration endpoints + @self.app.get("/api/onboarding/config") + async def onboarding_config(): + """Get onboarding configuration and requirements.""" + try: + return get_onboarding_config() + except Exception as e: + logger.error(f"Error in onboarding_config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Enhanced provider endpoints + @self.app.get("/api/onboarding/providers/{provider}/setup") + async def provider_setup_info(provider: str): + """Get setup information for a specific provider.""" + try: + return await get_provider_setup_info(provider) + except Exception as e: + logger.error(f"Error in provider_setup_info: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/providers") + async def all_providers_info(): + """Get setup information for all providers.""" + try: + return await get_all_providers_info() + except Exception as e: + logger.error(f"Error in all_providers_info: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/providers/{provider}/validate") + async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest): + """Validate a specific provider's API key.""" + try: + return await validate_provider_key(provider, request) + except Exception as e: + logger.error(f"Error in validate_provider_key: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/validation/enhanced") + async def enhanced_validation_status(): + """Get enhanced validation status for all configured services.""" + try: + return await get_enhanced_validation_status() + except Exception as e: + logger.error(f"Error in enhanced_validation_status: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # New endpoints for FinalStep data loading + @self.app.get("/api/onboarding/summary") + async def onboarding_summary(current_user: dict = Depends(get_current_user)): + """Get comprehensive onboarding summary for FinalStep.""" + try: + return await get_onboarding_summary(current_user) + except Exception as e: + logger.error(f"Error in onboarding_summary: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/website-analysis") + async def website_analysis_data(current_user: dict = Depends(get_current_user)): + """Get website analysis data for FinalStep.""" + try: + return await get_website_analysis_data(current_user) + except Exception as e: + logger.error(f"Error in website_analysis_data: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/research-preferences") + async def research_preferences_data(current_user: dict = Depends(get_current_user)): + """Get research preferences data for FinalStep.""" + try: + return await get_research_preferences_data(current_user) + except Exception as e: + logger.error(f"Error in research_preferences_data: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Business Information endpoints + @self.app.post("/api/onboarding/business-info") + async def business_info_save(request: 'BusinessInfoRequest'): + """Save business information for users without websites.""" + try: + from models.business_info_request import BusinessInfoRequest + return await save_business_info(request) + except Exception as e: + logger.error(f"Error in business_info_save: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/business-info/{business_info_id}") + async def business_info_get(business_info_id: int): + """Get business information by ID.""" + try: + return await get_business_info(business_info_id) + except Exception as e: + logger.error(f"Error in business_info_get: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/business-info/user/{user_id}") + async def business_info_get_by_user(user_id: int): + """Get business information by user ID.""" + try: + return await get_business_info_by_user(user_id) + except Exception as e: + logger.error(f"Error in business_info_get_by_user: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.put("/api/onboarding/business-info/{business_info_id}") + async def business_info_update(business_info_id: int, request: 'BusinessInfoRequest'): + """Update business information.""" + try: + from models.business_info_request import BusinessInfoRequest + return await update_business_info(business_info_id, request) + except Exception as e: + logger.error(f"Error in business_info_update: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # Persona generation endpoints + @self.app.post("/api/onboarding/step4/generate-personas") + async def generate_personas(request: dict, current_user: dict = Depends(get_current_user)): + """Generate AI writing personas for Step 4.""" + try: + return await generate_writing_personas(request, current_user) + except Exception as e: + logger.error(f"Error in generate_personas: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step4/generate-personas-async") + async def generate_personas_async(request: dict, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)): + """Start async persona generation task.""" + try: + return await generate_writing_personas_async(request, current_user, background_tasks) + except Exception as e: + logger.error(f"Error in generate_personas_async: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/step4/persona-task/{task_id}") + async def get_persona_task(task_id: str): + """Get persona generation task status.""" + try: + return await get_persona_task_status(task_id) + except Exception as e: + logger.error(f"Error in get_persona_task: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/step4/persona-latest") + async def persona_latest(current_user: dict = Depends(get_current_user)): + """Get latest cached persona for current user.""" + try: + return await get_latest_persona(current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in persona_latest: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step4/persona-save") + async def persona_save(request: dict, current_user: dict = Depends(get_current_user)): + """Save edited persona back to cache.""" + try: + return await save_persona_update(request, current_user) + except HTTPException as he: + raise he + except Exception as e: + logger.error(f"Error in persona_save: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step4/assess-persona-quality") + async def assess_persona_quality_endpoint(request: dict, current_user: dict = Depends(get_current_user)): + """Assess the quality of generated personas.""" + try: + return await assess_persona_quality(request, current_user) + except Exception as e: + logger.error(f"Error in assess_persona_quality: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/api/onboarding/step4/regenerate-persona") + async def regenerate_persona_endpoint(request: dict, current_user: dict = Depends(get_current_user)): + """Regenerate a specific persona with improvements.""" + try: + return await regenerate_persona(request, current_user) + except Exception as e: + logger.error(f"Error in regenerate_persona: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.get("/api/onboarding/step4/persona-options") + async def get_persona_options(current_user: dict = Depends(get_current_user)): + """Get persona generation options and configurations.""" + try: + return await get_persona_generation_options(current_user) + except Exception as e: + logger.error(f"Error in get_persona_options: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + def get_onboarding_status(self) -> Dict[str, Any]: + """Get the status of onboarding endpoints.""" + return { + "onboarding_endpoints": [ + "/api/onboarding/init", + "/api/onboarding/status", + "/api/onboarding/progress", + "/api/onboarding/step/{step_number}", + "/api/onboarding/step/{step_number}/complete", + "/api/onboarding/step/{step_number}/skip", + "/api/onboarding/step/{step_number}/validate", + "/api/onboarding/api-keys", + "/api/onboarding/api-keys/onboarding", + "/api/onboarding/start", + "/api/onboarding/complete", + "/api/onboarding/reset", + "/api/onboarding/resume", + "/api/onboarding/config", + "/api/onboarding/providers/{provider}/setup", + "/api/onboarding/providers", + "/api/onboarding/providers/{provider}/validate", + "/api/onboarding/validation/enhanced", + "/api/onboarding/summary", + "/api/onboarding/website-analysis", + "/api/onboarding/research-preferences", + "/api/onboarding/business-info", + "/api/onboarding/step4/generate-personas", + "/api/onboarding/step4/generate-personas-async", + "/api/onboarding/step4/persona-task/{task_id}", + "/api/onboarding/step4/persona-latest", + "/api/onboarding/step4/persona-save", + "/api/onboarding/step4/assess-persona-quality", + "/api/onboarding/step4/regenerate-persona", + "/api/onboarding/step4/persona-options" + ], + "total_endpoints": 30, + "status": "active" + } diff --git a/backend/alwrity_utils/production_optimizer.py b/backend/alwrity_utils/production_optimizer.py new file mode 100644 index 00000000..6ea7d6e6 --- /dev/null +++ b/backend/alwrity_utils/production_optimizer.py @@ -0,0 +1,137 @@ +""" +Production Optimizer Module +Handles production-specific optimizations and configurations. +""" + +import os +import sys +from typing import List, Dict, Any + + +class ProductionOptimizer: + """Optimizes ALwrity backend for production deployment.""" + + def __init__(self): + self.production_optimizations = { + 'disable_spacy_download': True, + 'disable_nltk_download': True, + 'skip_linguistic_setup': True, + 'minimal_database_setup': True, + 'skip_file_creation': True + } + + def apply_production_optimizations(self) -> bool: + """Apply production-specific optimizations.""" + print("šŸš€ Applying production optimizations...") + + # Set production environment variables + self._set_production_env_vars() + + # Disable heavy operations + self._disable_heavy_operations() + + # Optimize logging + self._optimize_logging() + + print("āœ… Production optimizations applied") + return True + + def _set_production_env_vars(self) -> None: + """Set production-specific environment variables.""" + production_vars = { + 'HOST': '0.0.0.0', + 'PORT': '8000', + 'RELOAD': 'false', + 'LOG_LEVEL': 'INFO', + 'DEBUG': 'false', + 'PYTHONUNBUFFERED': '1', # Ensure logs are flushed immediately + 'PYTHONDONTWRITEBYTECODE': '1' # Don't create .pyc files + } + + for key, value in production_vars.items(): + os.environ.setdefault(key, value) + print(f" āœ… {key}={value}") + + def _disable_heavy_operations(self) -> None: + """Disable operations that are too heavy for production startup.""" + print(" ⚔ Disabling heavy operations for production...") + + # Disable spaCy model download + os.environ.setdefault('DISABLE_SPACY_DOWNLOAD', 'true') + + # Disable NLTK data download + os.environ.setdefault('DISABLE_NLTK_DOWNLOAD', 'true') + + # Skip linguistic analyzer setup + os.environ.setdefault('SKIP_LINGUISTIC_SETUP', 'true') + + print(" āœ… Heavy operations disabled") + + def _optimize_logging(self) -> None: + """Optimize logging for production.""" + print(" šŸ“ Optimizing logging for production...") + + # Set appropriate log level + os.environ.setdefault('LOG_LEVEL', 'INFO') + + # Disable debug logging + os.environ.setdefault('DEBUG', 'false') + + print(" āœ… Logging optimized") + + def skip_linguistic_setup(self) -> bool: + """Skip linguistic analysis setup in production.""" + if os.getenv('SKIP_LINGUISTIC_SETUP', 'false').lower() == 'true': + print("āš ļø Skipping linguistic analysis setup (production mode)") + return True + return False + + def skip_spacy_setup(self) -> bool: + """Skip spaCy model setup in production.""" + if os.getenv('DISABLE_SPACY_DOWNLOAD', 'false').lower() == 'true': + print("āš ļø Skipping spaCy model setup (production mode)") + return True + return False + + def skip_nltk_setup(self) -> bool: + """Skip NLTK data setup in production.""" + if os.getenv('DISABLE_NLTK_DOWNLOAD', 'false').lower() == 'true': + print("āš ļø Skipping NLTK data setup (production mode)") + return True + return False + + def get_production_config(self) -> Dict[str, Any]: + """Get production configuration settings.""" + return { + 'host': os.getenv('HOST', '0.0.0.0'), + 'port': int(os.getenv('PORT', '8000')), + 'reload': False, # Never reload in production + 'log_level': os.getenv('LOG_LEVEL', 'info'), + 'access_log': True, + 'workers': 1, # Single worker for Render + 'timeout_keep_alive': 30, + 'timeout_graceful_shutdown': 30 + } + + def validate_production_environment(self) -> bool: + """Validate that the environment is ready for production.""" + print("šŸ” Validating production environment...") + + # Check critical environment variables + required_vars = ['HOST', 'PORT', 'LOG_LEVEL'] + missing_vars = [] + + for var in required_vars: + if not os.getenv(var): + missing_vars.append(var) + + if missing_vars: + print(f"āŒ Missing environment variables: {missing_vars}") + return False + + # Check that reload is disabled + if os.getenv('RELOAD', 'false').lower() == 'true': + print("āš ļø Warning: RELOAD is enabled in production") + + print("āœ… Production environment validated") + return True diff --git a/backend/alwrity_utils/rate_limiter.py b/backend/alwrity_utils/rate_limiter.py new file mode 100644 index 00000000..7082991e --- /dev/null +++ b/backend/alwrity_utils/rate_limiter.py @@ -0,0 +1,125 @@ +""" +Rate Limiting Module +Handles rate limiting middleware and request tracking. +""" + +import time +from collections import defaultdict +from typing import Dict, List, Optional +from fastapi import Request, Response +from fastapi.responses import JSONResponse +from loguru import logger + + +class RateLimiter: + """Manages rate limiting for ALwrity backend.""" + + def __init__(self, window_seconds: int = 60, max_requests: int = 200): + self.window_seconds = window_seconds + self.max_requests = max_requests + self.request_counts: Dict[str, List[float]] = defaultdict(list) + + # Endpoints exempt from rate limiting + self.exempt_paths = [ + "/stream/strategies", + "/stream/strategic-intelligence", + "/stream/keyword-research", + "/latest-strategy", + "/ai-analytics", + "/gap-analysis", + "/calendar-events", + "/calendar-generation/progress", + "/health", + "/health/database" + ] + + def is_exempt_path(self, path: str) -> bool: + """Check if a path is exempt from rate limiting.""" + return any(exempt_path in path for exempt_path in self.exempt_paths) + + def clean_old_requests(self, client_ip: str, current_time: float) -> None: + """Clean old requests from the tracking dictionary.""" + self.request_counts[client_ip] = [ + req_time for req_time in self.request_counts[client_ip] + if current_time - req_time < self.window_seconds + ] + + def is_rate_limited(self, client_ip: str, current_time: float) -> bool: + """Check if a client has exceeded the rate limit.""" + self.clean_old_requests(client_ip, current_time) + return len(self.request_counts[client_ip]) >= self.max_requests + + def add_request(self, client_ip: str, current_time: float) -> None: + """Add a request to the tracking dictionary.""" + self.request_counts[client_ip].append(current_time) + + def get_rate_limit_response(self) -> JSONResponse: + """Get a rate limit exceeded response.""" + return JSONResponse( + status_code=429, + content={ + "detail": "Too many requests", + "retry_after": self.window_seconds + }, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*" + } + ) + + async def rate_limit_middleware(self, request: Request, call_next) -> Response: + """Rate limiting middleware with exemptions for streaming endpoints.""" + try: + client_ip = request.client.host if request.client else "unknown" + current_time = time.time() + path = request.url.path + + # Check if path is exempt from rate limiting + if self.is_exempt_path(path): + # Allow streaming endpoints without rate limiting + response = await call_next(request) + return response + + # Check rate limit + if self.is_rate_limited(client_ip, current_time): + logger.warning(f"Rate limit exceeded for {client_ip}") + return self.get_rate_limit_response() + + # Add current request + self.add_request(client_ip, current_time) + + response = await call_next(request) + return response + + except Exception as e: + logger.error(f"Error in rate limiting middleware: {e}") + # Continue without rate limiting if there's an error + response = await call_next(request) + return response + + def get_rate_limit_status(self, client_ip: str) -> Dict[str, any]: + """Get current rate limit status for a client.""" + current_time = time.time() + self.clean_old_requests(client_ip, current_time) + + request_count = len(self.request_counts[client_ip]) + remaining_requests = max(0, self.max_requests - request_count) + + return { + "client_ip": client_ip, + "requests_in_window": request_count, + "max_requests": self.max_requests, + "remaining_requests": remaining_requests, + "window_seconds": self.window_seconds, + "is_limited": request_count >= self.max_requests + } + + def reset_rate_limit(self, client_ip: Optional[str] = None) -> Dict[str, any]: + """Reset rate limit for a specific client or all clients.""" + if client_ip: + self.request_counts[client_ip] = [] + return {"message": f"Rate limit reset for {client_ip}"} + else: + self.request_counts.clear() + return {"message": "Rate limit reset for all clients"} diff --git a/backend/alwrity_utils/router_manager.py b/backend/alwrity_utils/router_manager.py new file mode 100644 index 00000000..6e13155a --- /dev/null +++ b/backend/alwrity_utils/router_manager.py @@ -0,0 +1,168 @@ +""" +Router Manager Module +Handles FastAPI router inclusion and management. +""" + +from fastapi import FastAPI +from loguru import logger +from typing import List, Dict, Any, Optional + + +class RouterManager: + """Manages FastAPI router inclusion and organization.""" + + def __init__(self, app: FastAPI): + self.app = app + self.included_routers = [] + self.failed_routers = [] + + def include_router_safely(self, router, router_name: str = None) -> bool: + """Include a router safely with error handling.""" + 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}") + 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}") + return False + + def include_core_routers(self) -> bool: + """Include core application routers.""" + try: + logger.info("Including core routers...") + + # Component logic router + from api.component_logic import router as component_logic_router + self.include_router_safely(component_logic_router, "component_logic") + + # Subscription router + from api.subscription_api import router as subscription_router + self.include_router_safely(subscription_router, "subscription") + + # GSC router + from routers.gsc_auth import router as gsc_auth_router + self.include_router_safely(gsc_auth_router, "gsc_auth") + + # WordPress router + from routers.wordpress_oauth import router as wordpress_oauth_router + self.include_router_safely(wordpress_oauth_router, "wordpress_oauth") + + # SEO tools router + from routers.seo_tools import router as seo_tools_router + self.include_router_safely(seo_tools_router, "seo_tools") + + # Facebook Writer router + from api.facebook_writer.routers import facebook_router + self.include_router_safely(facebook_router, "facebook_writer") + + # LinkedIn routers + from routers.linkedin import router as linkedin_router + self.include_router_safely(linkedin_router, "linkedin") + + from api.linkedin_image_generation import router as linkedin_image_router + self.include_router_safely(linkedin_image_router, "linkedin_image") + + # Brainstorm router + from api.brainstorm import router as brainstorm_router + self.include_router_safely(brainstorm_router, "brainstorm") + + # Hallucination detector and writing assistant + from api.hallucination_detector import router as hallucination_detector_router + self.include_router_safely(hallucination_detector_router, "hallucination_detector") + + from api.writing_assistant import router as writing_assistant_router + self.include_router_safely(writing_assistant_router, "writing_assistant") + + # Content planning and user data + from api.content_planning.api.router import router as content_planning_router + self.include_router_safely(content_planning_router, "content_planning") + + from api.user_data import router as user_data_router + self.include_router_safely(user_data_router, "user_data") + + from api.user_environment import router as user_environment_router + self.include_router_safely(user_environment_router, "user_environment") + + # Strategy copilot + from api.content_planning.strategy_copilot import router as strategy_copilot_router + self.include_router_safely(strategy_copilot_router, "strategy_copilot") + + logger.info("āœ… Core routers included successfully") + return True + + except Exception as e: + logger.error(f"āŒ Error including core routers: {e}") + return False + + def include_optional_routers(self) -> bool: + """Include optional routers with error handling.""" + try: + logger.info("Including optional routers...") + + # AI Blog Writer router + try: + from api.blog_writer.router import router as blog_writer_router + self.include_router_safely(blog_writer_router, "blog_writer") + except Exception as e: + logger.warning(f"AI Blog Writer router not mounted: {e}") + + # Wix Integration router + try: + from api.wix_routes import router as wix_router + self.include_router_safely(wix_router, "wix") + except Exception as e: + logger.warning(f"Wix Integration router not mounted: {e}") + + # Blog Writer SEO Analysis router + try: + from api.blog_writer.seo_analysis import router as blog_seo_analysis_router + self.include_router_safely(blog_seo_analysis_router, "blog_seo_analysis") + except Exception as e: + logger.warning(f"Blog Writer SEO Analysis router not mounted: {e}") + + # Persona router + try: + from api.persona_routes import router as persona_router + self.include_router_safely(persona_router, "persona") + except Exception as e: + logger.warning(f"Persona router not mounted: {e}") + + # Stability AI routers + try: + from routers.stability import router as stability_router + self.include_router_safely(stability_router, "stability") + + from routers.stability_advanced import router as stability_advanced_router + self.include_router_safely(stability_advanced_router, "stability_advanced") + + from routers.stability_admin import router as stability_admin_router + self.include_router_safely(stability_admin_router, "stability_admin") + except Exception as e: + logger.warning(f"Stability AI routers not mounted: {e}") + + # Step 3 Research router + try: + from api.onboarding_utils.step3_routes import router as step3_research_router + self.include_router_safely(step3_research_router, "step3_research") + except Exception as e: + logger.warning(f"Step 3 Research router not mounted: {e}") + + logger.info("āœ… Optional routers processed") + return True + + except Exception as e: + logger.error(f"āŒ Error including optional routers: {e}") + return False + + def get_router_status(self) -> Dict[str, Any]: + """Get the status of router inclusion.""" + return { + "included_routers": self.included_routers, + "failed_routers": self.failed_routers, + "total_included": len(self.included_routers), + "total_failed": len(self.failed_routers) + } diff --git a/backend/app.py b/backend/app.py index 2ec57e42..63afe167 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3,64 +3,23 @@ from fastapi import FastAPI, HTTPException, Depends, Request, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Dict, Any, Optional import os -import time -from collections import defaultdict from loguru import logger from dotenv import load_dotenv import asyncio from datetime import datetime from middleware.monitoring_middleware import monitoring_middleware +# Import modular utilities +from alwrity_utils import HealthChecker, RateLimiter, FrontendServing, RouterManager, OnboardingManager + # Load environment variables load_dotenv() -# Import the new enhanced functions -from api.onboarding import ( - health_check, - initialize_onboarding, # NEW: Batch init endpoint - get_onboarding_status, - get_onboarding_progress_full, - get_step_data, - complete_step, - skip_step, - validate_step_access, - get_api_keys, - get_api_keys_for_onboarding, - save_api_key, - validate_api_keys, - start_onboarding, - complete_onboarding, - reset_onboarding, - get_resume_info, - get_onboarding_config, - get_provider_setup_info, - get_all_providers_info, - validate_provider_key, - get_enhanced_validation_status, - get_onboarding_summary, - get_website_analysis_data, - get_research_preferences_data, - save_business_info, - get_business_info, - get_business_info_by_user, - update_business_info, - # Persona generation endpoints - generate_writing_personas, - generate_writing_personas_async, - get_persona_task_status, - assess_persona_quality, - regenerate_persona, - get_persona_generation_options, - # New cache helpers - get_latest_persona, - save_persona_update, - StepCompletionRequest, - APIKeyRequest -) +# Import middleware from middleware.auth_middleware import get_current_user # Import component logic endpoints @@ -138,554 +97,69 @@ app.add_middleware( # Temporarily disabled for Wix testing # app.middleware("http")(monitoring_middleware) -# Simple rate limiting -request_counts = defaultdict(list) -RATE_LIMIT_WINDOW = 60 # 60 seconds -RATE_LIMIT_MAX_REQUESTS = 200 # Increased for testing - calendar generation polling +# Initialize modular utilities +health_checker = HealthChecker() +rate_limiter = RateLimiter(window_seconds=60, max_requests=200) +frontend_serving = FrontendServing(app) +router_manager = RouterManager(app) +onboarding_manager = OnboardingManager(app) @app.middleware("http") async def rate_limit_middleware(request: Request, call_next): - """Simple rate limiting middleware with exemptions for streaming endpoints.""" - try: - client_ip = request.client.host if request.client else "unknown" - current_time = time.time() - - # Exempt streaming endpoints and frequently called endpoints from rate limiting - path = request.url.path - if any(streaming_path in path for streaming_path in [ - "/stream/strategies", - "/stream/strategic-intelligence", - "/stream/keyword-research", - "/latest-strategy", # Exempt latest strategy endpoint from rate limiting - "/ai-analytics", # Exempt AI analytics endpoint from rate limiting - "/gap-analysis", # Exempt gap analysis endpoint from rate limiting - "/calendar-events", # Exempt calendar events endpoint from rate limiting - "/calendar-generation/progress", # Exempt calendar generation progress from rate limiting - "/health" # Exempt health check endpoints from rate limiting - ]): - # Allow streaming endpoints without rate limiting - response = await call_next(request) - return response - - # Clean old requests - request_counts[client_ip] = [req_time for req_time in request_counts[client_ip] - if current_time - req_time < RATE_LIMIT_WINDOW] - - # Check rate limit - if len(request_counts[client_ip]) >= RATE_LIMIT_MAX_REQUESTS: - logger.warning(f"Rate limit exceeded for {client_ip}") - return JSONResponse( - status_code=429, - content={"detail": "Too many requests", "retry_after": RATE_LIMIT_WINDOW}, - headers={ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", - "Access-Control-Allow-Headers": "*" - } - ) - - # Add current request - request_counts[client_ip].append(current_time) - - response = await call_next(request) - return response - - except Exception as e: - logger.error(f"Error in rate limiting middleware: {e}") - # Continue without rate limiting if there's an error - response = await call_next(request) - return response + """Rate limiting middleware using modular utilities.""" + return await rate_limiter.rate_limit_middleware(request, call_next) -# Health check endpoint +# Health check endpoints using modular utilities @app.get("/health") async def health(): """Health check endpoint.""" - return health_check() + return health_checker.basic_health_check() @app.get("/health/database") -async def database_health_check(): - """Database health check endpoint including persona tables verification.""" - try: - from services.database import get_db_session - from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult, PersonaValidationResult - - session = get_db_session() - if not session: - return {"status": "error", "message": "Could not get database session"} - - # Test all persona tables - tables_status = {} - try: - session.query(WritingPersona).first() - tables_status["writing_personas"] = "ok" - except Exception as e: - tables_status["writing_personas"] = f"error: {str(e)}" - - try: - session.query(PlatformPersona).first() - tables_status["platform_personas"] = "ok" - except Exception as e: - tables_status["platform_personas"] = f"error: {str(e)}" - - try: - session.query(PersonaAnalysisResult).first() - tables_status["persona_analysis_results"] = "ok" - except Exception as e: - tables_status["persona_analysis_results"] = f"error: {str(e)}" - - try: - session.query(PersonaValidationResult).first() - tables_status["persona_validation_results"] = "ok" - except Exception as e: - tables_status["persona_validation_results"] = f"error: {str(e)}" - - session.close() - - # Check if all tables are ok - all_ok = all(status == "ok" for status in tables_status.values()) - - return { - "status": "healthy" if all_ok else "warning", - "message": "Database connection successful" if all_ok else "Some persona tables may have issues", - "persona_tables": tables_status, - "timestamp": datetime.utcnow().isoformat() - } - - except Exception as e: - return { - "status": "error", - "message": f"Database health check failed: {str(e)}", - "timestamp": datetime.utcnow().isoformat() - } +async def database_health(): + """Database health check endpoint.""" + return health_checker.database_health_check() -# Onboarding initialization - BATCH ENDPOINT (reduces 4 API calls to 1) -@app.get("/api/onboarding/init") -async def onboarding_init(current_user: dict = Depends(get_current_user)): - """ - Batch initialization endpoint - combines user info, status, and progress. - This eliminates 3-4 separate API calls on initial load, reducing latency by 60-75%. - """ - try: - return await initialize_onboarding(current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in onboarding_init: {e}") - raise HTTPException(status_code=500, detail=str(e)) +@app.get("/health/comprehensive") +async def comprehensive_health(): + """Comprehensive health check endpoint.""" + return health_checker.comprehensive_health_check() -# Onboarding status endpoints +# Rate limiting management endpoints +@app.get("/api/rate-limit/status") +async def rate_limit_status(request: Request): + """Get current rate limit status for the requesting client.""" + client_ip = request.client.host if request.client else "unknown" + return rate_limiter.get_rate_limit_status(client_ip) + +@app.post("/api/rate-limit/reset") +async def reset_rate_limit(request: Request, client_ip: Optional[str] = None): + """Reset rate limit for a specific client or all clients.""" + if client_ip is None: + client_ip = request.client.host if request.client else "unknown" + return rate_limiter.reset_rate_limit(client_ip) + +# Frontend serving management endpoints +@app.get("/api/frontend/status") +async def frontend_status(): + """Get frontend serving status.""" + return frontend_serving.get_frontend_status() + +# Router management endpoints +@app.get("/api/routers/status") +async def router_status(): + """Get router inclusion status.""" + return router_manager.get_router_status() + +# Onboarding management endpoints @app.get("/api/onboarding/status") -async def onboarding_status(current_user: dict = Depends(get_current_user)): - """Get the current onboarding status.""" - try: - # Pass current_user explicitly to user-scoped handler - return await get_onboarding_status(current_user) - except HTTPException as he: - # Preserve HTTP error codes like 401 Unauthorized - raise he - except Exception as e: - logger.error(f"Error in onboarding_status: {e}") - raise HTTPException(status_code=500, detail=str(e)) +async def onboarding_status(): + """Get onboarding manager status.""" + return onboarding_manager.get_onboarding_status() -@app.get("/api/onboarding/progress") -async def onboarding_progress(current_user: dict = Depends(get_current_user)): - """Get the full onboarding progress data.""" - try: - return await get_onboarding_progress_full(current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in onboarding_progress: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Step management endpoints -@app.get("/api/onboarding/step/{step_number}") -async def step_data(step_number: int, current_user: dict = Depends(get_current_user)): - """Get data for a specific step.""" - try: - return await get_step_data(step_number, current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in step_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step/{step_number}/complete") -async def step_complete(step_number: int, request: StepCompletionRequest, current_user: dict = Depends(get_current_user)): - """Mark a step as completed.""" - try: - return await complete_step(step_number, request, current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in step_complete: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step/{step_number}/skip") -async def step_skip(step_number: int, current_user: dict = Depends(get_current_user)): - """Skip a step (for optional steps).""" - try: - return await skip_step(step_number, current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in step_skip: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/step/{step_number}/validate") -async def step_validate(step_number: int, current_user: dict = Depends(get_current_user)): - """Validate if user can access a specific step.""" - try: - return await validate_step_access(step_number, current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in step_validate: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# API key management endpoints -@app.get("/api/onboarding/api-keys") -async def api_keys(): - """Get all configured API keys (masked).""" - try: - return await get_api_keys() - except Exception as e: - logger.error(f"Error in api_keys: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/api-keys/onboarding") -async def api_keys_for_onboarding(): - """Get all configured API keys for onboarding (unmasked).""" - try: - return await get_api_keys_for_onboarding() - except Exception as e: - logger.error(f"Error in api_keys_for_onboarding: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/api-keys") -async def api_key_save(request: APIKeyRequest): - """Save an API key for a provider.""" - try: - return await save_api_key(request) - except Exception as e: - logger.error(f"Error in api_key_save: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/api-keys/validate") -async def api_key_validate(): - """Validate all configured API keys.""" - try: - return await validate_api_keys() - except Exception as e: - logger.error(f"Error in api_key_validate: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Onboarding control endpoints -@app.post("/api/onboarding/start") -async def onboarding_start(current_user: dict = Depends(get_current_user)): - """Start a new onboarding session.""" - try: - return await start_onboarding(current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in onboarding_start: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/complete") -async def onboarding_complete(current_user: dict = Depends(get_current_user)): - """Complete the onboarding process.""" - try: - return await complete_onboarding(current_user) - except HTTPException as he: - raise he - except Exception as e: - logger.error(f"Error in onboarding_complete: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/reset") -async def onboarding_reset(): - """Reset the onboarding progress.""" - try: - return await reset_onboarding() - except Exception as e: - logger.error(f"Error in onboarding_reset: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Resume functionality -@app.get("/api/onboarding/resume") -async def onboarding_resume(): - """Get information for resuming onboarding.""" - try: - return await get_resume_info() - except Exception as e: - logger.error(f"Error in onboarding_resume: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Configuration endpoints -@app.get("/api/onboarding/config") -async def onboarding_config(): - """Get onboarding configuration and requirements.""" - try: - return get_onboarding_config() - except Exception as e: - logger.error(f"Error in onboarding_config: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Enhanced provider endpoints -@app.get("/api/onboarding/providers/{provider}/setup") -async def provider_setup_info(provider: str): - """Get setup information for a specific provider.""" - try: - return await get_provider_setup_info(provider) - except Exception as e: - logger.error(f"Error in provider_setup_info: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/providers") -async def all_providers_info(): - """Get setup information for all providers.""" - try: - return await get_all_providers_info() - except Exception as e: - logger.error(f"Error in all_providers_info: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/providers/{provider}/validate") -async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest): - """Validate a specific provider's API key.""" - try: - return await validate_provider_key(provider, request) - except Exception as e: - logger.error(f"Error in validate_provider_key: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/validation/enhanced") -async def enhanced_validation_status(): - """Get enhanced validation status for all configured services.""" - try: - return await get_enhanced_validation_status() - except Exception as e: - logger.error(f"Error in enhanced_validation_status: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# New endpoints for FinalStep data loading -@app.get("/api/onboarding/summary") -async def onboarding_summary(current_user: dict = Depends(get_current_user)): - """Get comprehensive onboarding summary for FinalStep.""" - try: - return await get_onboarding_summary(current_user) - except Exception as e: - logger.error(f"Error in onboarding_summary: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/website-analysis") -async def website_analysis_data(current_user: dict = Depends(get_current_user)): - """Get website analysis data for FinalStep.""" - try: - return await get_website_analysis_data(current_user) - except Exception as e: - logger.error(f"Error in website_analysis_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/research-preferences") -async def research_preferences_data(current_user: dict = Depends(get_current_user)): - """Get research preferences data for FinalStep.""" - try: - return await get_research_preferences_data(current_user) - except Exception as e: - logger.error(f"Error in research_preferences_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Business Information endpoints -@app.post("/api/onboarding/business-info") -async def business_info_save(request: 'BusinessInfoRequest'): - """Save business information for users without websites.""" - try: - from models.business_info_request import BusinessInfoRequest - return await save_business_info(request) - except Exception as e: - logger.error(f"Error in business_info_save: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/business-info/{business_info_id}") -async def business_info_get(business_info_id: int): - """Get business information by ID.""" - try: - return await get_business_info(business_info_id) - except Exception as e: - logger.error(f"Error in business_info_get: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/business-info/user/{user_id}") -async def business_info_get_by_user(user_id: int): - """Get business information by user ID.""" - try: - return await get_business_info_by_user(user_id) - except Exception as e: - logger.error(f"Error in business_info_get_by_user: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.put("/api/onboarding/business-info/{business_info_id}") -async def business_info_update(business_info_id: int, request: 'BusinessInfoRequest'): - """Update business information.""" - try: - from models.business_info_request import BusinessInfoRequest - return await update_business_info(business_info_id, request) - except Exception as e: - logger.error(f"Error in business_info_update: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Persona generation endpoints -@app.post("/api/onboarding/step4/generate-personas") -async def generate_personas(request: dict, current_user: dict = Depends(get_current_user)): - """Generate AI writing personas for Step 4.""" - try: - return await generate_writing_personas(request, current_user) - except Exception as e: - logger.error(f"Error in generate_personas: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step4/generate-personas-async") -async def generate_personas_async(request: dict, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)): - """Start async persona generation task.""" - try: - return await generate_writing_personas_async(request, current_user, background_tasks) - except Exception as e: - logger.error(f"Error in generate_personas_async: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/step4/persona-task/{task_id}") -async def get_persona_task(task_id: str): - """Get persona generation task status.""" - try: - return await get_persona_task_status(task_id) - except Exception as e: - logger.error(f"Error in get_persona_task: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/step4/persona-latest") -async def persona_latest(current_user: dict = Depends(get_current_user)): - """Get latest cached persona for current user.""" - try: - return await get_latest_persona(current_user) - except HTTPException as he: - # Re-raise HTTP exceptions (like 404) as-is - raise he - except Exception as e: - logger.error(f"Error in persona_latest: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step4/persona-save") -async def persona_save(request: dict, current_user: dict = Depends(get_current_user)): - """Save edited persona back to cache.""" - try: - return await save_persona_update(request, current_user) - except HTTPException as he: - # Re-raise HTTP exceptions as-is - raise he - except Exception as e: - logger.error(f"Error in persona_save: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step4/assess-persona-quality") -async def assess_persona_quality_endpoint(request: dict, current_user: dict = Depends(get_current_user)): - """Assess the quality of generated personas.""" - try: - return await assess_persona_quality(request, current_user) - except Exception as e: - logger.error(f"Error in assess_persona_quality: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/onboarding/step4/regenerate-persona") -async def regenerate_persona_endpoint(request: dict, current_user: dict = Depends(get_current_user)): - """Regenerate a specific persona with improvements.""" - try: - return await regenerate_persona(request, current_user) - except Exception as e: - logger.error(f"Error in regenerate_persona: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/api/onboarding/step4/persona-options") -async def get_persona_options(current_user: dict = Depends(get_current_user)): - """Get persona generation options and configurations.""" - try: - return await get_persona_generation_options(current_user) - except Exception as e: - logger.error(f"Error in get_persona_options: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -# Include component logic router -app.include_router(component_logic_router) - -# Include subscription and usage tracking router -app.include_router(subscription_router) - -# Include GSC router -from routers.gsc_auth import router as gsc_auth_router -app.include_router(gsc_auth_router) - -# Include WordPress router -from routers.wordpress_oauth import router as wordpress_oauth_router -app.include_router(wordpress_oauth_router) - -# Include SEO tools router -app.include_router(seo_tools_router) -# Include Facebook Writer router -app.include_router(facebook_router) -# Include LinkedIn content generation router -app.include_router(linkedin_router) -# Include LinkedIn image generation router -app.include_router(linkedin_image_router) -app.include_router(brainstorm_router) - -# Include hallucination detector router -app.include_router(hallucination_detector_router) -app.include_router(writing_assistant_router) - -# Include user data router -# Include content planning router -app.include_router(content_planning_router) -app.include_router(user_data_router) -app.include_router(strategy_copilot_router) -app.include_router(user_environment_router) - -# Include AI Blog Writer router -try: - from api.blog_writer.router import router as blog_writer_router - app.include_router(blog_writer_router) -except Exception as e: - logger.warning(f"AI Blog Writer router not mounted: {e}") - -# Include Wix Integration router -try: - from api.wix_routes import router as wix_router - app.include_router(wix_router) -except Exception as e: - logger.warning(f"Wix Integration router not mounted: {e}") - -# Include Blog Writer SEO Analysis router (comprehensive SEO analysis) -try: - from api.blog_writer.seo_analysis import router as blog_seo_analysis_router - app.include_router(blog_seo_analysis_router) -except Exception as e: - logger.warning(f"Blog Writer SEO Analysis router not mounted: {e}") - -# Include persona router -from api.persona_routes import router as persona_router -app.include_router(persona_router) - -# Include Stability AI routers -from routers.stability import router as stability_router -from routers.stability_advanced import router as stability_advanced_router -from routers.stability_admin import router as stability_admin_router -app.include_router(stability_router) -app.include_router(stability_advanced_router) -app.include_router(stability_admin_router) - -# Step 3 Research router -from api.onboarding_utils.step3_routes import router as step3_research_router -app.include_router(step3_research_router) +# Include routers using modular utilities +router_manager.include_core_routers() +router_manager.include_optional_routers() # SEO Dashboard endpoints @app.get("/api/seo-dashboard/data") @@ -744,33 +218,14 @@ async def batch_analyze_urls_endpoint(urls: list[str]): """Analyze multiple URLs in batch.""" return await batch_analyze_urls(urls) +# Setup frontend serving using modular utilities +frontend_serving.setup_frontend_serving() + # Serve React frontend (for production) @app.get("/") async def serve_frontend(): """Serve the React frontend.""" - # Check if frontend build exists - frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build") - index_html = os.path.join(frontend_path, "index.html") - - if os.path.exists(index_html): - return FileResponse(index_html) - else: - return { - "message": "Frontend not built. Please run 'npm run build' in the frontend directory.", - "api_docs": "/api/docs" - } - -# Mount static files for React app (only if directory exists) -try: - frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build") - static_path = os.path.join(frontend_build_path, "static") - if os.path.exists(static_path): - app.mount("/static", StaticFiles(directory=static_path), name="static") - logger.info("Frontend static files mounted successfully") - else: - logger.info("Frontend build directory not found. Static files not mounted.") -except Exception as e: - logger.info(f"Could not mount static files: {e}") + return frontend_serving.serve_frontend() # Startup event @app.on_event("startup") diff --git a/backend/middleware/logging_middleware.py b/backend/middleware/logging_middleware.py index 3b553435..34cbc9ee 100644 --- a/backend/middleware/logging_middleware.py +++ b/backend/middleware/logging_middleware.py @@ -17,7 +17,7 @@ import os import time # Logging configuration -LOG_BASE_DIR = "/workspace/backend/logs" +LOG_BASE_DIR = "logs" os.makedirs(LOG_BASE_DIR, exist_ok=True) # Ensure subdirectories exist diff --git a/backend/render.yaml b/backend/render.yaml new file mode 100644 index 00000000..58ca37fd --- /dev/null +++ b/backend/render.yaml @@ -0,0 +1,44 @@ +# Render deployment configuration for ALwrity Backend +services: + - type: web + name: alwrity-backend + env: python + buildCommand: pip install -r requirements.txt + startCommand: python start_alwrity_backend.py --production + healthCheckPath: /health + envVars: + - key: HOST + value: 0.0.0.0 + - key: PORT + value: 8000 + - key: RELOAD + value: false + - key: LOG_LEVEL + value: INFO + - key: PYTHONPATH + value: /opt/render/project/src/backend + - key: TMPDIR + value: /tmp + # Add your environment variables here: + # - key: GEMINI_API_KEY + # value: your_gemini_key_here + # - key: OPENAI_API_KEY + # value: your_openai_key_here + # - key: ANTHROPIC_API_KEY + # value: your_anthropic_key_here + # - key: MISTRAL_API_KEY + # value: your_mistral_key_here + # - key: TAVILY_API_KEY + # value: your_tavily_key_here + # - key: EXA_API_KEY + # value: your_exa_key_here + # - key: SERPER_API_KEY + # value: your_serper_key_here + # - key: CLERK_SECRET_KEY + # value: your_clerk_secret_here + # - key: GSC_REDIRECT_URI + # value: https://your-frontend.vercel.app/gsc/callback + # - key: WORDPRESS_REDIRECT_URI + # value: https://your-frontend.vercel.app/wp/callback + # - key: WIX_REDIRECT_URI + # value: https://your-frontend.vercel.app/wix/callback diff --git a/backend/routers/seo_tools.py b/backend/routers/seo_tools.py index 3167903a..cb71b8eb 100644 --- a/backend/routers/seo_tools.py +++ b/backend/routers/seo_tools.py @@ -33,7 +33,7 @@ from middleware.logging_middleware import log_api_call, save_to_file router = APIRouter(prefix="/api/seo", tags=["AI SEO Tools"]) # Configuration for intelligent logging -LOG_DIR = "/workspace/backend/logs/seo_tools" +LOG_DIR = "logs/seo_tools" os.makedirs(LOG_DIR, exist_ok=True) # Request/Response Models diff --git a/backend/services/user_workspace_manager.py b/backend/services/user_workspace_manager.py index 1572be5f..ae5aee58 100644 --- a/backend/services/user_workspace_manager.py +++ b/backend/services/user_workspace_manager.py @@ -18,14 +18,32 @@ class UserWorkspaceManager: def __init__(self, db_session: Session): self.db = db_session - self.base_workspace_dir = Path("lib/workspace") - self.user_workspaces_dir = self.base_workspace_dir / "users" + # Use environment-safe paths for production + if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"): + # In production, use temp directories or skip file operations + self.base_workspace_dir = Path("/tmp/alwrity_workspace") + self.user_workspaces_dir = self.base_workspace_dir / "users" + else: + # In development, use local directories + self.base_workspace_dir = Path("lib/workspace") + self.user_workspaces_dir = self.base_workspace_dir / "users" def create_user_workspace(self, user_id: str) -> Dict[str, Any]: """Create a complete user workspace with progressive setup.""" try: logger.info(f"Creating workspace for user {user_id}") + # Check if we're in production and skip file operations if needed + if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"): + logger.info("Production environment detected - skipping file workspace creation") + return { + "user_id": user_id, + "workspace_path": "/tmp/alwrity_workspace/users/user_" + user_id, + "config": self._create_user_config(user_id), + "created_at": datetime.utcnow().isoformat(), + "production_mode": True + } + # Create user-specific directories user_dir = self.user_workspaces_dir / f"user_{user_id}" user_dir.mkdir(parents=True, exist_ok=True) diff --git a/backend/start_alwrity_backend.py b/backend/start_alwrity_backend.py index acaed846..cb47511d 100644 --- a/backend/start_alwrity_backend.py +++ b/backend/start_alwrity_backend.py @@ -1,524 +1,25 @@ #!/usr/bin/env python3 """ -ALwrity Backend Server - Comprehensive Startup Script -Handles setup, dependency installation, and server startup. +ALwrity Backend Server - Modular Startup Script +Handles setup, dependency installation, and server startup using modular utilities. Run this from the backend directory to set up and start the FastAPI server. """ import os import sys -import subprocess -import time import argparse from pathlib import Path -def install_requirements(): - """Install required Python packages.""" - print("šŸ“¦ Installing required packages...") - - requirements_file = Path(__file__).parent / "requirements.txt" - - try: - subprocess.check_call([ - sys.executable, "-m", "pip", "install", "-r", str(requirements_file) - ]) - print("[OK] All packages installed successfully!") - return True - except subprocess.CalledProcessError as e: - print(f"[ERROR] Error installing packages: {e}") - return False +# Import modular utilities +from alwrity_utils import ( + DependencyManager, + EnvironmentSetup, + DatabaseSetup, + ProductionOptimizer +) -def create_env_file(): - """Create a .env file with default configuration.""" - env_file = Path(__file__).parent / ".env" - - if env_file.exists(): - print("[INFO] .env file already exists") - return True - - print("šŸ”§ Creating .env file with default configuration...") - - env_content = """# ALwrity Backend Configuration -# 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 - -# Research API Keys (Optional) -# TAVILY_API_KEY=your_tavily_api_key_here -# SERPER_API_KEY=your_serper_api_key_here -# METAPHOR_API_KEY=your_metaphor_api_key_here -# FIRECRAWL_API_KEY=your_firecrawl_api_key_here - -# Server Configuration -HOST=0.0.0.0 -PORT=8000 -DEBUG=true - -# Logging -LOG_LEVEL=INFO -""" - - try: - with open(env_file, 'w') as f: - f.write(env_content) - print("[OK] .env file created successfully!") - return True - except Exception as e: - print(f"[ERROR] Error creating .env file: {e}") - return False - -def setup_monitoring_tables(): - """Set up API monitoring database tables.""" - print("šŸ“Š Setting up API monitoring tables...") - - try: - # Import and run the monitoring table creation - sys.path.append(str(Path(__file__).parent)) - from scripts.create_monitoring_tables import create_monitoring_tables - - if create_monitoring_tables(): - print("[OK] API monitoring tables created successfully!") - return True - else: - print("[WARNING] Warning: Failed to create monitoring tables, continuing anyway...") - return True # Don't fail startup for monitoring issues - - except Exception as e: - print(f"[WARNING] Warning: Could not set up monitoring tables: {e}") - print(" Monitoring will be disabled. Continuing startup...") - return True # Don't fail startup for monitoring issues - -def setup_billing_tables(): - """Set up billing and subscription database tables.""" - print("šŸ’³ Setting up billing and subscription tables...") - - try: - # Import and run the billing table creation - sys.path.append(str(Path(__file__).parent)) - from scripts.create_billing_tables import create_billing_tables, check_existing_tables - from services.database import DATABASE_URL - from sqlalchemy import create_engine - - # Create engine to check existing tables - engine = create_engine(DATABASE_URL, echo=False) - - # Check existing tables - if not check_existing_tables(engine): - print("[OK] Billing tables already exist, skipping creation") - return True - - if create_billing_tables(): - print("[OK] Billing and subscription tables created successfully!") - return True - else: - print("[WARNING] Warning: Failed to create billing tables, continuing anyway...") - return True # Don't fail startup for billing issues - - except Exception as e: - print(f"[WARNING] Warning: Could not set up billing tables: {e}") - print(" Billing system will be disabled. Continuing startup...") - return True # Don't fail startup for billing issues - -def setup_monitoring_middleware(): - """Set up monitoring middleware in app.py if not already present.""" - print("šŸ” Setting up API monitoring middleware...") - - app_file = Path(__file__).parent / "app.py" - - if not app_file.exists(): - print("[WARNING] Warning: app.py not found, skipping middleware setup") - return True - - try: - with open(app_file, 'r') as f: - content = f.read() - - # Check if monitoring middleware is already set up - if "monitoring_middleware" in content: - print("[OK] Monitoring middleware already configured") - return True - - # Add monitoring middleware import and setup - monitoring_import = "from middleware.monitoring_middleware import monitoring_middleware\n" - monitoring_setup = "app.middleware(\"http\")(monitoring_middleware)\n" - - # Find the right place to add the import (after other imports) - lines = content.split('\n') - import_end_index = 0 - - for i, line in enumerate(lines): - if line.strip().startswith('import ') or line.strip().startswith('from '): - import_end_index = i + 1 - elif line.strip() and not line.strip().startswith('#'): - break - - # Insert monitoring import - lines.insert(import_end_index, monitoring_import) - - # Find the right place to add middleware setup (after app creation) - app_creation_index = -1 - for i, line in enumerate(lines): - if 'app = FastAPI(' in line or 'app = FastAPI()' in line: - app_creation_index = i - break - - if app_creation_index != -1: - # Find the end of app configuration - setup_index = app_creation_index + 1 - for i in range(app_creation_index + 1, len(lines)): - if lines[i].strip() and not lines[i].strip().startswith('#'): - setup_index = i + 1 - break - - lines.insert(setup_index, monitoring_setup) - - # Write back to file - with open(app_file, 'w') as f: - f.write('\n'.join(lines)) - - print("[OK] Monitoring middleware configured successfully!") - return True - - except Exception as e: - print(f"[WARNING] Warning: Could not set up monitoring middleware: {e}") - print(" Monitoring will be disabled. Continuing startup...") - return True # Don't fail startup for monitoring issues - -def setup_spacy_model(): - """Set up spaCy English model for linguistic analysis.""" - print("Setting up spaCy English model...") - - try: - import spacy - - # Check if en_core_web_sm model is already installed - model_name = "en_core_web_sm" - - try: - # Try to load the model directly - nlp = spacy.load(model_name) - - # Test the model with a simple sentence - test_doc = nlp("This is a test sentence.") - if test_doc and len(test_doc) > 0: - print(f"SUCCESS: spaCy model '{model_name}' is already installed and working") - print(f" Test: Processed {len(test_doc)} tokens successfully") - return True - else: - raise OSError("Model loaded but not functioning correctly") - - except OSError: - print(f"INFO: spaCy model '{model_name}' not found or not working, downloading...") - - # Try to download the model using subprocess - try: - print(f" Downloading {model_name}...") - result = subprocess.run([ - sys.executable, "-m", "spacy", "download", model_name - ], capture_output=True, text=True, timeout=300) # 5 minute timeout - - if result.returncode == 0: - print(f" SUCCESS: Model download completed") - else: - print(f" WARNING: Download warning: {result.stderr}") - - except subprocess.TimeoutExpired: - print(f" ERROR: Download timed out after 5 minutes") - return False - except subprocess.CalledProcessError as e: - print(f" ERROR: Download failed: {e}") - return False - - # Verify the model was downloaded correctly - try: - nlp = spacy.load(model_name) - - # Test the model - test_doc = nlp("This is a test sentence.") - if test_doc and len(test_doc) > 0: - print(f"SUCCESS: spaCy model '{model_name}' downloaded and verified successfully") - print(f" Test: Processed {len(test_doc)} tokens successfully") - return True - else: - print(f"ERROR: Model downloaded but not functioning correctly") - return False - - except OSError as e: - print(f"ERROR: Model downloaded but failed to load: {e}") - return False - - except subprocess.CalledProcessError as e: - print(f"ERROR: Error downloading spaCy model: {e}") - print(" Manual installation required:") - print(" 1. Install spaCy: pip install spacy>=3.7.0") - print(" 2. Download model: python -m spacy download en_core_web_sm") - print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"") - print(" 4. Restart the backend") - return False - except ImportError as e: - print(f"ERROR: spaCy not installed: {e}") - print(" Manual installation required:") - print(" 1. Install spaCy: pip install spacy>=3.7.0") - print(" 2. Download model: python -m spacy download en_core_web_sm") - print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"") - print(" 4. Restart the backend") - return False - except Exception as e: - print(f"ERROR: Error setting up spaCy model: {e}") - print(" Manual installation required:") - print(" 1. Install spaCy: pip install spacy>=3.7.0") - print(" 2. Download model: python -m spacy download en_core_web_sm") - print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"") - print(" 4. Restart the backend") - return False - -def setup_nltk_data(): - """Set up required NLTK data for linguistic analysis.""" - print("Setting up NLTK data...") - - try: - import nltk - - # Required NLTK data packages - required_data = [ - 'punkt_tab', # Updated for newer NLTK versions - 'stopwords', - 'averaged_perceptron_tagger_eng', # Updated for newer NLTK versions - 'wordnet', - 'omw-1.4' - ] - - for data_package in required_data: - try: - nltk.data.find(f'tokenizers/{data_package}' if data_package in ['punkt', 'punkt_tab'] - else f'corpora/{data_package}' if data_package in ['stopwords', 'wordnet', 'omw-1.4'] - else f'taggers/{data_package}' if data_package in ['averaged_perceptron_tagger', 'averaged_perceptron_tagger_eng'] - else f'corpora/{data_package}') - print(f" SUCCESS: {data_package}") - except LookupError: - print(f" INFO: Downloading {data_package}...") - nltk.download(data_package, quiet=True) - print(f" SUCCESS: {data_package} downloaded") - - print("SUCCESS: All required NLTK data is available") - return True - - except Exception as e: - print(f"ERROR: Error setting up NLTK data: {e}") - return False - -def check_dependencies(): - """Check if required dependencies are installed.""" - print("šŸ” Checking dependencies...") - - required_packages = [ - 'fastapi', - 'uvicorn', - 'pydantic', - 'loguru', - 'openai', - 'google.generativeai', - 'anthropic', - 'mistralai', - 'sqlalchemy', - 'spacy', # Added spaCy to required packages - 'nltk' # Added NLTK to required packages - ] - - missing_packages = [] - - for package in required_packages: - try: - __import__(package.replace('-', '_')) - print(f" [OK] {package}") - except ImportError: - print(f" [ERROR] {package} - MISSING") - missing_packages.append(package) - - if missing_packages: - print(f"\n[ERROR] Missing packages: {', '.join(missing_packages)}") - print("Installing missing packages...") - return install_requirements() - else: - print("\n[OK] All dependencies are available!") - return True - -def setup_environment(): - """Set up the environment for the backend.""" - print("šŸ”§ Setting up environment...") - - # Create necessary directories - directories = [ - "lib/workspace/alwrity_content", - "lib/workspace/alwrity_web_research", - "lib/workspace/alwrity_prompts", - "lib/workspace/alwrity_config" - ] - - for directory in directories: - Path(directory).mkdir(parents=True, exist_ok=True) - print(f" [OK] Created directory: {directory}") - - # Create .env file if it doesn't exist - create_env_file() - - # Set up monitoring - setup_monitoring_tables() - setup_monitoring_middleware() - - # Set up billing and subscription system - setup_billing_tables() - - # Set up persona tables - if setup_persona_tables(): - # Verify persona tables were created successfully - verify_persona_tables() - else: - print("[WARNING] Warning: Persona tables setup failed, but continuing...") - - # Set up linguistic analysis dependencies (Required for persona generation) - print("🧠 Setting up linguistic analysis dependencies...") - - # Set up spaCy model (REQUIRED for persona generation) - if not setup_spacy_model(): - print("[ERROR] CRITICAL: spaCy model setup failed - persona generation will not work!") - print(" Please ensure spaCy is installed and en_core_web_sm model is available") - return False - - # Set up NLTK data (supplementary to spaCy) - if not setup_nltk_data(): - print("[WARNING] Warning: NLTK data setup failed, but continuing...") - - print("[OK] Environment setup complete") - return True - -def setup_persona_tables(): - """Set up persona database tables.""" - print("šŸ”§ Setting up persona tables...") - try: - from services.database import engine - from models.persona_models import Base as PersonaBase - - # Create persona tables - PersonaBase.metadata.create_all(bind=engine) - print("[OK] Persona tables created successfully") - - # Verify tables were created - from sqlalchemy import inspect - inspector = inspect(engine) - tables = inspector.get_table_names() - - persona_tables = [ - 'writing_personas', - 'platform_personas', - 'persona_analysis_results', - 'persona_validation_results' - ] - - created_tables = [table for table in persona_tables if table in tables] - print(f"[OK] Verified persona tables created: {created_tables}") - - if len(created_tables) != len(persona_tables): - missing = [table for table in persona_tables if table not in created_tables] - print(f"[WARNING] Warning: Missing persona tables: {missing}") - return False - - return True - - except Exception as e: - print(f"[ERROR] Error setting up persona tables: {e}") - return False - -def verify_persona_tables(): - """Verify that persona tables exist and are accessible.""" - print("šŸ” Verifying persona tables...") - try: - from services.database import get_db_session - from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult, PersonaValidationResult - - session = get_db_session() - if session: - # Try to query all persona tables to verify they exist - session.query(WritingPersona).first() - session.query(PlatformPersona).first() - session.query(PersonaAnalysisResult).first() - session.query(PersonaValidationResult).first() - session.close() - print("[OK] All persona tables verified successfully") - return True - else: - print("[WARNING] Warning: Could not get database session") - return False - except Exception as e: - print(f"[WARNING] Warning: Could not verify persona tables: {e}") - return False - -def verify_linguistic_analyzer(): - """Verify that the linguistic analyzer is working correctly.""" - print("Verifying linguistic analyzer setup...") - try: - from services.persona.enhanced_linguistic_analyzer import EnhancedLinguisticAnalyzer - - # Try to initialize the linguistic analyzer - analyzer = EnhancedLinguisticAnalyzer() - - # Test with a sample text - test_texts = [ - "This is a test sentence for linguistic analysis.", - "ALwrity provides high-quality AI writing assistance.", - "The persona generation system uses advanced NLP techniques." - ] - - # Perform a simple analysis - analysis_result = analyzer.analyze_writing_style(test_texts) - - if analysis_result and 'basic_metrics' in analysis_result: - print("SUCCESS: Linguistic analyzer verified successfully") - print(f" Analyzed {len(test_texts)} text samples") - print(f" Analysis keys: {list(analysis_result.keys())}") - return True - else: - print("WARNING: Linguistic analyzer returned unexpected result") - print(f" Result: {analysis_result}") - return False - - except Exception as e: - print(f"WARNING: Could not verify linguistic analyzer: {e}") - return False - -def verify_billing_tables(): - """Verify that billing and subscription tables exist and are accessible.""" - print("šŸ” Verifying billing and subscription tables...") - try: - from services.database import get_db_session - from models.subscription_models import ( - SubscriptionPlan, UserSubscription, APIUsageLog, - UsageSummary, APIProviderPricing, UsageAlert - ) - - session = get_db_session() - if session: - # Try to query all billing tables to verify they exist - session.query(SubscriptionPlan).first() - session.query(UserSubscription).first() - session.query(APIUsageLog).first() - session.query(UsageSummary).first() - session.query(APIProviderPricing).first() - session.query(UsageAlert).first() - session.close() - print("[OK] All billing and subscription tables verified successfully") - return True - else: - print("[WARNING] Warning: Could not get database session") - return False - except Exception as e: - print(f"[WARNING] Warning: Could not verify billing tables: {e}") - return False - -def start_backend(enable_reload=False): +def start_backend(enable_reload=False, production_mode=False): """Start the backend server.""" print("šŸš€ Starting ALwrity Backend...") @@ -527,7 +28,7 @@ def start_backend(enable_reload=False): os.environ.setdefault("PORT", "8000") # Set reload based on argument or environment variable - if enable_reload: + if enable_reload and not production_mode: os.environ.setdefault("RELOAD", "true") print(" šŸ”„ Development mode: Auto-reload enabled") else: @@ -553,26 +54,20 @@ def start_backend(enable_reload=False): init_database() print("[OK] Database initialized successfully") - # Verify persona tables exist - verify_persona_tables() - - # Verify linguistic analyzer is working - verify_linguistic_analyzer() - - # Verify billing tables exist - verify_billing_tables() - print("\n🌐 Backend is starting...") print(" šŸ“– API Documentation: http://localhost:8000/api/docs") print(" šŸ” Health Check: http://localhost:8000/health") print(" šŸ“Š ReDoc: http://localhost:8000/api/redoc") - print(" šŸ“ˆ API Monitoring: http://localhost:8000/api/content-planning/monitoring/health") - print(" šŸ’³ Billing Dashboard: http://localhost:8000/api/subscription/plans") - print(" šŸ“Š Usage Tracking: http://localhost:8000/api/subscription/usage/demo") + + if not production_mode: + print(" šŸ“ˆ API Monitoring: http://localhost:8000/api/content-planning/monitoring/health") + print(" šŸ’³ Billing Dashboard: http://localhost:8000/api/subscription/plans") + 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 (default): python start_alwrity_backend.py") + 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) @@ -606,7 +101,8 @@ def start_backend(enable_reload=False): "temp/*", "middleware/*", "models/*", - "scripts/*" + "scripts/*", + "alwrity_utils/*" ], reload_includes=[ "app.py", @@ -624,16 +120,25 @@ def start_backend(enable_reload=False): return True + def main(): """Main function to set up and start the backend.""" # Parse command line arguments parser = argparse.ArgumentParser(description="ALwrity Backend Server") 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)") args = parser.parse_args() + # Determine mode + production_mode = args.production + enable_reload = (args.reload or args.dev) and not production_mode + 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'}") + print("=" * 40) # Check if we're in the right directory if not os.path.exists("app.py"): @@ -642,21 +147,63 @@ def main(): print(" Expected files:", [f for f in os.listdir('.') if f.endswith('.py')]) return False + # Initialize modular components + dependency_manager = DependencyManager() + environment_setup = EnvironmentSetup(production_mode=production_mode) + database_setup = DatabaseSetup(production_mode=production_mode) + production_optimizer = ProductionOptimizer() + + # Apply production optimizations if needed + if production_mode: + if not production_optimizer.apply_production_optimizations(): + print("[ERROR] Production optimization failed") + return False + # Check and install dependencies - if not check_dependencies(): - print("[ERROR] Failed to install dependencies") - return False + critical_ok, missing_critical = dependency_manager.check_critical_dependencies() + if not critical_ok: + print("[ERROR] Critical dependencies missing, installing...") + if not dependency_manager.install_requirements(): + print("[ERROR] Failed to install dependencies") + return False + + # Check optional dependencies (non-critical) + dependency_manager.check_optional_dependencies() # Setup environment - if not setup_environment(): - print("[ERROR] Environment setup failed") + if not environment_setup.setup_directories(): + print("[ERROR] Directory setup failed") return False - # Start backend with reload option - enable_reload = args.reload or args.dev - return start_backend(enable_reload=enable_reload) + if not environment_setup.setup_environment_variables(): + print("[ERROR] Environment variable setup failed") + return False + + # Create .env file only in development + if not production_mode: + environment_setup.create_env_file() + + # Setup database + if not database_setup.setup_essential_tables(): + print("[WARNING] Database setup had issues, continuing...") + + # Setup advanced features only in development + if not production_mode: + database_setup.setup_advanced_tables() + database_setup.verify_tables() + + # Setup linguistic analysis (skip in production) + if not production_mode and not production_optimizer.skip_linguistic_setup(): + if not production_optimizer.skip_spacy_setup(): + dependency_manager.setup_spacy_model() + if not production_optimizer.skip_nltk_setup(): + dependency_manager.setup_nltk_data() + + # Start backend + return start_backend(enable_reload=enable_reload, production_mode=production_mode) + if __name__ == "__main__": success = main() if not success: - sys.exit(1) \ No newline at end of file + sys.exit(1) \ No newline at end of file diff --git a/frontend/env.production.example b/frontend/env.production.example new file mode 100644 index 00000000..c9d68e2b --- /dev/null +++ b/frontend/env.production.example @@ -0,0 +1,11 @@ +# Production Environment Variables for Vercel +# Copy this file to .env.production and update with your actual values + +# Backend API URL (from Railway/Render deployment) +REACT_APP_API_URL=https://your-backend-url.railway.app + +# Environment +REACT_APP_ENVIRONMENT=production + +# Optional: Custom domain for OAuth redirects +# REACT_APP_DOMAIN=your-custom-domain.com diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 78697530..ab20f912 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -7,9 +7,17 @@ export const setAuthTokenGetter = (getter: () => Promise) => { authTokenGetter = getter; }; -// Create a shared axios instance for all API calls (same-origin; CRA proxy forwards to backend) +// Get API URL from environment variables +const getApiUrl = () => { + if (process.env.NODE_ENV === 'production') { + return process.env.REACT_APP_API_URL || 'https://your-backend-url.railway.app'; + } + return ''; // Use proxy in development +}; + +// Create a shared axios instance for all API calls export const apiClient = axios.create({ - baseURL: '', + baseURL: getApiUrl(), timeout: 60000, // Increased to 60 seconds for regular API calls headers: { 'Content-Type': 'application/json', @@ -18,7 +26,7 @@ export const apiClient = axios.create({ // Create a specialized client for AI operations with extended timeout export const aiApiClient = axios.create({ - baseURL: '', + baseURL: getApiUrl(), timeout: 180000, // 3 minutes timeout for AI operations (matching 20-25 second responses) headers: { 'Content-Type': 'application/json', @@ -27,7 +35,7 @@ export const aiApiClient = axios.create({ // Create a specialized client for long-running operations like SEO analysis export const longRunningApiClient = axios.create({ - baseURL: '', + baseURL: getApiUrl(), timeout: 300000, // 5 minutes timeout for SEO analysis headers: { 'Content-Type': 'application/json', @@ -36,7 +44,7 @@ export const longRunningApiClient = axios.create({ // Create a specialized client for polling operations with reasonable timeout export const pollingApiClient = axios.create({ - baseURL: '', + baseURL: getApiUrl(), timeout: 60000, // 60 seconds timeout for polling status checks headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/hooks/useWixConnection.ts b/frontend/src/hooks/useWixConnection.ts index e51c7a75..babf8760 100644 --- a/frontend/src/hooks/useWixConnection.ts +++ b/frontend/src/hooks/useWixConnection.ts @@ -42,53 +42,18 @@ export const useWixConnection = () => { if (connectedFlag && tokensRaw) { const tokens = JSON.parse(tokensRaw); - // Try to get actual site information from Wix API - try { - const { createClient, OAuthStrategy } = await import('@wix/sdk'); - const wixClient = createClient({ - auth: OAuthStrategy({ clientId: '75d88e36-1c76-4009-b769-15f4654556df' }) - }); - wixClient.auth.setTokens(tokens); - - // Get member info to extract site URL - const memberInfo = await wixClient.auth.getMemberInfo(); - console.log('Wix member info:', memberInfo); - - // Try to extract site URL from member info or use a default - let siteUrl = 'Connected Wix Site'; - if (memberInfo?.member?.email) { - // Extract domain from email or use email as identifier - const email = memberInfo.member.email; - const domain = email.split('@')[1]; - siteUrl = `https://${domain}`; - } - - setStatus({ - connected: true, - sites: [{ - id: 'wix-site-1', - blog_url: siteUrl, - blog_id: 'wix-blog', - created_at: new Date().toISOString(), - scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE' - }], - total_sites: 1 - }); - } catch (apiError) { - console.log('Wix API error, using fallback:', apiError); - // Fallback if API call fails - setStatus({ - connected: true, - sites: [{ - id: 'wix-site-1', - blog_url: 'Connected Wix Site', - blog_id: 'wix-blog', - created_at: new Date().toISOString(), - scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE' - }], - total_sites: 1 - }); - } + // Set connected status with site information from tokens + setStatus({ + connected: true, + sites: [{ + id: 'wix-site-1', + blog_url: 'Connected Wix Site', + blog_id: 'wix-blog', + created_at: new Date().toISOString(), + scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE' + }], + total_sites: 1 + }); console.log('Wix status checked: connected via sessionStorage'); } else { diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..566c9691 --- /dev/null +++ b/vercel.json @@ -0,0 +1,21 @@ +{ + "version": 2, + "builds": [ + { + "src": "frontend/package.json", + "use": "@vercel/static-build", + "config": { + "distDir": "build" + } + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "/frontend/$1" + } + ], + "env": { + "REACT_APP_ENVIRONMENT": "production" + } +}