Initial commit.

This commit is contained in:
2025-06-09 15:18:16 +02:00
commit 47d4ed1cc0
10 changed files with 1858 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*~
*/venv
devbox-back/devbox_emails.db
devbox-back/__pycache__

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# DevBox Landing Page
Git repository for frontend and backend for the DevBox landing page and email collection service.

16
devbox-back/README.md Normal file
View File

@ -0,0 +1,16 @@
# DevBox Email Collection API
A simple and efficient email collection backend for the DevBox landing page.
## Features
- 📧 Email collection and validation
- 📊 Statistics and analytics
- 💾 SQLite database storage
- 🔍 Search and export functionality
- 🚀 FastAPI with automatic documentation
## Quick Start
```bash
./run.sh

541
devbox-back/database_manager.py Executable file
View File

@ -0,0 +1,541 @@
#!/usr/bin/env python3
"""
DevBox Email Database Management Utilities
This script provides database management utilities for the DevBox email collection system.
Offers tools for database maintenance, backups, and data analysis.
Usage:
python database_manager.py --help
python database_manager.py --export
python database_manager.py --backup
python database_manager.py --stats
python database_manager.py --cleanup-duplicates
python database_manager.py --validate-emails
"""
import sqlite3
import csv
import argparse
import json
import sys
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
import logging
import shutil
import os
import re
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DevBoxEmailManager:
"""
Database manager for DevBox email subscriptions
"""
def __init__(self, db_file: str = "devbox_emails.db"):
self.db_file = db_file
self.ensure_database_exists()
def ensure_database_exists(self):
"""Ensure database exists and is properly initialized"""
if not os.path.exists(self.db_file):
logger.warning(f"Database {self.db_file} not found. Creating new database...")
self.init_database()
else:
logger.info(f"Using existing database: {self.db_file}")
# Verify table structure
self._verify_schema()
def get_connection(self):
"""Get database connection with error handling"""
try:
conn = sqlite3.connect(self.db_file)
conn.row_factory = sqlite3.Row
return conn
except Exception as e:
logger.error(f"Failed to connect to database: {e}")
raise
def _verify_schema(self):
"""Verify database schema is correct"""
conn = self.get_connection()
try:
# Check if table exists and has correct columns
cursor = conn.execute("PRAGMA table_info(email_signups)")
columns = {row[1] for row in cursor.fetchall()}
expected_columns = {'id', 'email', 'source', 'created_at', 'user_agent', 'ip_address'}
if not expected_columns.issubset(columns):
logger.warning("Database schema incomplete. Updating...")
self._update_schema(conn)
finally:
conn.close()
def _update_schema(self, conn):
"""Update database schema if needed"""
try:
# Add missing columns if they don't exist
try:
conn.execute("ALTER TABLE email_signups ADD COLUMN user_agent TEXT")
logger.info("Added user_agent column")
except sqlite3.OperationalError:
pass # Column already exists
try:
conn.execute("ALTER TABLE email_signups ADD COLUMN ip_address TEXT")
logger.info("Added ip_address column")
except sqlite3.OperationalError:
pass # Column already exists
conn.commit()
except Exception as e:
logger.error(f"Failed to update schema: {e}")
def init_database(self):
"""Initialize database with required tables"""
conn = self.get_connection()
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS email_signups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
source TEXT DEFAULT 'devbox-landing',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_agent TEXT,
ip_address TEXT
)
""")
# Create indexes for better performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_email ON email_signups(email)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_created_at ON email_signups(created_at)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON email_signups(source)")
conn.commit()
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {e}")
raise
finally:
conn.close()
def export_to_csv(self, filename: Optional[str] = None, source_filter: Optional[str] = None) -> str:
"""Export emails to CSV file"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
source_suffix = f"_{source_filter}" if source_filter else ""
filename = f"devbox_emails{source_suffix}_{timestamp}.csv"
conn = self.get_connection()
try:
query = """
SELECT id, email, source, created_at, user_agent, ip_address
FROM email_signups
"""
params = []
if source_filter:
query += " WHERE source = ?"
params.append(source_filter)
query += " ORDER BY created_at DESC"
emails = conn.execute(query, params).fetchall()
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['ID', 'Email', 'Source', 'Created At', 'User Agent', 'IP Address'])
for email in emails:
writer.writerow([
email['id'],
email['email'],
email['source'] or 'devbox-landing',
email['created_at'],
email['user_agent'] or '',
email['ip_address'] or ''
])
logger.info(f"Exported {len(emails)} emails to {filename}")
print(f"✅ Exported {len(emails)} emails to {filename}")
return filename
except Exception as e:
logger.error(f"Export failed: {e}")
raise
finally:
conn.close()
def get_detailed_stats(self) -> Dict:
"""Get comprehensive statistics"""
conn = self.get_connection()
try:
# Basic stats
basic_stats = conn.execute("""
SELECT
COUNT(*) as total_signups,
COUNT(CASE WHEN DATE(created_at) = DATE('now') THEN 1 END) as today_signups,
COUNT(CASE WHEN DATE(created_at) >= DATE('now', '-7 days') THEN 1 END) as week_signups,
COUNT(CASE WHEN DATE(created_at) >= DATE('now', '-30 days') THEN 1 END) as month_signups,
MIN(created_at) as first_signup,
MAX(created_at) as latest_signup
FROM email_signups
""").fetchone()
if not basic_stats or basic_stats['total_signups'] == 0:
return {
'message': 'No email signups found in database',
'basic_stats': {
'total_signups': 0,
'today_signups': 0,
'week_signups': 0,
'month_signups': 0,
'first_signup': None,
'latest_signup': None
},
'sources': {},
'daily_signups': [],
'top_domains': []
}
# Source breakdown
sources = conn.execute("""
SELECT COALESCE(source, 'devbox-landing') as source, COUNT(*) as count
FROM email_signups
GROUP BY COALESCE(source, 'devbox-landing')
ORDER BY count DESC
""").fetchall()
# Daily signups for last 30 days
daily_signups = conn.execute("""
SELECT
DATE(created_at) as signup_date,
COUNT(*) as daily_count
FROM email_signups
WHERE DATE(created_at) >= DATE('now', '-30 days')
GROUP BY DATE(created_at)
ORDER BY signup_date DESC
""").fetchall()
# Top domains
top_domains = conn.execute("""
SELECT
CASE
WHEN INSTR(email, '@') > 0
THEN SUBSTR(email, INSTR(email, '@') + 1)
ELSE 'invalid'
END as domain,
COUNT(*) as count
FROM email_signups
GROUP BY domain
ORDER BY count DESC
LIMIT 10
""").fetchall()
return {
'basic_stats': dict(basic_stats),
'sources': {row['source']: row['count'] for row in sources},
'daily_signups': [dict(row) for row in daily_signups],
'top_domains': [dict(row) for row in top_domains]
}
except Exception as e:
logger.error(f"Failed to get stats: {e}")
raise
finally:
conn.close()
def cleanup_duplicates(self) -> int:
"""Remove duplicate emails (keep the earliest)"""
conn = self.get_connection()
try:
# Find duplicates
duplicates = conn.execute("""
SELECT email, COUNT(*) as count, MIN(id) as keep_id
FROM email_signups
GROUP BY LOWER(email)
HAVING COUNT(*) > 1
""").fetchall()
if not duplicates:
print("✅ No duplicates found")
return 0
removed_count = 0
for dup in duplicates:
# Remove all but the earliest (smallest ID)
result = conn.execute("""
DELETE FROM email_signups
WHERE LOWER(email) = LOWER(?) AND id != ?
""", (dup['email'], dup['keep_id']))
removed_count += result.rowcount
logger.info(f"Removed {result.rowcount} duplicate(s) for {dup['email']}")
print(f"🧹 Removed {result.rowcount} duplicate(s) for {dup['email']}")
conn.commit()
logger.info(f"Cleanup complete. Removed {removed_count} duplicate records.")
print(f"✅ Cleanup complete. Removed {removed_count} duplicate records.")
return removed_count
except Exception as e:
logger.error(f"Cleanup failed: {e}")
raise
finally:
conn.close()
def backup_database(self, backup_file: Optional[str] = None) -> str:
"""Create a backup of the database"""
if backup_file is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = f"devbox_emails_backup_{timestamp}.db"
try:
if not os.path.exists(self.db_file):
raise FileNotFoundError(f"Source database {self.db_file} does not exist")
shutil.copy2(self.db_file, backup_file)
file_size = os.path.getsize(backup_file)
logger.info(f"Database backed up to {backup_file} ({file_size:,} bytes)")
print(f"✅ Database backed up to {backup_file} ({file_size:,} bytes)")
return backup_file
except Exception as e:
logger.error(f"Backup failed: {e}")
raise
def analyze_growth(self, days: int = 30) -> Dict:
"""Analyze signup growth trends"""
conn = self.get_connection()
try:
growth_data = conn.execute("""
SELECT
DATE(created_at) as date,
COUNT(*) as signups,
SUM(COUNT(*)) OVER (ORDER BY DATE(created_at)) as cumulative
FROM email_signups
WHERE DATE(created_at) >= DATE('now', '-' || ? || ' days')
GROUP BY DATE(created_at)
ORDER BY date
""", (days,)).fetchall()
if not growth_data:
return {
'message': f'No data available for the last {days} days',
'period_days': days,
'total_signups_in_period': 0,
'growth_rate_percent': 0,
'daily_data': [],
'average_daily_signups': 0
}
# Calculate growth rate
total_signups = growth_data[-1]['cumulative'] if growth_data else 0
first_day_cumulative = growth_data[0]['cumulative'] - growth_data[0]['signups'] if growth_data else 0
signups_in_period = total_signups - first_day_cumulative
growth_rate = 0
if first_day_cumulative > 0:
growth_rate = (signups_in_period / first_day_cumulative) * 100
return {
'period_days': days,
'total_signups_in_period': signups_in_period,
'growth_rate_percent': round(growth_rate, 2),
'daily_data': [dict(row) for row in growth_data],
'average_daily_signups': round(signups_in_period / days, 2) if days > 0 else 0
}
except Exception as e:
logger.error(f"Growth analysis failed: {e}")
raise
finally:
conn.close()
def validate_emails(self) -> Dict:
"""Validate email formats in the database"""
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
conn = self.get_connection()
try:
all_emails = conn.execute("SELECT id, email FROM email_signups").fetchall()
if not all_emails:
return {
'total_emails': 0,
'valid_emails': 0,
'invalid_emails': 0,
'invalid_list': []
}
valid_emails = []
invalid_emails = []
for row in all_emails:
email = row['email'].strip() if row['email'] else ''
if email and email_pattern.match(email):
valid_emails.append(email)
else:
invalid_emails.append({'id': row['id'], 'email': email})
return {
'total_emails': len(all_emails),
'valid_emails': len(valid_emails),
'invalid_emails': len(invalid_emails),
'invalid_list': invalid_emails
}
except Exception as e:
logger.error(f"Email validation failed: {e}")
raise
finally:
conn.close()
def get_email_count(self) -> int:
"""Get total number of subscribed emails"""
conn = self.get_connection()
try:
result = conn.execute("SELECT COUNT(*) as count FROM email_signups").fetchone()
return result['count'] if result else 0
except Exception as e:
logger.error(f"Failed to get email count: {e}")
return 0
finally:
conn.close()
def print_stats_table(stats: Dict):
"""Print statistics in a nice table format"""
print("\n📊 DevBox Email Statistics")
print("=" * 50)
basic = stats.get('basic_stats', {})
print(f"Total Signups: {basic.get('total_signups', 0):,}")
print(f"Today: {basic.get('today_signups', 0):,}")
print(f"This Week: {basic.get('week_signups', 0):,}")
print(f"This Month: {basic.get('month_signups', 0):,}")
print(f"First Signup: {basic.get('first_signup', 'N/A')}")
print(f"Latest Signup: {basic.get('latest_signup', 'N/A')}")
sources = stats.get('sources', {})
if sources:
print(f"\n📈 Sources:")
for source, count in sources.items():
print(f" {source}: {count:,}")
domains = stats.get('top_domains', [])
if domains:
print(f"\n🌐 Top Domains:")
for domain in domains[:5]:
print(f" {domain['domain']}: {domain['count']:,}")
def main():
"""Command line interface for database management"""
parser = argparse.ArgumentParser(
description='DevBox Email Database Manager',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python database_manager.py --stats
python database_manager.py --export
python database_manager.py --export --source-filter devbox-landing
python database_manager.py --backup
python database_manager.py --cleanup-duplicates
python database_manager.py --validate-emails
python database_manager.py --analyze-growth 14
"""
)
parser.add_argument('--export', action='store_true',
help='Export emails to CSV')
parser.add_argument('--backup', action='store_true',
help='Create database backup')
parser.add_argument('--stats', action='store_true',
help='Show detailed statistics')
parser.add_argument('--cleanup-duplicates', action='store_true',
help='Remove duplicate emails')
parser.add_argument('--analyze-growth', type=int, metavar='DAYS',
help='Analyze growth over N days')
parser.add_argument('--validate-emails', action='store_true',
help='Validate email formats')
parser.add_argument('--source-filter', type=str,
help='Filter by source (for export)')
parser.add_argument('--db-file', type=str, default='devbox_emails.db',
help='Database file path')
parser.add_argument('--count', action='store_true',
help='Show total email count')
args = parser.parse_args()
# If no arguments provided, show help
if len(sys.argv) == 1:
parser.print_help()
return 0
try:
manager = DevBoxEmailManager(args.db_file)
if args.count:
count = manager.get_email_count()
print(f"📧 Total emails in database: {count:,}")
if args.export:
filename = manager.export_to_csv(source_filter=args.source_filter)
# filename is already printed by the method
if args.backup:
backup_file = manager.backup_database()
# backup_file is already printed by the method
if args.stats:
stats = manager.get_detailed_stats()
if 'message' in stats:
print(f" {stats['message']}")
else:
print_stats_table(stats)
if args.cleanup_duplicates:
removed = manager.cleanup_duplicates()
# results are already printed by the method
if args.analyze_growth:
growth = manager.analyze_growth(args.analyze_growth)
print(f"\n📈 Growth Analysis ({args.analyze_growth} days):")
print("=" * 40)
if 'message' in growth:
print(f" {growth['message']}")
else:
print(f"Total signups in period: {growth['total_signups_in_period']:,}")
print(f"Growth rate: {growth['growth_rate_percent']}%")
print(f"Average daily signups: {growth['average_daily_signups']}")
if args.validate_emails:
validation = manager.validate_emails()
print(f"\n✉️ Email Validation Results:")
print("=" * 35)
print(f"Total emails: {validation['total_emails']:,}")
print(f"Valid emails: {validation['valid_emails']:,}")
print(f"Invalid emails: {validation['invalid_emails']:,}")
if validation['invalid_emails'] > 0:
print(f"\n⚠️ Invalid emails found:")
for invalid in validation['invalid_list'][:10]: # Show first 10
print(f" ID {invalid['id']}: {invalid['email']}")
if len(validation['invalid_list']) > 10:
print(f" ... and {len(validation['invalid_list']) - 10} more")
return 0
except KeyboardInterrupt:
print("\n🛑 Operation cancelled by user")
return 1
except Exception as e:
logger.error(f"Operation failed: {e}")
print(f"❌ Error: {e}")
return 1
if __name__ == "__main__":
exit(main())

465
devbox-back/main.py Normal file
View File

@ -0,0 +1,465 @@
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr
from contextlib import asynccontextmanager
import sqlite3
import logging
from datetime import datetime
from typing import List, Optional
import uvicorn
import os
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Database configuration
DATABASE_FILE = "devbox_emails.db"
def get_db_connection():
"""Create and return a database connection"""
conn = sqlite3.connect(DATABASE_FILE)
conn.row_factory = sqlite3.Row # This allows dict-like access to rows
return conn
def init_database():
"""Initialize the database with required tables"""
conn = get_db_connection()
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS email_signups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
source TEXT DEFAULT 'devbox-landing',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_agent TEXT,
ip_address TEXT
)
""")
# Create indexes for better performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_email ON email_signups(email)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_created_at ON email_signups(created_at)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON email_signups(source)")
conn.commit()
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Error initializing database: {e}")
raise
finally:
conn.close()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Handle application startup and shutdown events"""
# Startup
logger.info("DevBox Email Collection API starting up...")
init_database()
logger.info("DevBox Email Collection API started successfully")
yield
# Shutdown
logger.info("DevBox Email Collection API shutting down...")
# Create FastAPI app with lifespan
app = FastAPI(
title="DevBox Email Collection API",
description="Backend API for collecting email signups for DevBox landing page",
version="1.0.0",
lifespan=lifespan
)
# CORS middleware to allow requests from your frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify your domain
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Pydantic models
class EmailSignup(BaseModel):
email: EmailStr
source: Optional[str] = "devbox-landing"
class EmailSignupResponse(BaseModel):
success: bool
message: str
email: str
id: Optional[int] = None
class EmailListResponse(BaseModel):
total_count: int
emails: List[dict]
class StatsResponse(BaseModel):
total_signups: int
today_signups: int
week_signups: int
month_signups: int
first_signup: Optional[str]
latest_signup: Optional[str]
sources: dict
class UnsubscribeResponse(BaseModel):
success: bool
message: str
# Helper function to get client IP
def get_client_ip(request: Request) -> str:
"""Extract client IP address from request"""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else "unknown"
# API endpoints
@app.post("/api/subscribe", response_model=EmailSignupResponse)
async def subscribe_email(signup: EmailSignup, request: Request):
"""
Subscribe an email address to the DevBox mailing list
"""
conn = get_db_connection()
try:
# Check if email already exists
existing = conn.execute(
"SELECT id FROM email_signups WHERE email = ?",
(signup.email,)
).fetchone()
if existing:
return EmailSignupResponse(
success=False,
message="Email already subscribed",
email=signup.email
)
# Get client information
user_agent = request.headers.get("User-Agent", "")
ip_address = get_client_ip(request)
# Insert new email
cursor = conn.execute(
"""
INSERT INTO email_signups (email, source, created_at, user_agent, ip_address)
VALUES (?, ?, ?, ?, ?)
""",
(signup.email, signup.source, datetime.utcnow(), user_agent, ip_address)
)
conn.commit()
logger.info(f"New email subscription: {signup.email} from {ip_address}")
return EmailSignupResponse(
success=True,
message="Successfully subscribed to DevBox updates!",
email=signup.email,
id=cursor.lastrowid
)
except sqlite3.IntegrityError:
return EmailSignupResponse(
success=False,
message="Email already subscribed",
email=signup.email
)
except Exception as e:
logger.error(f"Error subscribing email {signup.email}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.get("/api/emails", response_model=EmailListResponse)
async def get_emails(limit: int = 100, offset: int = 0, source: Optional[str] = None):
"""
Get list of subscribed emails (for admin purposes)
Note: In production, add authentication/authorization
"""
conn = get_db_connection()
try:
# Build query with optional source filter
base_query = "SELECT COUNT(*) as count FROM email_signups"
emails_query = """
SELECT id, email, source, created_at, ip_address
FROM email_signups
"""
params = []
if source:
base_query += " WHERE source = ?"
emails_query += " WHERE source = ?"
params.append(source)
# Get total count
total_count = conn.execute(base_query, params).fetchone()["count"]
# Get emails with pagination
emails_query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
emails = conn.execute(emails_query, params).fetchall()
# Convert to list of dicts
email_list = [dict(email) for email in emails]
return EmailListResponse(
total_count=total_count,
emails=email_list
)
except Exception as e:
logger.error(f"Error fetching emails: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.get("/api/stats", response_model=StatsResponse)
async def get_stats():
"""
Get detailed statistics about email signups
"""
conn = get_db_connection()
try:
# Get basic stats
stats = conn.execute("""
SELECT
COUNT(*) as total_signups,
COUNT(CASE WHEN DATE(created_at) = DATE('now') THEN 1 END) as today_signups,
COUNT(CASE WHEN DATE(created_at) >= DATE('now', '-7 days') THEN 1 END) as week_signups,
COUNT(CASE WHEN DATE(created_at) >= DATE('now', '-30 days') THEN 1 END) as month_signups,
MIN(created_at) as first_signup,
MAX(created_at) as latest_signup
FROM email_signups
""").fetchone()
# Get source breakdown
sources = conn.execute("""
SELECT source, COUNT(*) as count
FROM email_signups
GROUP BY source
ORDER BY count DESC
""").fetchall()
sources_dict = {row["source"]: row["count"] for row in sources}
return StatsResponse(
total_signups=stats["total_signups"],
today_signups=stats["today_signups"],
week_signups=stats["week_signups"],
month_signups=stats["month_signups"],
first_signup=stats["first_signup"],
latest_signup=stats["latest_signup"],
sources=sources_dict
)
except Exception as e:
logger.error(f"Error fetching stats: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.delete("/api/unsubscribe/{email}", response_model=UnsubscribeResponse)
async def unsubscribe_email(email: str):
"""
Remove an email from the subscription list
"""
conn = get_db_connection()
try:
cursor = conn.execute("DELETE FROM email_signups WHERE email = ?", (email,))
conn.commit()
if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Email not found")
logger.info(f"Email unsubscribed: {email}")
return UnsubscribeResponse(
success=True,
message=f"Email {email} successfully unsubscribed"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error unsubscribing email {email}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.get("/api/export")
async def export_emails(source: Optional[str] = None):
"""
Export emails to CSV format
Note: In production, add authentication/authorization
"""
conn = get_db_connection()
try:
query = """
SELECT email, source, created_at, ip_address
FROM email_signups
"""
params = []
if source:
query += " WHERE source = ?"
params.append(source)
query += " ORDER BY created_at DESC"
emails = conn.execute(query, params).fetchall()
# Convert to CSV format
import io
import csv
from fastapi.responses import StreamingResponse
output = io.StringIO()
writer = csv.writer(output)
# Write header
writer.writerow(['Email', 'Source', 'Created At', 'IP Address'])
# Write data
for email in emails:
writer.writerow([
email['email'],
email['source'],
email['created_at'],
email['ip_address']
])
output.seek(0)
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"devbox_emails_{timestamp}.csv"
return StreamingResponse(
io.BytesIO(output.getvalue().encode('utf-8')),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
except Exception as e:
logger.error(f"Error exporting emails: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.get("/api/search")
async def search_emails(q: str, limit: int = 50):
"""
Search emails by email address or domain
"""
if len(q) < 2:
raise HTTPException(status_code=400, detail="Search query must be at least 2 characters")
conn = get_db_connection()
try:
emails = conn.execute("""
SELECT id, email, source, created_at
FROM email_signups
WHERE email LIKE ?
ORDER BY created_at DESC
LIMIT ?
""", (f"%{q}%", limit)).fetchall()
return {
"query": q,
"count": len(emails),
"emails": [dict(email) for email in emails]
}
except Exception as e:
logger.error(f"Error searching emails: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
@app.get("/")
async def root():
"""
Root endpoint with API information
"""
return {
"message": "DevBox Email Collection API",
"version": "1.0.0",
"timestamp": datetime.utcnow().isoformat(),
"endpoints": {
"subscribe": "POST /api/subscribe",
"list_emails": "GET /api/emails",
"stats": "GET /api/stats",
"search": "GET /api/search?q=query",
"export": "GET /api/export",
"unsubscribe": "DELETE /api/unsubscribe/{email}",
"health": "GET /health",
"docs": "GET /docs"
}
}
@app.get("/health")
async def health_check():
"""
Health check endpoint
"""
conn = get_db_connection()
try:
# Test database connection
conn.execute("SELECT 1").fetchone()
db_status = "healthy"
except Exception as e:
db_status = f"unhealthy: {str(e)}"
logger.error(f"Database health check failed: {e}")
finally:
conn.close()
return {
"status": "healthy" if db_status == "healthy" else "unhealthy",
"timestamp": datetime.utcnow().isoformat(),
"database": db_status,
"version": "1.0.0"
}
# Custom exception handler
@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
return {
"error": "Not Found",
"message": "The requested endpoint was not found",
"available_endpoints": [
"/",
"/health",
"/docs",
"/api/subscribe",
"/api/emails",
"/api/stats",
"/api/search",
"/api/export"
]
}
if __name__ == "__main__":
# Check if running in development mode
import sys
dev_mode = "--dev" in sys.argv or os.getenv("DEV_MODE", "false").lower() == "true"
if dev_mode:
# Development settings
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8080,
reload=True,
log_level="info"
)
else:
# Production settings
uvicorn.run(
app,
host="0.0.0.0",
port=8080,
log_level="info"
)

View File

@ -0,0 +1,46 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "devbox-email-api"
version = "1.0.0"
description = "Email collection API for DevBox landing page"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "DevBox Team"},
]
requires-python = ">=3.8"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic[email]>=2.5.0",
"python-multipart>=0.0.6",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=23.0.0",
"isort>=5.12.0",
"flake8>=6.0.0",
]
[project.urls]
Homepage = "https://github.com/yourusername/devbox-email-api"
Repository = "https://github.com/yourusername/devbox-email-api"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.black]
line-length = 88
target-version = ['py38']
[tool.isort]
profile = "black"
line_length = 88
[project.scripts]
devbox-api = "main:main"

41
devbox-back/run.sh Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
# DevBox Email Collection API - Simple Startup Script
echo "🚀 Starting DevBox Email Collection API..."
# Check if Python 3 is available
if ! command -v python3 &> /dev/null; then
echo "❌ Error: python3 not found"
echo "Please install Python 3"
exit 1
fi
echo "✅ Found Python: $(python3 --version)"
# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
echo "📦 Creating virtual environment..."
python3 -m venv venv
fi
# Activate virtual environment
echo "🔧 Activating virtual environment..."
source venv/bin/activate
# Install dependencies directly (simpler approach)
echo "📚 Installing dependencies..."
pip install --upgrade pip
pip install fastapi uvicorn[standard] pydantic[email] python-multipart
echo ""
echo "🎉 Setup complete!"
echo "📍 API will be available at: http://localhost:8000"
echo "📖 API documentation at: http://localhost:8000/docs"
echo ""
echo "🚀 Starting server..."
echo "💡 Press Ctrl+C to stop"
echo ""
# Start the application
python main.py --dev

110
devbox-front/index.html Normal file
View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevBox - DevOps Made Simple</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<header class="header">
<div class="logo">
<h1>DevBox</h1>
</div>
</header>
<main class="main-content">
<div class="hero-section">
<h2 class="hero-title">DevOps Without the Headaches</h2>
<p class="hero-subtitle">
Focus on building great products while we handle HTTPS, monitoring, CI/CD, security,
logs, and backups. Complete DevOps infrastructure for indie developers and small businesses.
</p>
<form class="email-form" id="emailForm">
<div class="input-group">
<input
type="email"
id="email"
placeholder="Enter your email for early access"
required
class="email-input"
>
<button type="submit" class="submit-btn">
Join the Waitlist
</button>
</div>
<p class="form-note">
Be the first to experience effortless DevOps. No spam, just launch updates.
</p>
</form>
<div class="success-message" id="successMessage">
<div class="success-content">
<div class="success-icon"></div>
<h3>Welcome aboard!</h3>
<p>We'll notify you as soon as DevBox is ready to simplify your deployments.</p>
</div>
</div>
</div>
<div class="features-preview">
<div class="feature-grid">
<div class="feature-item">
<div class="feature-icon">🔒</div>
<h3>Auto HTTPS & Security</h3>
<p>Automatic SSL certificates, security headers, and vulnerability scanning out of the box</p>
</div>
<div class="feature-item">
<div class="feature-icon">📊</div>
<h3>Monitoring & Logs</h3>
<p>Real-time metrics, centralized logging, and intelligent alerting without configuration</p>
</div>
<div class="feature-item">
<div class="feature-icon">🚀</div>
<h3>One-Click CI/CD</h3>
<p>Push to deploy with automated testing, staging environments, and rollback capabilities</p>
</div>
<div class="feature-item">
<div class="feature-icon">💾</div>
<h3>Automated Backups</h3>
<p>Daily encrypted backups with point-in-time recovery and cross-region replication</p>
</div>
<div class="feature-item">
<div class="feature-icon"></div>
<h3>Scale on Demand</h3>
<p>Auto-scaling infrastructure that grows with your business without manual intervention</p>
</div>
<div class="feature-item">
<div class="feature-icon">🎯</div>
<h3>Developer First</h3>
<p>Simple APIs, intuitive dashboard, and integrations with your favorite tools</p>
</div>
</div>
</div>
<div class="value-proposition">
<h3 class="value-title">Stop Fighting Infrastructure</h3>
<p class="value-text">
Spend your time building features your customers love, not wrestling with
Kubernetes configs, SSL certificates, and monitoring dashboards.
DevBox handles the complexity so you don't have to.
</p>
</div>
</main>
<footer class="footer">
<div class="social-links">
<a href="#" class="social-link">Twitter</a>
<a href="#" class="social-link">GitHub</a>
<a href="#" class="social-link">LinkedIn</a>
</div>
<p class="footer-text">© 2025 DevBox. Built for developers, by developers.</p>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>

270
devbox-front/script.js Normal file
View File

@ -0,0 +1,270 @@
document.addEventListener('DOMContentLoaded', function() {
const emailForm = document.getElementById('emailForm');
const successMessage = document.getElementById('successMessage');
const API_BASE_URL = 'http://localhost:8080'; // Backend API URL
emailForm.addEventListener('submit', function(e) {
e.preventDefault();
const emailInput = document.getElementById('email');
const email = emailInput.value.trim();
// Basic email validation
if (!isValidEmail(email)) {
showError('Please enter a valid email address');
return;
}
// Submit email to backend
submitEmail(email);
});
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function submitEmail(email) {
// Show loading state
const submitBtn = document.querySelector('.submit-btn');
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Joining...';
submitBtn.disabled = true;
submitBtn.classList.add('loading');
// Send to backend
fetch(`${API_BASE_URL}/api/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
source: 'devbox-landing'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Hide form and show success message
emailForm.style.display = 'none';
successMessage.classList.add('show');
console.log('DevBox waitlist email submitted:', email);
// Optional: Track the successful signup
trackEmailSignup(email);
} else {
// Show error message
showError(data.message || 'Email already subscribed or invalid.');
// Reset button
resetSubmitButton(submitBtn, originalText);
}
})
.catch((error) => {
console.error('Error:', error);
showError('Network error. Please check your connection and try again.');
// Reset button
resetSubmitButton(submitBtn, originalText);
});
}
function resetSubmitButton(submitBtn, originalText) {
submitBtn.textContent = originalText;
submitBtn.disabled = false;
submitBtn.classList.remove('loading');
}
function showError(message) {
// Remove existing error messages
const existingError = document.querySelector('.error-message');
if (existingError) {
existingError.remove();
}
// Create and show error message
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.style.cssText = `
background: #ef4444;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
margin-top: 1rem;
font-size: 0.9rem;
border: 1px solid #dc2626;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
animation: slideDown 0.3s ease;
`;
errorDiv.textContent = message;
emailForm.appendChild(errorDiv);
// Remove error after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.style.animation = 'slideUp 0.3s ease';
setTimeout(() => {
errorDiv.remove();
}, 300);
}
}, 5000);
}
function trackEmailSignup(email) {
// Optional: Add analytics tracking here
// Example: Google Analytics, Mixpanel, etc.
console.log(`📧 Email signup tracked: ${email} at ${new Date().toISOString()}`);
// You could also send additional tracking data to your backend
// or third-party analytics services here
}
// Add smooth scrolling for any anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth'
});
}
});
});
// Add subtle animation to feature cards on scroll (Intersection Observer)
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe feature items for animation
document.querySelectorAll('.feature-item').forEach(item => {
// Set initial state
item.style.opacity = '0';
item.style.transform = 'translateY(20px)';
item.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
// Start observing
observer.observe(item);
});
// Add keyboard accessibility
document.addEventListener('keydown', function(e) {
// Close error messages with Escape key
if (e.key === 'Escape') {
const errorMessage = document.querySelector('.error-message');
if (errorMessage) {
errorMessage.remove();
}
}
});
// Form validation enhancements
const emailInput = document.getElementById('email');
emailInput.addEventListener('input', function() {
// Remove error styling when user starts typing
const errorMessage = document.querySelector('.error-message');
if (errorMessage) {
errorMessage.style.opacity = '0.5';
}
// Real-time validation feedback
if (this.value.length > 0) {
if (isValidEmail(this.value)) {
this.style.borderColor = '#10b981';
this.style.boxShadow = '0 0 0 1px rgba(16, 185, 129, 0.2)';
} else {
this.style.borderColor = '#ef4444';
this.style.boxShadow = '0 0 0 1px rgba(239, 68, 68, 0.2)';
}
} else {
// Reset to default styling
this.style.borderColor = '';
this.style.boxShadow = '';
}
});
// Add focus states for better accessibility
emailInput.addEventListener('focus', function() {
this.parentElement.style.boxShadow = '0 0 0 3px rgba(50, 108, 229, 0.1)';
});
emailInput.addEventListener('blur', function() {
this.parentElement.style.boxShadow = '';
// Reset border styling
this.style.borderColor = '';
this.style.boxShadow = '';
});
// Simple analytics: track page engagement
let startTime = Date.now();
let maxScroll = 0;
window.addEventListener('scroll', function() {
const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
maxScroll = Math.max(maxScroll, scrollPercent);
});
window.addEventListener('beforeunload', function() {
const timeOnPage = Math.round((Date.now() - startTime) / 1000);
console.log(`📊 Session stats: ${timeOnPage}s on page, ${Math.round(maxScroll)}% max scroll`);
// You could send this data to your analytics endpoint
// fetch('/api/analytics', { method: 'POST', body: JSON.stringify({ timeOnPage, maxScroll }) });
});
// Add some visual polish: animate elements on load
setTimeout(() => {
document.body.classList.add('loaded');
}, 100);
console.log('🚀 DevBox landing page initialized');
console.log(`📡 API endpoint: ${API_BASE_URL}`);
});
// Add CSS animations via JavaScript (if not in CSS file)
const style = document.createElement('style');
style.textContent = `
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
body {
opacity: 0;
transition: opacity 0.3s ease;
}
body.loaded {
opacity: 1;
}
`;
document.head.appendChild(style);

