from fastapi import FastAPI, HTTPException, Response, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel, validator import requests import os import base64 import json from typing import Optional, List import random import time from datetime import datetime import psutil import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def update_deployment_status(repo_owner, repo_name, workflow_run_id, status, conclusion=None, workflow_url=None): """Forward deployment status to VistaPanel Backend API""" try: payload = { 'repo_owner': repo_owner, 'repo_name': repo_name, 'workflow_run_id': workflow_run_id, 'status': status, 'conclusion': conclusion, 'workflow_url': workflow_url } # The VistaPanel endpoint URL api_url = os.getenv('CELESTINE_API_URL', 'https://celestine.indevs.in/api/webhook-receiver.php') secret = os.getenv('WEBHOOK_SECRET', 'savage') headers = { 'X-Celestine-Secret': secret, 'Content-Type': 'application/json', 'User-Agent': 'Celestine-HF-Space/1.0' } logger.info(f"๐Ÿ“ค Forwarding to VistaPanel: {api_url}") logger.info(f" Payload: {json.dumps(payload, indent=2)}") # Increase timeout and add retry logic response = requests.post( api_url, json=payload, headers=headers, timeout=15, allow_redirects=True ) logger.info(f" Response Status: {response.status_code}") logger.info(f" Response Body: {response.text[:200]}") if response.status_code == 200: logger.info(f"โœ“ Forwarded status to Celestine: {repo_owner}/{repo_name} -> {status}") return True else: logger.error(f"Failed to forward status: HTTP {response.status_code} - {response.text}") return False except requests.exceptions.Timeout: logger.error(f"Webhook forwarding timeout after 15s") return False except requests.exceptions.ConnectionError as e: logger.error(f"Webhook forwarding connection error: {e}") return False except Exception as e: logger.error(f"Webhook forwarding error: {e}") return False app = FastAPI(title="MOFH API Proxy + IONA AI") # Track uptime START_TIME = time.time() REQUEST_COUNT = {"total": 0, "generate": 0, "create": 0, "errors": 0} # High-quality image library for professional websites WEBSITE_IMAGES = [ "https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1497215728101-856f4ea42174?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1542744094-24638eff58bb?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1531482615713-2afd69097998?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=1920&h=1080", "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&q=80&w=1920&h=1080" ] def get_random_images(count=10): """Get random images from the library""" return random.sample(WEBSITE_IMAGES, min(count, len(WEBSITE_IMAGES))) MOFH_API_URL = "https://panel.myownfreehost.net:2087/xml-api/" MOFH_API_USERNAME = "ZqHK1H5BBnrrywnsXXW2pY42QNqDxHru74HtwZofkn1LRawd4TDcKCPnIViBiPaJg1sUXUlKErn4EhXmqu0vx1STF263TxiTQuyKzYcErjSax5W4lBoLXqpE0aQqnLLKi8Pk7fncTbcfwqnKVRNOzLd34xdakdiHH1Z4gmDbetSmYauCId4Cj8SVizSBTuN6TMIrz2oMKAoXPypSocVOOUI0rSOIld34MY6yXtcHdSgh3Jd5J8mgM8SiW8BvHtE" MOFH_API_PASSWORD = "Uj39SyF5okKP73Ml6laLEay40MZEXSe81lStBEc9dQC2odK49Tw4K5l4BXI3DfnIVA5ZoIH0GFKvBRBO1cLIU1djjSBCABBkp2GO2oeH4BROw238dZwODu0kklR0uezHPfezHMCbcbjSKLRKJX5Ns7gZ9wgXxZbO3e8Rf4MoZnQVk5IUNLvKPWHDP43PsQnAFAYOhDNnKPBBg2CoHXsJ1Kq8C5wRuBd64GPyPhY7sbSCibAMUWKRkUSzRi7aq5i" # IONA AI - Multiple API Keys for Load Balancing (No Rate Limits) GROQ_API_KEYS = [ "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", # Replace with real keys "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", "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", "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" ] # Model Configuration - Use ONLY the most capable model AI_MODELS = { "groq": [ "llama-3.3-70b-versatile", # Most capable, reliable for complex generation ] } # Track API usage for rotation api_usage_counter = {key: 0 for key in GROQ_API_KEYS} def get_next_api_key(): """Round-robin API key selection for load balancing""" min_usage_key = min(api_usage_counter, key=api_usage_counter.get) api_usage_counter[min_usage_key] += 1 return min_usage_key def get_random_model(): """Select model (always use 70B for reliability)""" return AI_MODELS["groq"][0] # Always use llama-3.3-70b-versatile class AIWebsiteRequest(BaseModel): prompt: str business_type: Optional[str] = "general" color_scheme: Optional[str] = "modern" include_database: Optional[bool] = True php_only: Optional[bool] = False features: Optional[List[str]] = [] @validator('prompt') def validate_prompt(cls, v): if not v or len(v.strip()) < 10: raise ValueError('Prompt must be at least 10 characters') if len(v) > 10000: raise ValueError('Prompt too long (max 10000 characters)') return v.strip() class AccountRequest(BaseModel): username: str password: str email: str domain: str plan: str = "free" @validator('username') def validate_username(cls, v): if not v or len(v) < 3: raise ValueError('Username must be at least 3 characters') if len(v) > 20: raise ValueError('Username too long (max 20 characters)') return v.strip() @validator('email') def validate_email(cls, v): if '@' not in v or '.' not in v: raise ValueError('Invalid email format') return v.strip() @app.get("/", response_class=HTMLResponse) def root(): """Uptime dashboard with external IP display""" import socket hostname = socket.gethostname() local_ip = socket.gethostbyname(hostname) # Get external IP try: external_ip = requests.get('https://api.ipify.org', timeout=5).text except: external_ip = "Unable to detect" # Calculate uptime uptime_seconds = int(time.time() - START_TIME) uptime_hours = uptime_seconds // 3600 uptime_minutes = (uptime_seconds % 3600) // 60 uptime_secs = uptime_seconds % 60 # Get system stats try: cpu_percent = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() memory_percent = memory.percent memory_used = memory.used / (1024**3) # GB memory_total = memory.total / (1024**3) # GB except: cpu_percent = 0 memory_percent = 0 memory_used = 0 memory_total = 0 # API key status api_key_usage = [] for key, count in api_usage_counter.items(): api_key_usage.append(f"{key[:20]}... โ†’ {count} requests") html_content = f""" IONA AI - System Status

Celestine Hosting Backend

Intelligent Website Generator & MOFH API Proxy

๐ŸŸข ONLINE

โฑ๏ธ Uptime

Running Since {datetime.fromtimestamp(START_TIME).strftime('%Y-%m-%d %H:%M:%S')}
Uptime {uptime_hours}h {uptime_minutes}m {uptime_secs}s
Total Requests {REQUEST_COUNT['total']}
Errors {REQUEST_COUNT['errors']}

๐ŸŒ Network Info

Hostname {hostname}
Local IP {local_ip}
External IP Address
{external_ip}
โš ๏ธ Add this IP to MOFH API whitelist

๐Ÿ’ป System Resources

CPU Usage {cpu_percent:.1f}%
{cpu_percent:.1f}%
Memory Usage {memory_percent:.1f}%
{memory_percent:.1f}%
Memory {memory_used:.2f} GB / {memory_total:.2f} GB

๐Ÿค– AI Configuration

