#!/usr/bin/env python3 """ AudioForge Launch Verification Script Systematically verifies all items in LAUNCH_CHECKLIST.md Usage: python scripts/launch_verification.py [--section SECTION] [--fix] Options: --section SECTION Run specific section only (backend, frontend, security, etc.) --fix Attempt to auto-fix issues where possible --verbose Show detailed output """ import argparse import asyncio import json import os import subprocess import sys import time from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Dict, List, Optional, Tuple from urllib.parse import urlparse try: import httpx import psutil from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from rich.panel import Panel except ImportError: print("Installing required packages...") subprocess.run([sys.executable, "-m", "pip", "install", "httpx", "psutil", "rich"], check=True) import httpx import psutil from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from rich.panel import Panel class CheckStatus(Enum): """Status of a verification check.""" PASS = "✅" FAIL = "❌" WARN = "⚠️" SKIP = "⏭️" INFO = "ℹ️" @dataclass class CheckResult: """Result of a single verification check.""" name: str status: CheckStatus message: str details: Optional[str] = None fix_available: bool = False fix_command: Optional[str] = None @dataclass class SectionResult: """Result of a verification section.""" name: str checks: List[CheckResult] = field(default_factory=list) @property def passed(self) -> int: return sum(1 for c in self.checks if c.status == CheckStatus.PASS) @property def failed(self) -> int: return sum(1 for c in self.checks if c.status == CheckStatus.FAIL) @property def warned(self) -> int: return sum(1 for c in self.checks if c.status == CheckStatus.WARN) @property def total(self) -> int: return len(self.checks) @property def success_rate(self) -> float: if self.total == 0: return 0.0 return (self.passed / self.total) * 100 class LaunchVerifier: """Main verification orchestrator.""" def __init__(self, console: Console, fix_mode: bool = False, verbose: bool = False): self.console = console self.fix_mode = fix_mode self.verbose = verbose self.root_path = Path(__file__).parent.parent self.results: Dict[str, SectionResult] = {} async def verify_all(self) -> Dict[str, SectionResult]: """Run all verification checks.""" sections = [ ("Backend", self.verify_backend), ("Frontend", self.verify_frontend), ("UI/UX", self.verify_ui_ux), ("Integration", self.verify_integration), ("Performance", self.verify_performance), ("Security", self.verify_security), ("Documentation", self.verify_documentation), ] for section_name, verify_func in sections: self.console.print(f"\n[bold cyan]═══ {section_name} Verification ═══[/bold cyan]\n") result = await verify_func() self.results[section_name] = result self._print_section_summary(result) return self.results async def verify_backend(self) -> SectionResult: """Verify backend setup and health.""" result = SectionResult(name="Backend") # Check Python version python_version = sys.version_info if python_version >= (3, 11): result.checks.append(CheckResult( name="Python Version", status=CheckStatus.PASS, message=f"Python {python_version.major}.{python_version.minor}.{python_version.micro}" )) else: result.checks.append(CheckResult( name="Python Version", status=CheckStatus.FAIL, message=f"Python 3.11+ required, found {python_version.major}.{python_version.minor}", fix_available=False )) # Check .env file env_path = self.root_path / "backend" / ".env" env_example = self.root_path / "backend" / ".env.example" if env_path.exists(): result.checks.append(CheckResult( name="Environment File", status=CheckStatus.PASS, message=".env file exists" )) elif env_example.exists() and self.fix_mode: import shutil shutil.copy(env_example, env_path) result.checks.append(CheckResult( name="Environment File", status=CheckStatus.PASS, message=".env created from .env.example" )) else: result.checks.append(CheckResult( name="Environment File", status=CheckStatus.FAIL, message=".env file missing", fix_available=True, fix_command="cp backend/.env.example backend/.env" )) # Check backend dependencies backend_path = self.root_path / "backend" pyproject_path = backend_path / "pyproject.toml" if pyproject_path.exists(): result.checks.append(CheckResult( name="Backend Package Config", status=CheckStatus.PASS, message="pyproject.toml found" )) else: result.checks.append(CheckResult( name="Backend Package Config", status=CheckStatus.FAIL, message="pyproject.toml missing" )) # Check if backend is running try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get("http://localhost:8000/health") if response.status_code == 200: result.checks.append(CheckResult( name="Backend Health Check", status=CheckStatus.PASS, message="Backend responding on port 8000", details=f"Response: {response.json()}" )) else: result.checks.append(CheckResult( name="Backend Health Check", status=CheckStatus.WARN, message=f"Backend returned status {response.status_code}" )) except Exception as e: result.checks.append(CheckResult( name="Backend Health Check", status=CheckStatus.WARN, message="Backend not running or not accessible", details=str(e), fix_command="cd backend && uvicorn app.main:app --reload" )) # Check API documentation try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get("http://localhost:8000/docs") if response.status_code == 200: result.checks.append(CheckResult( name="API Documentation", status=CheckStatus.PASS, message="API docs accessible at /docs" )) else: result.checks.append(CheckResult( name="API Documentation", status=CheckStatus.WARN, message="API docs not accessible" )) except Exception: result.checks.append(CheckResult( name="API Documentation", status=CheckStatus.SKIP, message="Backend not running" )) # Check storage directories storage_path = backend_path / "storage" / "audio" required_dirs = ["music", "vocals", "mixed", "mastered"] missing_dirs = [d for d in required_dirs if not (storage_path / d).exists()] if not missing_dirs: result.checks.append(CheckResult( name="Storage Directories", status=CheckStatus.PASS, message="All storage directories exist" )) elif self.fix_mode: for dir_name in missing_dirs: (storage_path / dir_name).mkdir(parents=True, exist_ok=True) result.checks.append(CheckResult( name="Storage Directories", status=CheckStatus.PASS, message=f"Created missing directories: {', '.join(missing_dirs)}" )) else: result.checks.append(CheckResult( name="Storage Directories", status=CheckStatus.WARN, message=f"Missing directories: {', '.join(missing_dirs)}", fix_available=True )) return result async def verify_frontend(self) -> SectionResult: """Verify frontend setup and build.""" result = SectionResult(name="Frontend") frontend_path = self.root_path / "frontend" # Check package.json package_json = frontend_path / "package.json" if package_json.exists(): result.checks.append(CheckResult( name="Package Configuration", status=CheckStatus.PASS, message="package.json found" )) # Check if node_modules exists node_modules = frontend_path / "node_modules" if node_modules.exists(): result.checks.append(CheckResult( name="Dependencies Installed", status=CheckStatus.PASS, message="node_modules directory exists" )) else: result.checks.append(CheckResult( name="Dependencies Installed", status=CheckStatus.FAIL, message="node_modules missing", fix_available=True, fix_command="cd frontend && pnpm install" )) else: result.checks.append(CheckResult( name="Package Configuration", status=CheckStatus.FAIL, message="package.json missing" )) # Check .env.local env_local = frontend_path / ".env.local" if env_local.exists(): result.checks.append(CheckResult( name="Environment Configuration", status=CheckStatus.PASS, message=".env.local exists" )) else: result.checks.append(CheckResult( name="Environment Configuration", status=CheckStatus.WARN, message=".env.local missing", fix_available=True, fix_command='echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > frontend/.env.local' )) # Check if frontend is running try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get("http://localhost:3000") if response.status_code == 200: result.checks.append(CheckResult( name="Frontend Server", status=CheckStatus.PASS, message="Frontend responding on port 3000" )) else: result.checks.append(CheckResult( name="Frontend Server", status=CheckStatus.WARN, message=f"Frontend returned status {response.status_code}" )) except Exception as e: result.checks.append(CheckResult( name="Frontend Server", status=CheckStatus.WARN, message="Frontend not running", details=str(e), fix_command="cd frontend && pnpm dev" )) # Check TypeScript configuration tsconfig = frontend_path / "tsconfig.json" if tsconfig.exists(): result.checks.append(CheckResult( name="TypeScript Configuration", status=CheckStatus.PASS, message="tsconfig.json found" )) else: result.checks.append(CheckResult( name="TypeScript Configuration", status=CheckStatus.FAIL, message="tsconfig.json missing" )) # Check for TypeScript errors (if tsc is available) try: proc = subprocess.run( ["pnpm", "tsc", "--noEmit"], cwd=frontend_path, capture_output=True, text=True, timeout=30 ) if proc.returncode == 0: result.checks.append(CheckResult( name="TypeScript Compilation", status=CheckStatus.PASS, message="No TypeScript errors" )) else: error_lines = proc.stdout.count('\n') result.checks.append(CheckResult( name="TypeScript Compilation", status=CheckStatus.FAIL, message=f"TypeScript errors found ({error_lines} lines)", details=proc.stdout[:500] )) except FileNotFoundError: result.checks.append(CheckResult( name="TypeScript Compilation", status=CheckStatus.SKIP, message="pnpm not found" )) except Exception as e: result.checks.append(CheckResult( name="TypeScript Compilation", status=CheckStatus.SKIP, message=f"Could not run tsc: {str(e)}" )) return result async def verify_ui_ux(self) -> SectionResult: """Verify UI/UX enhancements are working.""" result = SectionResult(name="UI/UX") frontend_path = self.root_path / "frontend" / "src" # Check for new components components_to_check = [ "sound-wave-background.tsx", "floating-notes.tsx", "prompt-suggestions.tsx", "mini-visualizer.tsx", "footer-stats.tsx", "keyboard-shortcuts.tsx", "confetti-effect.tsx", ] components_path = frontend_path / "components" missing_components = [] for component in components_to_check: if not (components_path / component).exists(): missing_components.append(component) if not missing_components: result.checks.append(CheckResult( name="Creative Components", status=CheckStatus.PASS, message=f"All {len(components_to_check)} creative components present" )) else: result.checks.append(CheckResult( name="Creative Components", status=CheckStatus.WARN, message=f"Missing {len(missing_components)} components", details=", ".join(missing_components) )) # Check globals.css for animations globals_css = frontend_path / "app" / "globals.css" if globals_css.exists(): content = globals_css.read_text() animations = [ "fade-in", "slide-in-left", "slide-in-right", "gradient", "pulse-glow", "bounce-subtle", "float-up", "confetti-fall" ] missing_animations = [a for a in animations if f"@keyframes {a}" not in content] if not missing_animations: result.checks.append(CheckResult( name="CSS Animations", status=CheckStatus.PASS, message=f"All {len(animations)} animations defined" )) else: result.checks.append(CheckResult( name="CSS Animations", status=CheckStatus.WARN, message=f"Missing {len(missing_animations)} animations", details=", ".join(missing_animations) )) else: result.checks.append(CheckResult( name="CSS Animations", status=CheckStatus.FAIL, message="globals.css not found" )) # Check tailwind config for font support tailwind_config = self.root_path / "frontend" / "tailwind.config.ts" if tailwind_config.exists(): content = tailwind_config.read_text() if "fontFamily" in content and "display" in content: result.checks.append(CheckResult( name="Typography Configuration", status=CheckStatus.PASS, message="Display font configured in Tailwind" )) else: result.checks.append(CheckResult( name="Typography Configuration", status=CheckStatus.WARN, message="Display font may not be configured" )) return result async def verify_integration(self) -> SectionResult: """Verify integration between frontend and backend.""" result = SectionResult(name="Integration") # Check if both services are running backend_running = False frontend_running = False try: async with httpx.AsyncClient(timeout=5.0) as client: await client.get("http://localhost:8000/health") backend_running = True except Exception: pass try: async with httpx.AsyncClient(timeout=5.0) as client: await client.get("http://localhost:3000") frontend_running = True except Exception: pass if backend_running and frontend_running: result.checks.append(CheckResult( name="Services Running", status=CheckStatus.PASS, message="Both frontend and backend are running" )) # Test API endpoint from frontend perspective try: async with httpx.AsyncClient(timeout=10.0) as client: # Test generations list endpoint response = await client.get("http://localhost:8000/api/v1/generations") if response.status_code in [200, 404]: # 404 is ok if no generations yet result.checks.append(CheckResult( name="API Endpoints", status=CheckStatus.PASS, message="Generations API endpoint accessible" )) else: result.checks.append(CheckResult( name="API Endpoints", status=CheckStatus.WARN, message=f"Unexpected status code: {response.status_code}" )) except Exception as e: result.checks.append(CheckResult( name="API Endpoints", status=CheckStatus.FAIL, message="Could not access API endpoints", details=str(e) )) else: services_status = [] if not backend_running: services_status.append("backend") if not frontend_running: services_status.append("frontend") result.checks.append(CheckResult( name="Services Running", status=CheckStatus.FAIL, message=f"Services not running: {', '.join(services_status)}", fix_command="docker-compose up -d" )) return result async def verify_performance(self) -> SectionResult: """Verify performance metrics.""" result = SectionResult(name="Performance") # Check backend response time try: async with httpx.AsyncClient(timeout=5.0) as client: start = time.time() response = await client.get("http://localhost:8000/health") duration = (time.time() - start) * 1000 # Convert to ms if response.status_code == 200 and duration < 200: result.checks.append(CheckResult( name="Backend Response Time", status=CheckStatus.PASS, message=f"Health check: {duration:.0f}ms (< 200ms target)" )) elif duration < 500: result.checks.append(CheckResult( name="Backend Response Time", status=CheckStatus.WARN, message=f"Health check: {duration:.0f}ms (target: < 200ms)" )) else: result.checks.append(CheckResult( name="Backend Response Time", status=CheckStatus.FAIL, message=f"Health check: {duration:.0f}ms (too slow)" )) except Exception: result.checks.append(CheckResult( name="Backend Response Time", status=CheckStatus.SKIP, message="Backend not running" )) # Check system resources cpu_percent = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() if cpu_percent < 80: result.checks.append(CheckResult( name="CPU Usage", status=CheckStatus.PASS, message=f"CPU: {cpu_percent:.1f}% (healthy)" )) else: result.checks.append(CheckResult( name="CPU Usage", status=CheckStatus.WARN, message=f"CPU: {cpu_percent:.1f}% (high)" )) if memory.percent < 80: result.checks.append(CheckResult( name="Memory Usage", status=CheckStatus.PASS, message=f"Memory: {memory.percent:.1f}% (healthy)" )) else: result.checks.append(CheckResult( name="Memory Usage", status=CheckStatus.WARN, message=f"Memory: {memory.percent:.1f}% (high)" )) return result async def verify_security(self) -> SectionResult: """Verify security configurations.""" result = SectionResult(name="Security") # Check for .env in .gitignore gitignore = self.root_path / ".gitignore" if gitignore.exists(): content = gitignore.read_text() if ".env" in content: result.checks.append(CheckResult( name="Environment Files Protected", status=CheckStatus.PASS, message=".env files in .gitignore" )) else: result.checks.append(CheckResult( name="Environment Files Protected", status=CheckStatus.FAIL, message=".env not in .gitignore", fix_available=True )) # Check for exposed secrets in frontend frontend_env = self.root_path / "frontend" / ".env.local" if frontend_env.exists(): content = frontend_env.read_text() dangerous_keys = ["SECRET", "PRIVATE", "KEY", "PASSWORD"] exposed = [key for key in dangerous_keys if key in content.upper() and "NEXT_PUBLIC" not in content] if not exposed: result.checks.append(CheckResult( name="Frontend Secrets", status=CheckStatus.PASS, message="No exposed secrets in .env.local" )) else: result.checks.append(CheckResult( name="Frontend Secrets", status=CheckStatus.WARN, message=f"Potential secrets found: {', '.join(exposed)}" )) # Check CORS configuration (if backend is running) try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.options( "http://localhost:8000/api/v1/generations", headers={"Origin": "http://localhost:3000"} ) if "access-control-allow-origin" in response.headers: result.checks.append(CheckResult( name="CORS Configuration", status=CheckStatus.PASS, message="CORS headers present" )) else: result.checks.append(CheckResult( name="CORS Configuration", status=CheckStatus.WARN, message="CORS headers not found" )) except Exception: result.checks.append(CheckResult( name="CORS Configuration", status=CheckStatus.SKIP, message="Backend not running" )) return result async def verify_documentation(self) -> SectionResult: """Verify documentation completeness.""" result = SectionResult(name="Documentation") required_docs = { "README.md": "Main documentation", "SETUP.md": "Setup instructions", "ARCHITECTURE.md": "Architecture overview", "CONTRIBUTING.md": "Contribution guidelines", "LAUNCH_CHECKLIST.md": "Launch checklist", } missing_docs = [] for doc_file, description in required_docs.items(): doc_path = self.root_path / doc_file if doc_path.exists(): size = doc_path.stat().st_size if size > 100: # At least 100 bytes continue missing_docs.append(f"{doc_file} ({description})") if not missing_docs: result.checks.append(CheckResult( name="Required Documentation", status=CheckStatus.PASS, message=f"All {len(required_docs)} documentation files present" )) else: result.checks.append(CheckResult( name="Required Documentation", status=CheckStatus.WARN, message=f"Missing or incomplete: {len(missing_docs)} files", details="\n".join(missing_docs) )) # Check for LICENSE license_file = self.root_path / "LICENSE" if license_file.exists(): result.checks.append(CheckResult( name="License File", status=CheckStatus.PASS, message="LICENSE file present" )) else: result.checks.append(CheckResult( name="License File", status=CheckStatus.WARN, message="LICENSE file missing" )) return result def _print_section_summary(self, result: SectionResult): """Print summary for a section.""" table = Table(show_header=True, header_style="bold magenta") table.add_column("Check", style="cyan", width=30) table.add_column("Status", justify="center", width=8) table.add_column("Message", width=50) for check in result.checks: status_str = check.status.value message = check.message if check.details and self.verbose: message += f"\n[dim]{check.details}[/dim]" if check.fix_available and check.fix_command: message += f"\n[yellow]Fix: {check.fix_command}[/yellow]" table.add_row(check.name, status_str, message) self.console.print(table) # Print summary summary = f"[bold]Summary:[/bold] {result.passed}/{result.total} passed" if result.failed > 0: summary += f", {result.failed} failed" if result.warned > 0: summary += f", {result.warned} warnings" success_rate = result.success_rate if success_rate == 100: color = "green" elif success_rate >= 80: color = "yellow" else: color = "red" self.console.print(f"\n{summary} ([{color}]{success_rate:.1f}% success rate[/{color}])\n") def print_final_report(self): """Print final verification report.""" self.console.print("\n[bold cyan]═══ FINAL VERIFICATION REPORT ═══[/bold cyan]\n") table = Table(show_header=True, header_style="bold magenta") table.add_column("Section", style="cyan", width=20) table.add_column("Passed", justify="center", width=10) table.add_column("Failed", justify="center", width=10) table.add_column("Warnings", justify="center", width=10) table.add_column("Success Rate", justify="center", width=15) total_passed = 0 total_failed = 0 total_warned = 0 total_checks = 0 for section_name, result in self.results.items(): total_passed += result.passed total_failed += result.failed total_warned += result.warned total_checks += result.total success_rate = result.success_rate if success_rate == 100: rate_str = f"[green]{success_rate:.1f}%[/green]" elif success_rate >= 80: rate_str = f"[yellow]{success_rate:.1f}%[/yellow]" else: rate_str = f"[red]{success_rate:.1f}%[/red]" table.add_row( section_name, str(result.passed), str(result.failed) if result.failed > 0 else "-", str(result.warned) if result.warned > 0 else "-", rate_str ) self.console.print(table) # Overall summary overall_rate = (total_passed / total_checks * 100) if total_checks > 0 else 0 if overall_rate == 100: status_emoji = "🎉" status_msg = "[bold green]READY TO LAUNCH![/bold green]" elif overall_rate >= 90: status_emoji = "✅" status_msg = "[bold yellow]ALMOST READY - Minor issues to fix[/bold yellow]" elif overall_rate >= 70: status_emoji = "⚠️" status_msg = "[bold yellow]NOT READY - Several issues to address[/bold yellow]" else: status_emoji = "❌" status_msg = "[bold red]NOT READY - Critical issues found[/bold red]" panel = Panel( f"{status_emoji} {status_msg}\n\n" f"Total Checks: {total_checks}\n" f"Passed: {total_passed}\n" f"Failed: {total_failed}\n" f"Warnings: {total_warned}\n" f"Overall Success Rate: {overall_rate:.1f}%", title="[bold]Launch Status[/bold]", border_style="cyan" ) self.console.print("\n", panel, "\n") # Print actionable items if total_failed > 0 or total_warned > 0: self.console.print("[bold yellow]Action Items:[/bold yellow]\n") for section_name, result in self.results.items(): fixable = [c for c in result.checks if c.fix_available and c.fix_command] if fixable: self.console.print(f"[cyan]{section_name}:[/cyan]") for check in fixable: self.console.print(f" • {check.name}: [yellow]{check.fix_command}[/yellow]") self.console.print() async def main(): """Main entry point.""" parser = argparse.ArgumentParser(description="AudioForge Launch Verification") parser.add_argument("--section", help="Run specific section only") parser.add_argument("--fix", action="store_true", help="Attempt to auto-fix issues") parser.add_argument("--verbose", action="store_true", help="Show detailed output") parser.add_argument("--json", help="Output results to JSON file") args = parser.parse_args() console = Console() console.print(Panel.fit( "[bold cyan]AudioForge Launch Verification[/bold cyan]\n" "Systematically verifying all launch checklist items", border_style="cyan" )) verifier = LaunchVerifier(console, fix_mode=args.fix, verbose=args.verbose) try: results = await verifier.verify_all() verifier.print_final_report() # Export to JSON if requested if args.json: output = { section: { "passed": result.passed, "failed": result.failed, "warned": result.warned, "total": result.total, "success_rate": result.success_rate, "checks": [ { "name": c.name, "status": c.status.name, "message": c.message, "details": c.details, } for c in result.checks ] } for section, result in results.items() } with open(args.json, 'w') as f: json.dump(output, f, indent=2) console.print(f"\n[green]Results exported to {args.json}[/green]") # Exit code based on failures total_failed = sum(r.failed for r in results.values()) sys.exit(0 if total_failed == 0 else 1) except KeyboardInterrupt: console.print("\n[yellow]Verification cancelled by user[/yellow]") sys.exit(130) except Exception as e: console.print(f"\n[red]Error during verification: {e}[/red]") if args.verbose: import traceback console.print(traceback.format_exc()) sys.exit(1) if __name__ == "__main__": asyncio.run(main())