AudioForge / scripts /launch_verification.py
OnyxlMunkey's picture
c618549
#!/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())