Active Models {len(AI_MODELS['groq'])}
API Keys {len(GROQ_API_KEYS)}
Generations {REQUEST_COUNT['generate']}
{''.join([f'
{usage}
' for usage in api_key_usage])}

๐Ÿ“ก API Endpoints

  • GET /
    System status dashboard
  • POST /generate-website
    Generate website with IONA AI
  • POST /create
    Create MOFH hosting account
  • GET /health
    Health check endpoint
  • GET /test
    Test AI generation

๐Ÿ‘จโ€๐Ÿ’ป Developer Info

Developer Pratyush Srivastava
GitHub @pratyush
Portfolio pratyush.dev
Version 2026.1.0
""" return html_content @app.get("/health") def health_check(): """Health check endpoint for monitoring""" return { "status": "healthy", "uptime_seconds": int(time.time() - START_TIME), "timestamp": datetime.now().isoformat(), "requests": REQUEST_COUNT } @app.get("/test") def test_generation(): """Test AI generation with a simple prompt""" try: api_key = get_next_api_key() model = get_random_model() test_prompt = "Create a simple HTML page with a heading 'Hello World' and a paragraph." headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": model, "messages": [ {"role": "system", "content": "You are a helpful assistant. Return only valid JSON."}, {"role": "user", "content": test_prompt} ], "temperature": 0.7, "max_tokens": 500 } response = requests.post( "https://api.groq.com/openai/v1/chat/completions", headers=headers, json=payload, timeout=30 ) if response.status_code == 200: result = response.json() return { "success": True, "model": model, "response_length": len(result['choices'][0]['message']['content']), "tokens_used": result.get('usage', {}), "message": "AI is working correctly!" } else: return { "success": False, "error": f"HTTP {response.status_code}", "message": "AI test failed" } except Exception as e: return { "success": False, "error": str(e), "message": "AI test failed with exception" } @app.post("/generate-website") def generate_website(req: AIWebsiteRequest): """IONA AI - Generate complete website with HTML, CSS, JS, and MySQL""" REQUEST_COUNT["total"] += 1 REQUEST_COUNT["generate"] += 1 try: logger.info("=== IONA AI Generation Started ===") logger.info(f"Business Type: {req.business_type}") logger.info(f"Prompt Length: {len(req.prompt)} chars") logger.info(f"PHP Only Mode: {req.php_only}") api_key = get_next_api_key() model = get_random_model() logger.info(f"Model: {model}, API Key: {api_key[:20]}...") # Get random images for this website selected_images = get_random_images(15) images_list = '\n'.join([f"- {img}" for img in selected_images]) # Check if PHP-only mode if req.php_only: # PHP-only generation prompt system_prompt = f"""You are IONA AI, an expert PHP backend developer. ๐ŸŽฏ HOSTING ENVIRONMENT: VistaPanel (Free Hosting) - PHP Version: 7.4 or 8.0 - MySQL Database available - PDO and MySQLi supported ๐ŸŽฏ MISSION: Generate ONLY PHP BACKEND FILES (NO HTML/CSS/JS). ๐Ÿ“ฆ REQUIRED OUTPUT FORMAT: Return ONLY a valid JSON object with these keys: {{ "contact_php": "Complete contact form handler with validation and email sending", "api_php": "RESTful API endpoints with JSON responses", "functions_php": "Helper functions for database, validation, sanitization", "config": "Database configuration with PDO connection", "sql": "MySQL schema with sample data", "readme": "Setup guide for VistaPanel" }} ๐Ÿ˜ PHP FILE REQUIREMENTS: 1. **contact_php** (Contact Form Handler): - Handle POST requests - Validate: name, email, phone, message - Sanitize all inputs (htmlspecialchars, filter_var) - Send email using mail() function - Store in database - Return JSON response - CSRF protection - Rate limiting (session-based) - Error handling 2. **api_php** (API Endpoints): - RESTful structure - GET /api.php?action=list (list items) - POST /api.php?action=create (create item) - PUT /api.php?action=update (update item) - DELETE /api.php?action=delete (delete item) - JSON responses - Authentication (if needed) - Input validation - Error handling 3. **functions_php** (Helper Functions): - Database query helpers (select, insert, update, delete) - Input validation functions - Sanitization functions - Email sending function - Authentication helpers - Session management - Error logging 4. **config** (Database Configuration): - PDO connection setup - Database credentials (localhost, username, password, dbname) - Error handling (try-catch) - Timezone settings - Error reporting settings - Session configuration 5. **sql** (MySQL Schema): - CREATE DATABASE statement - CREATE TABLE statements - Proper data types (VARCHAR, INT, TEXT, DATETIME) - Primary keys (AUTO_INCREMENT) - Foreign keys and indexes - Timestamps (created_at, updated_at) - Sample data (5-10 realistic entries) PHP SECURITY REQUIREMENTS: - Use prepared statements (PDO) for ALL database queries - Sanitize ALL user inputs - Validate email addresses with filter_var() - Use password_hash() for passwords - Implement CSRF tokens - Use htmlspecialchars() for output - Set proper error reporting - Use sessions securely PHP BEST PRACTICES: - PSR-12 coding standards - Proper indentation (4 spaces) - Clear variable names - Comments for complex logic - Separate concerns - Return JSON for AJAX requests - Use HTTP status codes - Error handling (try-catch) CODE FORMATTING: - Proper indentation (4 spaces per level) - Blank lines between functions - Comments for each function - Use \n for newlines in JSON strings IMPORTANT JSON RULES: - Return ONLY the JSON object - NO markdown code blocks - NO explanations - Escape quotes with backslash - Use \n for newlines - Do NOT return null values - Generate COMPLETE, FUNCTIONAL code CREATE PROFESSIONAL PHP BACKEND FOR VISTAPANEL! ๐Ÿš€""" user_prompt = f"""Generate PHP backend files for: {req.prompt} Requirements: - Complete contact form handler - RESTful API endpoints - Helper functions - Database configuration - MySQL schema with sample data Return as JSON with contact_php, api_php, functions_php, config, sql, and readme keys.""" else: # Full website generation prompt system_prompt = f"""You are IONA AI, an elite full-stack web developer with 15+ years of experience. ๐ŸŽฏ HOSTING ENVIRONMENT: VistaPanel (Free Hosting) - PHP Version: 7.4 or 8.0 - MySQL Database available - File Manager: VistaPanel - No SSH access - Standard PHP functions available - PDO and MySQLi supported ๐ŸŽฏ MISSION: Generate COMPLETE, PRODUCTION-READY, MULTI-FILE websites with REAL CONTENT. ๐Ÿ“ฆ REQUIRED OUTPUT FORMAT: Return ONLY a valid JSON object with these keys: {{ "html": "Complete HTML5 document with PROPER INDENTATION (2 spaces per level)", "css": "Complete CSS with PROPER FORMATTING and line breaks", "js": "Complete JavaScript with PROPER FORMATTING", "php": "PHP backend files (contact.php, api.php, etc.) with PROPER FORMATTING", "sql": "MySQL schema with sample data (if database needed)", "config": "PHP config file with database connection (if database needed)", "readme": "Setup and customization guide for VistaPanel" }} ๐Ÿ–ผ๏ธ USE THESE REAL IMAGES (randomly selected for this website): {images_list} IMPORTANT IMAGE USAGE: - Use different images for hero, gallery, team, testimonials, blog posts - Add proper alt text describing each image - Use responsive image techniques (srcset if needed) - Images are high-quality 1920x1080, perfect for hero sections โœจ CODE FORMATTING RULES (CRITICAL): 1. HTML: Proper indentation with 2 spaces per level 2. CSS: One rule per line, organized by sections with comments 3. JavaScript: Proper function formatting with line breaks 4. PHP: PSR-12 coding standards, proper indentation 5. NO MINIFIED CODE - Make it readable and maintainable 6. Add blank lines between major sections 7. Use \n for newlines in JSON strings 8. Properly indent nested elements ๐Ÿ˜ PHP FILE GENERATION (IMPORTANT): When generating PHP files, create COMPLETE, FUNCTIONAL code: 1. **contact.php** (Contact Form Handler): - Validate all inputs (name, email, message) - Sanitize data (htmlspecialchars, filter_var) - Send email using mail() function - Store in database if requested - Return JSON response - Include CSRF protection - Rate limiting (session-based) 2. **config.php** (Database Configuration): - PDO connection setup - Error handling (try-catch) - Database credentials (localhost, username, password, dbname) - Timezone settings - Error reporting settings - Session configuration 3. **functions.php** (Helper Functions): - Database query helpers - Input validation functions - Sanitization functions - Email sending function - Authentication helpers (if needed) 4. **api.php** (API Endpoints): - RESTful API structure - JSON responses - Error handling - Input validation - CORS headers (if needed) PHP SECURITY REQUIREMENTS: - Use prepared statements (PDO) for all database queries - Sanitize ALL user inputs - Validate email addresses with filter_var() - Use password_hash() for passwords - Implement CSRF tokens - Use htmlspecialchars() for output - Set proper error reporting - Use sessions securely PHP BEST PRACTICES: - PSR-12 coding standards - Proper error handling (try-catch) - Clear variable names - Comments for complex logic - Separate concerns (config, functions, handlers) - Return JSON for AJAX requests - Use HTTP status codes Example HTML formatting: ``` \n\n\n \n Page\n\n\n
\n \n
\n\n ``` Example PHP formatting: ``` false, 'error' => 'Required fields missing']);\n exit;\n }}\n \n // Process form\n echo json_encode(['success' => true, 'message' => 'Form submitted']);\n}}\n?> ``` ๐Ÿ“ CONTENT REQUIREMENTS: 1. Write REAL, PROFESSIONAL content (not Lorem Ipsum) 2. Create compelling headlines and descriptions 3. Write realistic testimonials with names 4. Generate actual blog post content 5. Create detailed service/product descriptions 6. Write engaging About Us content 7. Include realistic contact information (use placeholders) ๐ŸŽจ DESIGN EXCELLENCE: - Modern, professional, visually stunning - Consistent spacing (8px grid system) - Smooth animations (0.3s transitions) - Hover effects on interactive elements - Mobile-first responsive design - Glassmorphism, gradients, shadows - Professional color palette - Clean typography (2-3 fonts max) ๐Ÿ”ง TECHNICAL REQUIREMENTS: - Semantic HTML5 (header, nav, main, section, article, footer) - CSS Grid + Flexbox layouts - Vanilla JavaScript (ES6+) - Responsive breakpoints: 320px, 768px, 1024px, 1440px - Form validation with error messages - Smooth scroll behavior - Loading animations - Interactive elements (accordions, tabs, modals) ๐Ÿ“ฑ MUST INCLUDE SECTIONS: 1. Hero section with CTA button 2. Navigation (sticky on scroll, mobile menu) 3. About/Services section 4. Features/Benefits cards 5. Gallery/Portfolio (if applicable) 6. Testimonials (if applicable) 7. Pricing tables (if applicable) 8. Team members (if applicable) 9. Blog posts grid (if applicable) 10. Contact form with validation 11. Footer with social links 12. Back-to-top button ๐Ÿ—„๏ธ DATABASE (if requested): - Normalized MySQL schema - Proper indexes and foreign keys - Sample data (5-10 realistic entries) - PHP config with PDO connection - CREATE DATABASE statement - CREATE TABLE statements - INSERT sample data - Proper data types (VARCHAR, INT, TEXT, DATETIME) - Auto-increment primary keys - Timestamps (created_at, updated_at) โšก INTERACTIVE FEATURES: - Smooth scroll to sections - Fade-in animations on scroll - Form validation (real-time) - AJAX form submission (with PHP backend) - Image lightbox/modal - Mobile hamburger menu - Loading spinner - Success/error messages - Accordion/FAQ - Tabs for content - Carousel/slider (if needed) ๐Ÿš€ PERFORMANCE: - Optimized CSS (no unused styles) - Efficient JavaScript - Lazy loading for images - Minimal HTTP requests - Fast animations ๐Ÿ“Š SEO & ACCESSIBILITY: - Proper meta tags (title, description, keywords) - Open Graph tags - Semantic HTML structure - Alt text for all images - ARIA labels where needed - Keyboard navigation - Focus indicators - Sufficient color contrast ๐Ÿ“‹ VISTAPANEL SETUP INSTRUCTIONS (in README): 1. Upload files via File Manager 2. Create MySQL database in VistaPanel 3. Import SQL file using phpMyAdmin 4. Update config.php with database credentials 5. Set file permissions (755 for directories, 644 for files) 6. Test contact form 7. Customize content and images IMPORTANT JSON RULES: - Return ONLY the JSON object - NO markdown code blocks - NO explanations before or after - Escape quotes with backslash - Use \n for newlines - Ensure valid JSON syntax - Do NOT return null values - Generate COMPLETE, FUNCTIONAL code for ALL files CREATE PROFESSIONAL, PRODUCTION-READY CODE FOR VISTAPANEL! ๐Ÿš€""" # Enhanced user prompt with detailed specifications features_text = ', '.join(req.features) if req.features else 'Standard features' color_guide = _get_color_scheme_guide(req.color_scheme) user_prompt = f"""๐ŸŽจ PROJECT BRIEF: ๐Ÿ“‹ WEBSITE TYPE: {req.business_type.upper()} ๐Ÿ’ฌ CLIENT DESCRIPTION: {req.prompt} ๐ŸŽจ DESIGN SPECIFICATIONS: - Color Scheme: {req.color_scheme} - Style: Modern, professional, visually stunning - Layout: Clean, spacious, well-organized - Typography: Professional, readable, hierarchical โœจ REQUIRED FEATURES: {features_text} ๐Ÿ—„๏ธ DATABASE: {'YES - Include full MySQL schema with sample data' if req.include_database else 'NO - Static website only'} ๐ŸŽฏ SPECIFIC REQUIREMENTS: 1. HERO SECTION: - Eye-catching headline - Compelling subheadline - Clear call-to-action button - Background: gradient or image - Smooth scroll-down indicator 2. NAVIGATION: - Sticky header on scroll - Smooth scroll to sections - Mobile hamburger menu - Active section highlighting 3. CONTENT SECTIONS: - About/Services section with icons - Features/Benefits with cards - Testimonials with star ratings (if requested) - Gallery with lightbox (if requested) - Pricing tables (if requested) - Team members (if requested) - Blog posts grid (if requested) - FAQ accordion (if applicable) 4. CONTACT SECTION: - Working contact form with validation - Email, phone, address display - Social media links - Google Maps embed (placeholder) 5. FOOTER: - Multi-column layout - Quick links - Social media icons - Copyright notice - Back-to-top button 6. INTERACTIVE ELEMENTS: - Smooth scroll animations (fade-in, slide-up) - Hover effects on cards and buttons - Loading states for forms - Success/error messages - Image lazy loading - Parallax effects (subtle) 7. FORMS (if applicable): - Real-time validation - Clear error messages - Success confirmation - Spam protection (honeypot) - Required field indicators 8. PERFORMANCE: - Optimized CSS (no unused styles) - Efficient JavaScript (no jQuery) - Fast loading animations - Minimal HTTP requests 9. SEO: - Proper meta tags (title, description, keywords) - Open Graph tags for social sharing - Structured data (JSON-LD) - Semantic HTML structure - Alt text for all images 10. ACCESSIBILITY: - ARIA labels where needed - Keyboard navigation support - Focus indicators - Sufficient color contrast - Screen reader friendly {f'''11. DATABASE STRUCTURE (if applicable): - Users table (id, name, email, password, created_at) - Content table (id, title, description, image, created_at) - Messages/Contacts table (id, name, email, message, created_at) - Categories/Tags (if blog) - Proper relationships and indexes - Sample data for testing''' if req.include_database else ''} ๐ŸŽจ COLOR SCHEME GUIDANCE: {color_guide} ๐Ÿ“ฑ RESPONSIVE BREAKPOINTS: - Mobile: 320px - 767px (single column, stacked layout) - Tablet: 768px - 1023px (2 columns where appropriate) - Desktop: 1024px+ (full layout, max-width 1400px) โšก MUST INCLUDE: - Favicon link (placeholder) - Google Fonts (1-2 professional fonts) - Font Awesome icons (CDN) - Smooth scroll behavior - Loading animation on page load - 404 error handling - Print stylesheet basics ๐Ÿšซ AVOID: - jQuery or heavy frameworks - Inline styles (except critical CSS) - !important in CSS (unless necessary) - Console.log in production code - Hardcoded sensitive data - Broken links or placeholder text ๐Ÿ“ฆ DELIVERABLES: Return ONLY valid JSON with these exact keys: - html: Complete HTML5 document - css: Complete stylesheet with comments - js: Complete JavaScript with error handling - sql: MySQL schema with sample data (if database requested) - config: PHP database config (if database requested) - readme: Detailed setup and customization guide CREATE A WEBSITE THAT WILL IMPRESS AND DELIGHT! ๐Ÿš€โœจ""" logger.info(f"Prompt sizes - System: {len(system_prompt)}, User: {len(user_prompt)}") # Call Groq API headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], "temperature": 0.7, "max_tokens": 8000, "top_p": 0.9 } logger.info("Calling Groq API...") response = requests.post( "https://api.groq.com/openai/v1/chat/completions", headers=headers, json=payload, timeout=60 ) logger.info(f"Groq Response: {response.status_code}") if response.status_code != 200: logger.error(f"Groq Error: {response.text}") raise HTTPException(status_code=response.status_code, detail=response.text) result = response.json() raw_content = result['choices'][0]['message']['content'] logger.info(f"Response length: {len(raw_content)} chars") # Try to extract JSON from markdown code blocks if present if '```json' in raw_content: import re json_match = re.search(r'```json\s*(.+?)\s*```', raw_content, re.DOTALL) if json_match: raw_content = json_match.group(1) logger.info("Extracted JSON from markdown block") elif '```' in raw_content: import re json_match = re.search(r'```\s*(.+?)\s*```', raw_content, re.DOTALL) if json_match: raw_content = json_match.group(1) logger.info("Extracted content from code block") logger.info(f"Preview: {raw_content[:200]}...") try: generated_content = json.loads(raw_content) # Validate that we have the expected structure if not isinstance(generated_content, dict): raise ValueError("Response is not a JSON object") # Check if we have the required files required_keys = ['html', 'css', 'js'] has_required = any(key in generated_content for key in required_keys) if not has_required: # Check if the response is double-encoded (JSON string containing JSON) if len(generated_content) == 1: first_key = list(generated_content.keys())[0] first_value = generated_content[first_key] if isinstance(first_value, str) and first_value.strip().startswith('{'): try: inner_json = json.loads(first_value) if isinstance(inner_json, dict) and any(k in inner_json for k in required_keys): generated_content = inner_json logger.info("Unwrapped double-encoded JSON") except: pass # Final validation if 'html' not in generated_content: logger.warning("No HTML in response, using fallback") raise ValueError("Missing HTML content") logger.info(f"Files: {list(generated_content.keys())}") except (json.JSONDecodeError, ValueError) as je: logger.error(f"JSON parse failed: {str(je)}") # Try to fix common JSON issues try: # Remove markdown code blocks if still present if '```' in raw_content: parts = raw_content.split('```') for part in parts: if '{' in part and '}' in part: raw_content = part.replace('json\n', '').replace('json', '').strip() break # Try parsing again generated_content = json.loads(raw_content) # Validate again if not isinstance(generated_content, dict) or 'html' not in generated_content: raise ValueError("Invalid structure after cleanup") logger.info("Fixed JSON after cleanup") logger.info(f"Files: {list(generated_content.keys())}") except Exception as cleanup_error: # Final fallback: create minimal structure only if we have HTML-like content logger.warning(f"Using fallback structure: {str(cleanup_error)}") if ' str: """Return detailed color guidance based on selected scheme""" schemes = { "modern": """Primary: #6366f1 (Indigo), Secondary: #8b5cf6 (Purple) Accent: #ec4899 (Pink), Background: #0f172a (Dark Blue) Text: #f1f5f9 (Light Gray), Use gradients and glassmorphism""", "dark": """Primary: #3b82f6 (Blue), Secondary: #10b981 (Green) Accent: #f59e0b (Amber), Background: #111827 (Very Dark) Text: #f9fafb (White), High contrast, neon accents""", "light": """Primary: #2563eb (Blue), Secondary: #7c3aed (Purple) Accent: #dc2626 (Red), Background: #ffffff (White) Text: #1f2937 (Dark Gray), Clean, minimal, lots of whitespace""", "vibrant": """Primary: #f43f5e (Rose), Secondary: #8b5cf6 (Purple) Accent: #f59e0b (Amber), Background: #1e293b (Dark Slate) Text: #f1f5f9 (Light), Bold colors, high energy""", "professional": """Primary: #1e40af (Navy), Secondary: #475569 (Slate) Accent: #0891b2 (Cyan), Background: #f8fafc (Off White) Text: #0f172a (Dark), Corporate, trustworthy""", "warm": """Primary: #ea580c (Orange), Secondary: #dc2626 (Red) Accent: #facc15 (Yellow), Background: #292524 (Warm Dark) Text: #fafaf9 (Warm White), Cozy, inviting""", "nature": """Primary: #16a34a (Green), Secondary: #65a30d (Lime) Accent: #0891b2 (Teal), Background: #1c1917 (Earth Dark) Text: #fafaf9 (Natural White), Organic, earthy""" } return schemes.get(color_scheme, schemes["modern"]) @app.post("/create") def create_account(req: AccountRequest): """Create MOFH hosting account""" REQUEST_COUNT["total"] += 1 REQUEST_COUNT["create"] += 1 try: # Use HTTP Basic Authentication (the correct method) auth_string = f"{MOFH_API_USERNAME}:{MOFH_API_PASSWORD}" auth_b64 = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8') headers = { 'Authorization': f'Basic {auth_b64}', 'Content-Type': 'application/x-www-form-urlencoded' } # Data WITHOUT api_user/api_key (they're in the header) api_data = { 'username': req.username, 'password': req.password, 'contactemail': req.email, 'domain': req.domain, 'plan': req.plan } response = requests.post( f"{MOFH_API_URL}createacct.php", data=api_data, headers=headers, timeout=30, verify=False ) return { 'success': response.status_code == 200, 'http_code': response.status_code, 'response': response.text, 'debug': { 'api_url': f"{MOFH_API_URL}createacct.php", 'auth_method': 'Basic Auth (HTTP Header)', 'api_user_length': len(MOFH_API_USERNAME), 'api_key_length': len(MOFH_API_PASSWORD), 'sent_params': list(api_data.keys()) } } except Exception as e: REQUEST_COUNT["errors"] += 1 raise HTTPException(status_code=500, detail=str(e)) class FTPDeployRequest(BaseModel): ftp_host: str ftp_user: str ftp_pass: str files: dict # {"index.html": "content", "style.css": "content", ...} @validator('ftp_host') def validate_host(cls, v): if not v or len(v) < 3: raise ValueError('Invalid FTP host') return v.strip() @app.post("/deploy-ftp") def deploy_via_ftp(req: FTPDeployRequest): """Deploy files to FTP server (HF Space has no firewall restrictions)""" REQUEST_COUNT["total"] += 1 try: import ftplib from io import BytesIO logger.info(f"[FTP-DEPLOY] Connecting to {req.ftp_host}") deployed_files = [] errors = [] # Try regular FTP first ftp = None try: ftp = ftplib.FTP(timeout=30) ftp.connect(req.ftp_host, 21) ftp.login(req.ftp_user, req.ftp_pass) logger.info("[FTP-DEPLOY] Connected via regular FTP") except Exception as e: logger.warning(f"[FTP-DEPLOY] Regular FTP failed: {e}") # Try FTP_TLS try: ftp = ftplib.FTP_TLS(timeout=30) ftp.connect(req.ftp_host, 21) ftp.login(req.ftp_user, req.ftp_pass) ftp.prot_p() # Enable encryption logger.info("[FTP-DEPLOY] Connected via FTP_TLS") except Exception as e2: logger.error(f"[FTP-DEPLOY] FTP_TLS also failed: {e2}") raise HTTPException(status_code=500, detail=f"FTP connection failed: {str(e2)}") # Set passive mode ftp.set_pasv(True) # Try to change to web directory web_dirs = ['htdocs', 'public_html', 'www', 'html'] current_dir = '/' for web_dir in web_dirs: try: ftp.cwd(web_dir) current_dir = ftp.pwd() logger.info(f"[FTP-DEPLOY] Changed to directory: {current_dir}") break except: continue logger.info(f"[FTP-DEPLOY] Working directory: {current_dir}") # Upload each file for filename, content in req.files.items(): try: logger.info(f"[FTP-DEPLOY] Uploading {filename} ({len(content)} bytes)") # Convert string to bytes file_bytes = BytesIO(content.encode('utf-8')) # Upload file ftp.storbinary(f'STOR {filename}', file_bytes) deployed_files.append(filename) logger.info(f"[FTP-DEPLOY] โœ“ {filename} uploaded") except Exception as e: error_msg = f"{filename}: {str(e)}" errors.append(error_msg) logger.error(f"[FTP-DEPLOY] โœ— {error_msg}") # Close FTP connection try: ftp.quit() except: ftp.close() logger.info(f"[FTP-DEPLOY] Deployment complete. Success: {len(deployed_files)}, Errors: {len(errors)}") if not deployed_files: raise HTTPException(status_code=500, detail=f"No files deployed. Errors: {', '.join(errors)}") return { "success": True, "deployed_files": deployed_files, "errors": errors, "message": f"Successfully deployed {len(deployed_files)} file(s)" } except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"[FTP-DEPLOY] Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Deployment failed: {str(e)}") class GenerateAndDeployRequest(BaseModel): prompt: str business_type: Optional[str] = "general" color_scheme: Optional[str] = "modern" include_database: Optional[bool] = False features: Optional[List[str]] = [] ftp_host: str ftp_user: str ftp_pass: str @validator('prompt') def validate_prompt(cls, v): if not v or len(v.strip()) < 10: raise ValueError('Prompt must be at least 10 characters') if len(v) > 10000: raise ValueError('Prompt too long (max 10000 characters)') return v.strip() @app.post("/generate-and-deploy") def generate_and_deploy(req: GenerateAndDeployRequest): """Generate website with AI and deploy to FTP in one call""" REQUEST_COUNT["total"] += 1 REQUEST_COUNT["generate"] += 1 logs = [] try: import ftplib from io import BytesIO logs.append({"time": time.time(), "type": "info", "message": "Starting AI generation..."}) logger.info("=== IONA AI Generation + Deployment Started ===") logger.info(f"Prompt: {req.prompt[:100]}...") logger.info(f"FTP Host: {req.ftp_host}, User: {req.ftp_user}") api_key = get_next_api_key() model = get_random_model() logs.append({"time": time.time(), "type": "info", "message": f"Using model: {model}"}) # Generate with AI - FORCE JSON MODE WITH DETAILED INSTRUCTIONS system_prompt = """You are a web development code generator. Generate a complete, functional website with HTML, CSS, and JavaScript. Return a JSON object with these keys: - html: A complete HTML5 document with proper structure - css: Complete CSS stylesheet with modern styling - js: JavaScript code for interactivity - readme: Brief setup instructions The website must be: - Fully functional and complete - Mobile responsive - Modern and professional - Include all requested features IMPORTANT CODE FORMATTING RULES: - Use proper indentation (2 spaces per level) - Add newlines between sections - Format code readably with line breaks - Do NOT put everything on one line - Use \n for newlines in JSON strings - Properly indent nested elements IMPORTANT: Do NOT return null values. Generate actual code for each field.""" user_prompt = f"""Generate a {req.business_type} website with this description: {req.prompt} Use {req.color_scheme} color scheme and include {', '.join(req.features) if req.features else 'standard'} features. Return as JSON with html, css, js, and readme keys.""" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], "temperature": 0.7, # Balanced for creativity and consistency "max_tokens": 32000, # Maximum tokens for complete generation "response_format": {"type": "json_object"} # Force JSON mode } logs.append({"time": time.time(), "type": "info", "message": "Calling Groq AI..."}) response = requests.post( "https://api.groq.com/openai/v1/chat/completions", headers=headers, json=payload, timeout=180 # 3 minutes for complex generation ) if response.status_code != 200: logs.append({"time": time.time(), "type": "error", "message": f"Groq API error: HTTP {response.status_code}"}) raise HTTPException(status_code=response.status_code, detail=response.text) result = response.json() raw_content = result['choices'][0]['message']['content'] logger.info(f"[GENERATE-DEPLOY] Raw response length: {len(raw_content)}") logger.info(f"[GENERATE-DEPLOY] Raw response preview: {raw_content[:200]}") logs.append({"time": time.time(), "type": "success", "message": "AI generation complete"}) logs.append({"time": time.time(), "type": "info", "message": "Parsing AI response..."}) # Parse JSON with better error handling generated_content = None # Try extracting from markdown code blocks if '```json' in raw_content: import re json_match = re.search(r'```json\s*(.+?)\s*```', raw_content, re.DOTALL) if json_match: raw_content = json_match.group(1) logger.info("[GENERATE-DEPLOY] Extracted from ```json block") elif '```' in raw_content: import re json_match = re.search(r'```\s*(.+?)\s*```', raw_content, re.DOTALL) if json_match: raw_content = json_match.group(1) logger.info("[GENERATE-DEPLOY] Extracted from ``` block") # Try parsing JSON try: generated_content = json.loads(raw_content) logger.info(f"[GENERATE-DEPLOY] Parsed JSON successfully, keys: {list(generated_content.keys())}") except json.JSONDecodeError as e: logger.error(f"[GENERATE-DEPLOY] JSON parse failed: {e}") logger.error(f"[GENERATE-DEPLOY] Content: {raw_content[:500]}") # Try to find JSON object in the response import re json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' matches = re.findall(json_pattern, raw_content, re.DOTALL) for match in matches: try: generated_content = json.loads(match) if 'html' in generated_content: logger.info("[GENERATE-DEPLOY] Found valid JSON in response") break except: continue if not generated_content: logs.append({"time": time.time(), "type": "error", "message": "AI returned invalid JSON format"}) raise ValueError(f"AI response is not valid JSON. Error: {str(e)}. Response preview: {raw_content[:200]}") if 'html' not in generated_content: logs.append({"time": time.time(), "type": "error", "message": "No HTML in AI response"}) raise ValueError("No HTML in AI response") # Check if HTML is null or empty html_content = generated_content.get('html', '') if not html_content or html_content == 'null' or len(html_content.strip()) < 100: logs.append({"time": time.time(), "type": "error", "message": f"AI returned invalid HTML (length: {len(html_content)})"}) logs.append({"time": time.time(), "type": "error", "message": f"Model used: {model}"}) logs.append({"time": time.time(), "type": "error", "message": f"Response preview: {str(generated_content)[:200]}"}) raise ValueError(f"AI returned null or empty HTML content. Model: {model}. Please try again.") logs.append({"time": time.time(), "type": "success", "message": "AI response parsed successfully"}) logs.append({"time": time.time(), "type": "info", "message": f"Generated {len(generated_content.get('html', ''))} chars of HTML"}) logs.append({"time": time.time(), "type": "info", "message": "FTP blocked - returning files to PHP for deployment"}) # HF Space cannot connect to FTP (port 21 blocked) # Return generated files to PHP for deployment logger.info("[GENERATE-DEPLOY] FTP blocked by HF network - returning files") return { "success": True, "model_used": model, "deployed_files": [], "files": { 'html': generated_content.get('html', ''), 'css': generated_content.get('css', ''), 'js': generated_content.get('js', ''), 'php': generated_content.get('php', ''), 'sql': generated_content.get('sql', ''), 'config': generated_content.get('config', ''), 'readme': generated_content.get('readme', '') }, "ftp_blocked": True, "logs": logs, "message": "Website generated successfully. FTP blocked - files returned for PHP deployment." } except HTTPException: raise except json.JSONDecodeError as e: REQUEST_COUNT["errors"] += 1 logger.error(f"[GENERATE-DEPLOY] JSON Error: {str(e)}") logs.append({"time": time.time(), "type": "error", "message": f"JSON parsing failed: {str(e)}"}) raise HTTPException(status_code=500, detail=f"AI returned invalid JSON format. Please try again. Error: {str(e)}") except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"[GENERATE-DEPLOY] Error: {str(e)}") logs.append({"time": time.time(), "type": "error", "message": str(e)}) raise HTTPException(status_code=500, detail=f"Generation/Deployment failed: {str(e)}") # ============================================================================ # GITHUB DEPLOYMENT ENDPOINTS # ============================================================================ # Import GitHub deployment module github_deploy = None try: from github_deploy import GitHubDeployment # Initialize GitHub Deployment (will be configured via environment variables) GITHUB_APP_ID = os.getenv('GITHUB_APP_ID', '') GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID', '') GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '') GITHUB_PRIVATE_KEY = os.getenv('GITHUB_PRIVATE_KEY', '').replace('\\n', '\n') if GITHUB_APP_ID and GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET and GITHUB_PRIVATE_KEY: try: github_deploy = GitHubDeployment( GITHUB_APP_ID, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_PRIVATE_KEY ) logger.info("โœ… GitHub Deployment initialized successfully") except Exception as e: logger.error(f"โŒ GitHub Deployment initialization failed: {e}") else: logger.info("โ„น๏ธ GitHub Deployment not configured (environment variables not set)") except ImportError as e: logger.error(f"โŒ Failed to import github_deploy module: {e}") logger.error("โŒ Make sure github_deploy.py is in the same directory as app.py") except Exception as e: logger.error(f"โŒ Error loading GitHub deployment: {e}") class GitHubOAuthRequest(BaseModel): code: str class GitHubInstallationTokenRequest(BaseModel): installation_id: str class GitHubDeploymentStatusRequest(BaseModel): owner: str repo: str run_id: int installation_id: str class GitHubRepoRequest(BaseModel): access_token: str class GitHubSetupDeploymentRequest(BaseModel): owner: str repo: str access_token: str ftp_server: str ftp_username: str ftp_password: str branch: Optional[str] = 'main' class GitHubWorkflowRunsRequest(BaseModel): owner: str repo: str access_token: str limit: Optional[int] = 10 class GitHubWorkflowLogsRequest(BaseModel): owner: str repo: str run_id: int access_token: str class GitHubTriggerWorkflowRequest(BaseModel): owner: str repo: str access_token: str workflow_file: Optional[str] = 'celestine_deploy.yml' branch: Optional[str] = 'main' @app.get("/github/status") def github_status(): """Check if GitHub integration is configured""" return { "configured": github_deploy is not None, "app_id": GITHUB_APP_ID if GITHUB_APP_ID else None, "client_id": GITHUB_CLIENT_ID if GITHUB_CLIENT_ID else None, "message": "GitHub integration is ready" if github_deploy else "GitHub integration not configured" } @app.post("/github/oauth/exchange") def github_oauth_exchange(req: GitHubOAuthRequest): """Exchange OAuth code for access token AND get installation ID""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: # Get OAuth token result = github_deploy.exchange_code_for_token(req.code) access_token = result.get('access_token') # Get user's GitHub App installations headers = { 'Authorization': f'token {access_token}', 'Accept': 'application/vnd.github.v3+json' } logger.info("๐Ÿ“ฆ Fetching user's GitHub App installations...") response = requests.get( 'https://api.github.com/user/installations', headers=headers, timeout=30 ) installation_id = None if response.status_code == 200: installations = response.json().get('installations', []) if installations: installation_id = str(installations[0]['id']) logger.info(f"โœ“ Found installation ID: {installation_id}") else: logger.warning("โš  No installations found for user") else: logger.warning(f"โš  Failed to get installations: {response.status_code}") return { "success": True, "access_token": access_token, # Only for initial repo listing "installation_id": installation_id, # Store this! Never expires! "token_type": result.get('token_type'), "scope": result.get('scope') } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"OAuth exchange failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/installation-token") def get_installation_token(req: GitHubInstallationTokenRequest): """ Get fresh installation token (valid for 1 hour) This is the KEY to solving token expiration issues! """ REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: logger.info(f"๐Ÿ” Generating installation token for installation_id: {req.installation_id}") token = github_deploy.get_installation_access_token(req.installation_id) logger.info(f"โœ“ Installation token generated (length: {len(token)})") return { "success": True, "token": token, "expires_in": 3600, # 1 hour "message": "Fresh installation token generated" } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get installation token: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/deployment-status") def get_deployment_status(req: GitHubDeploymentStatusRequest): """ Get deployment status using installation token (never expires!) This replaces the need for stored OAuth tokens """ REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: # Generate fresh installation token installation_token = github_deploy.get_installation_access_token(req.installation_id) # Get workflow run status headers = { 'Authorization': f'token {installation_token}', 'Accept': 'application/vnd.github.v3+json' } response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs/{req.run_id}', headers=headers, timeout=30 ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) data = response.json() return { "success": True, "status": data.get('status'), "conclusion": data.get('conclusion'), "created_at": data.get('created_at'), "updated_at": data.get('updated_at'), "html_url": data.get('html_url') } except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get deployment status: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/repos") def github_get_repos(req: GitHubRepoRequest): """Get user's GitHub repositories""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: repos = github_deploy.get_user_repos(req.access_token) return { "success": True, "repositories": repos, "count": len(repos) } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get repos: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/detect-project") def github_detect_project(req: GitHubSetupDeploymentRequest): """Detect project type and build configuration""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: project_config = github_deploy.detect_project_type( req.owner, req.repo, req.access_token ) return { "success": True, "project_config": project_config } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Project detection failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/setup-deployment") def github_setup_deployment(req: GitHubSetupDeploymentRequest): """ Complete deployment setup: 1. Detect project type 2. Create FTP secrets 3. Inject workflow file """ REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: logger.info("=" * 80) logger.info("๐Ÿš€ GITHUB DEPLOYMENT SETUP STARTED") logger.info("=" * 80) logger.info(f"๐Ÿ“ฆ Repository: {req.owner}/{req.repo}") logger.info(f"๐ŸŒฟ Branch: {req.branch}") logger.info(f"๐Ÿ” Access Token: {'โœ“ Present' if req.access_token else 'โœ— Missing'} (length: {len(req.access_token) if req.access_token else 0})") logger.info(f"๐ŸŒ FTP Server: {req.ftp_server}") logger.info(f"๐Ÿ‘ค FTP Username: {req.ftp_username if req.ftp_username else 'โœ— EMPTY'}") logger.info(f"๐Ÿ”‘ FTP Password: {'โœ“ Present' if req.ftp_password else 'โœ— EMPTY'} (length: {len(req.ftp_password) if req.ftp_password else 0})") logger.info("-" * 80) if not req.ftp_username: logger.error("โŒ FTP Username is empty!") raise HTTPException(status_code=400, detail="FTP Username is required") if not req.ftp_password: logger.error("โŒ FTP Password is empty!") raise HTTPException(status_code=400, detail="FTP Password is required") result = github_deploy.setup_deployment( req.owner, req.repo, req.access_token, req.ftp_server, req.ftp_username, req.ftp_password, req.branch ) if not result['success']: logger.error(f"โŒ Deployment setup failed: {result.get('error', 'Unknown error')}") raise HTTPException(status_code=500, detail=result.get('error', 'Setup failed')) logger.info("=" * 80) logger.info("โœ… GITHUB DEPLOYMENT SETUP COMPLETED SUCCESSFULLY") logger.info(f"๐Ÿ“ฆ Project Type: {result.get('project_type')}") logger.info(f"๐Ÿ”จ Build Command: {result.get('build_command')}") logger.info(f"๐Ÿ“‚ Output Directory: {result.get('output_dir')}") logger.info(f"๐Ÿ”— Workflow URL: {result.get('workflow_url')}") logger.info(f"๐ŸŽฌ Actions URL: {result.get('actions_url')}") logger.info("=" * 80) return result except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error("=" * 80) logger.error(f"โŒ DEPLOYMENT SETUP EXCEPTION: {str(e)}") logger.error("=" * 80) raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/workflow-runs") def github_workflow_runs(req: GitHubWorkflowRunsRequest): """Get workflow run history""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: runs = github_deploy.get_workflow_runs( req.owner, req.repo, req.access_token, req.limit ) return { "success": True, "runs": runs, "count": len(runs) } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get workflow runs: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/workflow-logs") def github_workflow_logs(req: GitHubWorkflowLogsRequest): """Get workflow run logs""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: logs = github_deploy.get_workflow_logs( req.owner, req.repo, req.run_id, req.access_token ) return { "success": True, "logs": logs } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get logs: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/trigger-workflow") def github_trigger_workflow(req: GitHubTriggerWorkflowRequest): """Manually trigger a workflow""" REQUEST_COUNT["total"] += 1 if not github_deploy: raise HTTPException(status_code=503, detail="GitHub integration not configured") try: success = github_deploy.trigger_workflow( req.owner, req.repo, req.workflow_file, req.access_token, req.branch ) if not success: raise HTTPException(status_code=500, detail="Failed to trigger workflow") return { "success": True, "message": "Workflow triggered successfully" } except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to trigger workflow: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # GITHUB DEPLOYMENT STATUS TRACKING (WITH CACHING) # ============================================================================ # In-memory cache for deployment status (reduces GitHub API calls) deployment_status_cache = {} CACHE_TTL = 30 # seconds class GitHubDeploymentStatusRequest(BaseModel): access_token: str owner: str repo: str branch: str = "main" class GitHubLatestCommitRequest(BaseModel): access_token: str owner: str repo: str branch: str = "main" class GitHubTriggerDeployRequest(BaseModel): access_token: str owner: str repo: str branch: str = "main" class GitHubCreateDeploymentRequest(BaseModel): access_token: str owner: str repo: str ref: str = "main" environment: str = "production" description: str = "Deploying via CELESTINE" class GitHubUpdateDeploymentStatusRequest(BaseModel): access_token: str owner: str repo: str deployment_id: int state: str # pending, success, failure, error environment_url: Optional[str] = "" description: Optional[str] = "" @app.post("/github/deployment-status") def get_deployment_status(req: GitHubDeploymentStatusRequest): """ Get real-time deployment status from GitHub Actions Returns: status, conclusion, commit info, workflow URL Caches results for 30 seconds to reduce API calls """ REQUEST_COUNT["total"] += 1 try: cache_key = f"{req.owner}/{req.repo}/{req.branch}" now = time.time() # Check cache first if cache_key in deployment_status_cache: cached_data, cached_time = deployment_status_cache[cache_key] if now - cached_time < CACHE_TTL: logger.info(f"โœ“ Cache hit for {cache_key} (age: {int(now - cached_time)}s)") cached_data['cached'] = True cached_data['cache_age'] = int(now - cached_time) return cached_data logger.info(f"Fetching deployment status for {req.owner}/{req.repo} (branch: {req.branch})") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting' } # Get latest workflow runs for the branch response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs', headers=headers, params={ 'branch': req.branch, 'per_page': 5, 'status': 'completed,in_progress,queued' }, timeout=15 ) if response.status_code != 200: logger.error(f"GitHub API error: {response.status_code} - {response.text}") raise HTTPException(status_code=response.status_code, detail=f"GitHub API error: {response.text}") data = response.json() workflow_runs = data.get('workflow_runs', []) if not workflow_runs: result = { 'success': True, 'status': 'unknown', 'conclusion': None, 'commit_sha': '', 'commit_msg': '', 'workflow_url': '', 'run_id': None, 'cached': False, 'message': 'No workflow runs found' } deployment_status_cache[cache_key] = (result, now) return result # Get the most recent run latest_run = workflow_runs[0] # Map GitHub status to our status gh_status = latest_run.get('status', 'unknown') gh_conclusion = latest_run.get('conclusion') # Determine final status if gh_status == 'completed': if gh_conclusion == 'success': status = 'success' elif gh_conclusion in ['failure', 'cancelled', 'timed_out']: status = 'failure' else: status = 'unknown' elif gh_status == 'in_progress': status = 'in_progress' elif gh_status == 'queued': status = 'queued' else: status = 'unknown' result = { 'success': True, 'status': status, 'conclusion': gh_conclusion, 'commit_sha': latest_run.get('head_sha', ''), 'commit_msg': latest_run.get('head_commit', {}).get('message', ''), 'workflow_url': latest_run.get('html_url', ''), 'run_id': latest_run.get('id'), 'created_at': latest_run.get('created_at'), 'updated_at': latest_run.get('updated_at'), 'cached': False, 'cache_age': 0 } # Cache the result deployment_status_cache[cache_key] = (result, now) logger.info(f"โœ“ Cached deployment status for {cache_key}: {status}") return result except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get deployment status: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/latest-commit") def get_latest_commit(req: GitHubLatestCommitRequest): """ Get latest commit on a branch Used for auto-deploy detection Caches results for 60 seconds """ REQUEST_COUNT["total"] += 1 try: cache_key = f"commit_{req.owner}/{req.repo}/{req.branch}" now = time.time() # Check cache (longer TTL for commits) if cache_key in deployment_status_cache: cached_data, cached_time = deployment_status_cache[cache_key] if now - cached_time < 60: # 60 second cache for commits logger.info(f"โœ“ Cache hit for commit {cache_key}") cached_data['cached'] = True return cached_data logger.info(f"Fetching latest commit for {req.owner}/{req.repo} (branch: {req.branch})") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting' } # Get latest commit response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/commits/{req.branch}', headers=headers, timeout=15 ) if response.status_code != 200: logger.error(f"GitHub API error: {response.status_code}") raise HTTPException(status_code=response.status_code, detail="Failed to fetch commit") commit_data = response.json() result = { 'success': True, 'sha': commit_data.get('sha', ''), 'message': commit_data.get('commit', {}).get('message', ''), 'author': commit_data.get('commit', {}).get('author', {}).get('name', ''), 'date': commit_data.get('commit', {}).get('author', {}).get('date', ''), 'cached': False } # Cache the result deployment_status_cache[cache_key] = (result, now) logger.info(f"โœ“ Cached commit info for {cache_key}: {result['sha'][:7]}") return result except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get latest commit: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/trigger-deploy") def trigger_deploy(req: GitHubTriggerDeployRequest): """ Trigger a deployment by dispatching the workflow Clears cache to force fresh status check """ REQUEST_COUNT["total"] += 1 try: logger.info(f"Triggering deployment for {req.owner}/{req.repo} (branch: {req.branch})") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting', 'Content-Type': 'application/json' } # Trigger workflow dispatch response = requests.post( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/workflows/deploy.yml/dispatches', headers=headers, json={'ref': req.branch}, timeout=15 ) # 204 = success (no content) if response.status_code == 204: # Clear cache to force fresh status check cache_key = f"{req.owner}/{req.repo}/{req.branch}" if cache_key in deployment_status_cache: del deployment_status_cache[cache_key] logger.info(f"โœ“ Cleared cache for {cache_key}") logger.info(f"โœ“ Deployment triggered successfully for {req.owner}/{req.repo}") return { 'success': True, 'message': 'Deployment triggered successfully' } else: logger.error(f"Failed to trigger deployment: {response.status_code} - {response.text}") return { 'success': False, 'error': f"GitHub API returned {response.status_code}", 'details': response.text } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to trigger deployment: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/create-deployment") def create_github_deployment(req: GitHubCreateDeploymentRequest): """ Create a GitHub deployment (for status tracking on repo page) This fixes the yellow dots issue """ REQUEST_COUNT["total"] += 1 try: logger.info(f"Creating GitHub deployment for {req.owner}/{req.repo}") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting', 'Content-Type': 'application/json' } # Create deployment response = requests.post( f'https://api.github.com/repos/{req.owner}/{req.repo}/deployments', headers=headers, json={ 'ref': req.ref, 'environment': req.environment, 'description': req.description, 'auto_merge': False, 'required_contexts': [], 'production_environment': True }, timeout=15 ) if response.status_code in [200, 201]: deployment_data = response.json() logger.info(f"โœ“ GitHub deployment created: {deployment_data.get('id')}") return { 'success': True, 'deployment_id': deployment_data.get('id'), 'sha': deployment_data.get('sha') } else: logger.error(f"Failed to create deployment: {response.status_code} - {response.text}") return { 'success': False, 'error': f"GitHub API returned {response.status_code}", 'details': response.text } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to create deployment: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/update-deployment-status") def update_github_deployment_status(req: GitHubUpdateDeploymentStatusRequest): """ Update GitHub deployment status (success/failure) This updates the dots on the GitHub repo page """ REQUEST_COUNT["total"] += 1 try: logger.info(f"Updating deployment status for {req.owner}/{req.repo} (deployment: {req.deployment_id}, state: {req.state})") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting', 'Content-Type': 'application/json' } # Update deployment status response = requests.post( f'https://api.github.com/repos/{req.owner}/{req.repo}/deployments/{req.deployment_id}/statuses', headers=headers, json={ 'state': req.state, 'environment_url': req.environment_url, 'description': req.description, 'auto_inactive': False }, timeout=15 ) if response.status_code in [200, 201]: logger.info(f"โœ“ Deployment status updated to {req.state}") return { 'success': True, 'message': f'Deployment status updated to {req.state}' } else: logger.error(f"Failed to update deployment status: {response.status_code} - {response.text}") return { 'success': False, 'error': f"GitHub API returned {response.status_code}", 'details': response.text } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to update deployment status: {e}") raise HTTPException(status_code=500, detail=str(e)) # Cache cleanup task (runs every 5 minutes) @app.on_event("startup") async def startup_cache_cleanup(): """Clean up old cache entries periodically""" import asyncio async def cleanup_task(): while True: await asyncio.sleep(300) # 5 minutes now = time.time() expired_keys = [] for key, (data, cached_time) in deployment_status_cache.items(): if now - cached_time > 300: # 5 minutes expired_keys.append(key) for key in expired_keys: del deployment_status_cache[key] if expired_keys: logger.info(f"๐Ÿงน Cleaned up {len(expired_keys)} expired cache entries") asyncio.create_task(cleanup_task()) # ============================================================================ # GITHUB WEBHOOK ENDPOINT (Fixes 404 errors) # ============================================================================ import hmac import hashlib def verify_github_webhook_signature(payload_body: bytes, secret_token: str, signature_header: str) -> bool: """Verify that the webhook payload was sent from GitHub.""" if not signature_header: return False hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256) expected_signature = "sha256=" + hash_object.hexdigest() return hmac.compare_digest(expected_signature, signature_header) @app.post("/github/webhook") async def github_webhook(request: Request): """ Handle GitHub webhook events Used for real-time deployment status updates """ REQUEST_COUNT["total"] += 1 try: # 1. Verify Webhook Signature github_secret = os.getenv('WEBHOOK_SECRET', 'savage') signature_header = request.headers.get('X-Hub-Signature-256', '') # Read raw body for signature verification payload_body = await request.body() if not verify_github_webhook_signature(payload_body, github_secret, signature_header): logger.warning("Invalid GitHub webhook signature") return {"success": False, "error": "Invalid signature"} event_type = request.headers.get('X-GitHub-Event', 'unknown') # Handle ping and installation events immediately (required for GitHub Apps) if event_type == 'ping': logger.info("Received ping event from GitHub App") return {"success": True, "message": "pong"} if event_type == 'installation' or event_type == 'installation_repositories': logger.info(f"Received {event_type} event from GitHub App") return {"success": True, "message": f"{event_type} event received"} payload = await request.json() logger.info("=" * 80) logger.info("๐Ÿ“จ GitHub Webhook Received") logger.info("=" * 80) logger.info(f"Event Type: {event_type}") repo_full_name = "unknown" if 'repository' in payload: repo_full_name = payload['repository']['full_name'] logger.info(f"Repository: {repo_full_name}") # Clear cache for this repo to force fresh status check cache_keys_to_clear = [k for k in deployment_status_cache.keys() if repo_full_name in k] for key in cache_keys_to_clear: del deployment_status_cache[key] logger.info(f"โœ“ Cleared cache for {key}") # Handle workflow_run events if event_type == 'workflow_run': workflow_run = payload.get('workflow_run', {}) action = payload.get('action', '') repo_owner = payload['repository']['owner']['login'] repo_name = payload['repository']['name'] run_id = workflow_run.get('id') status = workflow_run.get('status') conclusion = workflow_run.get('conclusion') html_url = workflow_run.get('html_url') logger.info(f"Workflow Run: {action}") logger.info(f"Run ID: {run_id}") logger.info(f"Status: {status}") logger.info(f"Conclusion: {conclusion}") # Update database if run_id: update_deployment_status( repo_owner=repo_owner, repo_name=repo_name, workflow_run_id=run_id, status=status, conclusion=conclusion, workflow_url=html_url ) # Handle deployment_status events elif event_type == 'deployment_status': deployment = payload.get('deployment', {}) deployment_status = payload.get('deployment_status', {}) repo_owner = payload['repository']['owner']['login'] repo_name = payload['repository']['name'] state = deployment_status.get('state') logger.info(f"Deployment Status: {state}") # Map to our status status_map = { 'pending': 'pending', 'queued': 'queued', 'in_progress': 'in_progress', 'success': 'completed', 'failure': 'failed', 'error': 'failed' } mapped_status = status_map.get(state, 'pending') # This could fall back to other updates if needed return { "success": True, "message": "Webhook processed", "event": event_type, "repo": repo_full_name } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Webhook processing error: {e}") return { "success": False, "error": str(e) } # ============================================================================ # GITHUB ACTIONS LOGS ENDPOINT (Real-time workflow logs) # ============================================================================ class GitHubWorkflowLogsRequest(BaseModel): access_token: str owner: str repo: str run_id: Optional[int] = None branch: str = "main" @app.post("/github/workflow-logs") def get_workflow_logs(req: GitHubWorkflowLogsRequest): """ Get real-time GitHub Actions workflow logs Returns actual logs from the running/completed workflow """ REQUEST_COUNT["total"] += 1 try: logger.info(f"Fetching workflow logs for {req.owner}/{req.repo}") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting' } # If run_id not provided, get the latest run if not req.run_id: response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs', headers=headers, params={'branch': req.branch, 'per_page': 1}, timeout=15 ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail="Failed to fetch runs") runs = response.json().get('workflow_runs', []) if not runs: return { 'success': True, 'logs': [], 'jobs': [], 'message': 'No workflow runs found' } req.run_id = runs[0]['id'] # Get jobs for this run jobs_response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs/{req.run_id}/jobs', headers=headers, timeout=15 ) if jobs_response.status_code != 200: raise HTTPException(status_code=jobs_response.status_code, detail="Failed to fetch jobs") jobs_data = jobs_response.json() jobs = jobs_data.get('jobs', []) # Extract steps from all jobs all_steps = [] for job in jobs: job_name = job.get('name', 'Unknown Job') steps = job.get('steps', []) for step in steps: step_name = step.get('name', 'Unknown Step') status = step.get('status', 'pending') conclusion = step.get('conclusion') started_at = step.get('started_at', '') completed_at = step.get('completed_at', '') # Map status to our format if status == 'completed': if conclusion == 'success': step_status = 'completed' else: step_status = 'failed' elif status == 'in_progress': step_status = 'in-progress' else: step_status = 'pending' all_steps.append({ 'job': job_name, 'name': step_name, 'status': step_status, 'conclusion': conclusion, 'started_at': started_at, 'completed_at': completed_at, 'number': step.get('number', 0) }) # Try to get logs (may not be available for in-progress runs) logs_text = [] try: logs_response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs/{req.run_id}/logs', headers=headers, timeout=15, allow_redirects=True ) if logs_response.status_code == 200: # Parse logs (they come as a zip file, but we'll get the text) logs_text = logs_response.text.split('\n')[:100] # First 100 lines except: pass return { 'success': True, 'run_id': req.run_id, 'jobs': jobs, 'steps': all_steps, 'logs': logs_text, 'total_steps': len(all_steps) } except HTTPException: raise except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get workflow logs: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/github/workflow-steps") def get_workflow_steps(req: GitHubWorkflowLogsRequest): """ Get workflow steps with real-time status Optimized for deployment viewer UI """ REQUEST_COUNT["total"] += 1 try: logger.info(f"Fetching workflow steps for {req.owner}/{req.repo}") headers = { 'Authorization': f'token {req.access_token}', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'CELESTINE-Hosting' } # Get latest run if run_id not provided if not req.run_id: response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs', headers=headers, params={'branch': req.branch, 'per_page': 1}, timeout=15 ) if response.status_code != 200: return {'success': False, 'error': 'Failed to fetch runs'} runs = response.json().get('workflow_runs', []) if not runs: return {'success': True, 'steps': [], 'logs': []} req.run_id = runs[0]['id'] # Get jobs jobs_response = requests.get( f'https://api.github.com/repos/{req.owner}/{req.repo}/actions/runs/{req.run_id}/jobs', headers=headers, timeout=15 ) if jobs_response.status_code != 200: return {'success': False, 'error': 'Failed to fetch jobs'} jobs = jobs_response.json().get('jobs', []) # Build simplified steps list for UI steps = [] logs = [] for job in jobs: for step in job.get('steps', []): step_name = step.get('name', '') status = step.get('status', 'pending') conclusion = step.get('conclusion') # Map to UI status if status == 'completed': ui_status = 'completed' if conclusion == 'success' else 'failed' elif status == 'in_progress': ui_status = 'in-progress' else: ui_status = 'pending' steps.append({ 'id': step.get('number', len(steps) + 1), 'title': step_name, 'status': ui_status }) # Add log entry if status != 'pending': timestamp = step.get('started_at', '') if timestamp: time_str = timestamp.split('T')[1][:8] if 'T' in timestamp else '' if ui_status == 'completed': logs.append({ 'time': time_str, 'text': f"โœ“ {step_name}", 'color': 'text-emerald-400' }) elif ui_status == 'failed': logs.append({ 'time': time_str, 'text': f"โœ— {step_name} failed", 'color': 'text-red-400' }) elif ui_status == 'in-progress': logs.append({ 'time': time_str, 'text': f"โŸณ {step_name}...", 'color': 'text-indigo-400' }) return { 'success': True, 'run_id': req.run_id, 'steps': steps, 'logs': logs } except Exception as e: REQUEST_COUNT["errors"] += 1 logger.error(f"Failed to get workflow steps: {e}") return { 'success': False, 'error': str(e), 'steps': [], 'logs': [] }