362
devbox-front/styles.css Normal file
View File

@ -0,0 +1,362 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Kubernetes-inspired blue color palette */
--primary-blue: #326ce5;
--dark-blue: #1e3a8a;
--light-blue: #60a5fa;
--navy: #1e293b;
--slate: #334155;
--gray-light: #f8fafc;
--gray-medium: #64748b;
--success-green: #10b981;
--text-dark: #0f172a;
--text-light: #475569;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: var(--text-dark);
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--dark-blue) 50%, var(--navy) 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
padding: 2rem 0;
text-align: center;
}
.logo h1 {
font-size: 1.8rem;
font-weight: 600;
color: white;
letter-spacing: -0.5px;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem 0;
}
.hero-section {
max-width: 700px;
margin-bottom: 4rem;
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
color: white;
margin-bottom: 1.5rem;
letter-spacing: -1px;
line-height: 1.1;
}
.hero-subtitle {
font-size: 1.3rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 3rem;
font-weight: 300;
line-height: 1.6;
}
/* Email Form */
.email-form {
margin-bottom: 2rem;
}
.input-group {
display: flex;
max-width: 520px;
margin: 0 auto 1rem;
background: white;
border-radius: 12px;
padding: 4px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(96, 165, 250, 0.3);
}
.email-input {
flex: 1;
border: none;
padding: 1.2rem 1.5rem;
font-size: 1rem;
border-radius: 8px;
outline: none;
background: transparent;
color: var(--text-dark);
}
.email-input::placeholder {
color: var(--gray-medium);
}
.submit-btn {
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--dark-blue) 100%);
color: white;
border: none;
padding: 1.2rem 2rem;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(50, 108, 229, 0.4);
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(50, 108, 229, 0.6);
background: linear-gradient(135deg, var(--light-blue) 0%, var(--primary-blue) 100%);
}
.submit-btn:active {
transform: translateY(0);
}
.form-note {
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
margin-top: 1rem;
}
/* Success Message */
.success-message {
display: none;
background: white;
border-radius: 12px;
padding: 2.5rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
max-width: 450px;
margin: 0 auto;
border: 1px solid rgba(96, 165, 250, 0.3);
}
.success-message.show {
display: block;
animation: slideUp 0.5s ease;
}
.success-icon {
font-size: 3rem;
color: var(--success-green);
margin-bottom: 1rem;
}
.success-content h3 {
color: var(--text-dark);
margin-bottom: 0.5rem;
font-size: 1.5rem;
font-weight: 600;
}
.success-content p {
color: var(--text-light);
margin: 0;
line-height: 1.5;
}
/* Features Preview */
.features-preview {
width: 100%;
max-width: 1000px;
margin-bottom: 4rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.feature-item {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 2.5rem 2rem;
text-align: center;
transition: all 0.3s ease;
}
.feature-item:hover {
transform: translateY(-8px);
background: rgba(255, 255, 255, 0.15);
border-color: rgba(96, 165, 250, 0.4);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1.5rem;
}
.feature-item h3 {
color: white;
font-size: 1.25rem;
margin-bottom: 1rem;
font-weight: 600;
}
.feature-item p {
color: rgba(255, 255, 255, 0.85);
font-size: 0.95rem;
line-height: 1.6;
}
/* Value Proposition */
.value-proposition {
max-width: 600px;
margin-bottom: 3rem;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 3rem 2.5rem;
}
.value-title {
color: white;
font-size: 2rem;
font-weight: 600;
margin-bottom: 1.5rem;
letter-spacing: -0.5px;
}
.value-text {
color: rgba(255, 255, 255, 0.9);
font-size: 1.1rem;
line-height: 1.7;
font-weight: 300;
}
/* Footer */
.footer {
padding: 2rem 0;
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 2rem;
}
.social-links {
margin-bottom: 1rem;
}
.social-link {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
margin: 0 1rem;
font-weight: 500;
transition: all 0.3s ease;
padding: 0.5rem;
border-radius: 6px;
}
.social-link:hover {
color: white;
background: rgba(255, 255, 255, 0.1);
}
.footer-text {
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
}
/* Animations */
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.1rem;
}
.input-group {
flex-direction: column;
gap: 0.5rem;
max-width: 400px;
}
.submit-btn {
width: 100%;
margin-top: 0.5rem;
}
.feature-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.container {
padding: 0 15px;
}
.value-proposition {
padding: 2rem 1.5rem;
}
.value-title {
font-size: 1.75rem;
}
}
@media (max-width: 480px) {
.hero-title {
font-size: 2rem;
}
.feature-item {
padding: 2rem 1.5rem;
}
.value-proposition {
padding: 1.5rem;
}
}
/* Loading state for button */
.submit-btn.loading {
background: var(--gray-medium);
cursor: not-allowed;
transform: none;
}
.submit-btn.loading:hover {
transform: none;
box-shadow: none;
}