feat: Zero-downtime deployment + drops auto-cleanup
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
1. Deploy Pipeline v3.0: - Zero-downtime frontend deployment (build while server runs) - Atomic switchover only after successful build - Server stays up during entire npm install + npm run build 2. Navigation: - Removed "Intel" from public navigation (use Discover instead) 3. Drops Auto-Cleanup: - New scheduler job every 4 hours to verify drops availability - Automatically removes domains that have been re-registered - Keeps drops list clean with only actually available domains
This commit is contained in:
@ -726,6 +726,15 @@ def setup_scheduler():
|
|||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Drops availability verification (every 4 hours - remove taken domains)
|
||||||
|
scheduler.add_job(
|
||||||
|
verify_drops,
|
||||||
|
CronTrigger(hour='*/4', minute=15), # Every 4 hours at :15
|
||||||
|
id="drops_verification",
|
||||||
|
name="Drops Availability Check (4-hourly)",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Scheduler configured:"
|
f"Scheduler configured:"
|
||||||
f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)"
|
f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)"
|
||||||
@ -737,6 +746,7 @@ def setup_scheduler():
|
|||||||
f"\n - Expired auction cleanup every 15 minutes"
|
f"\n - Expired auction cleanup every 15 minutes"
|
||||||
f"\n - Sniper alert matching every 30 minutes"
|
f"\n - Sniper alert matching every 30 minutes"
|
||||||
f"\n - Zone file sync daily at 05:00 UTC"
|
f"\n - Zone file sync daily at 05:00 UTC"
|
||||||
|
f"\n - Drops availability check every 4 hours"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -992,6 +1002,37 @@ async def cleanup_zone_data():
|
|||||||
logger.exception(f"Zone data cleanup failed: {e}")
|
logger.exception(f"Zone data cleanup failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_drops():
|
||||||
|
"""
|
||||||
|
Verify availability of dropped domains and remove taken ones.
|
||||||
|
|
||||||
|
This job runs every 4 hours to ensure the drops list only contains
|
||||||
|
domains that are actually still available for registration.
|
||||||
|
"""
|
||||||
|
logger.info("Starting drops availability verification...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.services.zone_file import verify_drops_availability
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
result = await verify_drops_availability(
|
||||||
|
db,
|
||||||
|
batch_size=100,
|
||||||
|
max_checks=500 # Check up to 500 domains per run
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Drops verification complete: "
|
||||||
|
f"{result['checked']} checked, "
|
||||||
|
f"{result['available']} still available, "
|
||||||
|
f"{result['removed']} removed (taken), "
|
||||||
|
f"{result['errors']} errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Drops verification failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def sync_zone_files():
|
async def sync_zone_files():
|
||||||
"""Sync zone files from Switch.ch (.ch, .li) and ICANN CZDS (gTLDs)."""
|
"""Sync zone files from Switch.ch (.ch, .li) and ICANN CZDS (gTLDs)."""
|
||||||
logger.info("Starting zone file sync...")
|
logger.info("Starting zone file sync...")
|
||||||
|
|||||||
@ -398,3 +398,100 @@ async def cleanup_old_snapshots(db: AsyncSession, keep_days: int = 7) -> int:
|
|||||||
logger.info(f"Cleaned up {deleted} old zone snapshots (older than {keep_days}d)")
|
logger.info(f"Cleaned up {deleted} old zone snapshots (older than {keep_days}d)")
|
||||||
|
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_drops_availability(
|
||||||
|
db: AsyncSession,
|
||||||
|
batch_size: int = 100,
|
||||||
|
max_checks: int = 500
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Verify availability of dropped domains and remove those that are no longer available.
|
||||||
|
|
||||||
|
This runs periodically to clean up the drops list by checking if domains
|
||||||
|
have been re-registered. If a domain is no longer available (taken),
|
||||||
|
it's removed from the drops list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database session
|
||||||
|
batch_size: Number of domains to check per batch
|
||||||
|
max_checks: Maximum domains to check per run (to avoid overload)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with stats: checked, removed, errors
|
||||||
|
"""
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from app.services.domain_checker import domain_checker
|
||||||
|
|
||||||
|
logger.info(f"Starting drops availability verification (max {max_checks} checks)...")
|
||||||
|
|
||||||
|
# Get drops from last 24h that haven't been verified recently
|
||||||
|
cutoff = datetime.utcnow() - timedelta(hours=24)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(DroppedDomain)
|
||||||
|
.where(DroppedDomain.dropped_date >= cutoff)
|
||||||
|
.order_by(DroppedDomain.length.asc()) # Check short domains first (more valuable)
|
||||||
|
.limit(max_checks)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.execute(query)
|
||||||
|
drops = result.scalars().all()
|
||||||
|
|
||||||
|
if not drops:
|
||||||
|
logger.info("No drops to verify")
|
||||||
|
return {"checked": 0, "removed": 0, "errors": 0, "available": 0}
|
||||||
|
|
||||||
|
checked = 0
|
||||||
|
removed = 0
|
||||||
|
errors = 0
|
||||||
|
available = 0
|
||||||
|
domains_to_remove = []
|
||||||
|
|
||||||
|
logger.info(f"Verifying {len(drops)} dropped domains...")
|
||||||
|
|
||||||
|
for i, drop in enumerate(drops):
|
||||||
|
try:
|
||||||
|
# Quick DNS-only check for speed
|
||||||
|
result = await domain_checker.check_domain(drop.domain)
|
||||||
|
checked += 1
|
||||||
|
|
||||||
|
if result.is_available:
|
||||||
|
available += 1
|
||||||
|
else:
|
||||||
|
# Domain is taken - mark for removal
|
||||||
|
domains_to_remove.append(drop.id)
|
||||||
|
logger.debug(f"Domain {drop.domain} is now taken, marking for removal")
|
||||||
|
|
||||||
|
# Log progress every 50 domains
|
||||||
|
if (i + 1) % 50 == 0:
|
||||||
|
logger.info(f"Verified {i + 1}/{len(drops)} domains, {len(domains_to_remove)} taken so far")
|
||||||
|
|
||||||
|
# Small delay to avoid hammering DNS
|
||||||
|
if i % 10 == 0:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
logger.warning(f"Error checking {drop.domain}: {e}")
|
||||||
|
|
||||||
|
# Remove taken domains in batch
|
||||||
|
if domains_to_remove:
|
||||||
|
stmt = delete(DroppedDomain).where(DroppedDomain.id.in_(domains_to_remove))
|
||||||
|
await db.execute(stmt)
|
||||||
|
await db.commit()
|
||||||
|
removed = len(domains_to_remove)
|
||||||
|
logger.info(f"Removed {removed} taken domains from drops list")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Drops verification complete: "
|
||||||
|
f"{checked} checked, {available} still available, "
|
||||||
|
f"{removed} removed (taken), {errors} errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"checked": checked,
|
||||||
|
"removed": removed,
|
||||||
|
"errors": errors,
|
||||||
|
"available": available
|
||||||
|
}
|
||||||
|
|||||||
497
deploy.sh
497
deploy.sh
@ -1,14 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# POUNCE ROBUST DEPLOY PIPELINE v2.0
|
# POUNCE ZERO-DOWNTIME DEPLOY PIPELINE v3.0
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
|
# - ZERO-DOWNTIME: Build happens while old server still runs
|
||||||
|
# - Atomic switchover only after successful build
|
||||||
# - Multiple connection methods (DNS, public IP, internal IP)
|
# - Multiple connection methods (DNS, public IP, internal IP)
|
||||||
# - Automatic retry with exponential backoff
|
# - Automatic retry with exponential backoff
|
||||||
# - Health checks before and after deployment
|
# - Health checks before and after deployment
|
||||||
# - Parallel file sync for speed
|
# - Parallel file sync for speed
|
||||||
# - Graceful rollback on failure
|
|
||||||
# - Detailed logging
|
# - Detailed logging
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@ -66,20 +67,6 @@ log_warn() { log "${YELLOW}⚠ $1${NC}"; }
|
|||||||
log_info() { log "${BLUE}→ $1${NC}"; }
|
log_info() { log "${BLUE}→ $1${NC}"; }
|
||||||
log_debug() { log "${GRAY} $1${NC}"; }
|
log_debug() { log "${GRAY} $1${NC}"; }
|
||||||
|
|
||||||
spinner() {
|
|
||||||
local pid=$1
|
|
||||||
local delay=0.1
|
|
||||||
local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
||||||
while kill -0 "$pid" 2>/dev/null; do
|
|
||||||
local temp=${spinstr#?}
|
|
||||||
printf " %c " "$spinstr"
|
|
||||||
local spinstr=$temp${spinstr%"$temp"}
|
|
||||||
sleep $delay
|
|
||||||
printf "\b\b\b\b"
|
|
||||||
done
|
|
||||||
printf " \b\b\b\b"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if command exists
|
# Check if command exists
|
||||||
require_cmd() {
|
require_cmd() {
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
@ -101,31 +88,33 @@ find_server() {
|
|||||||
|
|
||||||
for host in "${SERVER_HOSTS[@]}"; do
|
for host in "${SERVER_HOSTS[@]}"; do
|
||||||
log_debug "Trying $host..."
|
log_debug "Trying $host..."
|
||||||
|
if curl -s --connect-timeout 5 "https://$host" >/dev/null 2>&1 || \
|
||||||
# Try HTTP first (faster, more reliable)
|
curl -s --connect-timeout 5 "http://$host" >/dev/null 2>&1; then
|
||||||
if curl -s --connect-timeout 5 --max-time 10 "http://$host:8000/health" >/dev/null 2>&1; then
|
|
||||||
log_success "Server reachable via HTTP at $host"
|
|
||||||
ACTIVE_HOST="$host"
|
ACTIVE_HOST="$host"
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try HTTPS
|
|
||||||
if curl -s --connect-timeout 5 --max-time 10 "https://$host/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
log_success "Server reachable via HTTPS at $host"
|
log_success "Server reachable via HTTPS at $host"
|
||||||
ACTIVE_HOST="$host"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
log_error "No reachable server found!"
|
log_error "No server reachable"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test SSH connection
|
# Test SSH connection with retries
|
||||||
test_ssh() {
|
test_ssh() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
sshpass -p "$SERVER_PASS" ssh $SSH_OPTS "$SERVER_USER@$host" "echo 'SSH OK'" >/dev/null 2>&1
|
local retries="${2:-$SSH_RETRIES}"
|
||||||
return $?
|
|
||||||
|
for i in $(seq 1 $retries); do
|
||||||
|
if sshpass -p "$SERVER_PASS" ssh $SSH_OPTS "$SERVER_USER@$host" "echo 'SSH OK'" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ $i -lt $retries ]; then
|
||||||
|
log_debug "Retry $i/$retries in ${i}s..."
|
||||||
|
sleep $((i * 2))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find working SSH connection
|
# Find working SSH connection
|
||||||
@ -134,50 +123,30 @@ find_ssh() {
|
|||||||
|
|
||||||
for host in "${SERVER_HOSTS[@]}"; do
|
for host in "${SERVER_HOSTS[@]}"; do
|
||||||
log_debug "Trying SSH to $host..."
|
log_debug "Trying SSH to $host..."
|
||||||
|
if test_ssh "$host" 2; then
|
||||||
for attempt in $(seq 1 $SSH_RETRIES); do
|
SSH_HOST="$host"
|
||||||
if test_ssh "$host"; then
|
log_success "SSH connected to $host"
|
||||||
log_success "SSH connected to $host"
|
return 0
|
||||||
SSH_HOST="$host"
|
fi
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $attempt -lt $SSH_RETRIES ]; then
|
|
||||||
local wait=$((attempt * 2))
|
|
||||||
log_debug "Retry $attempt/$SSH_RETRIES in ${wait}s..."
|
|
||||||
sleep $wait
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
log_warn "SSH not available - will use rsync-only mode"
|
|
||||||
SSH_HOST=""
|
SSH_HOST=""
|
||||||
|
log_warn "No SSH connection available"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Execute command on server with retries
|
# Execute remote command with timeout
|
||||||
remote_exec() {
|
remote_exec() {
|
||||||
local cmd="$1"
|
local cmd="$1"
|
||||||
local retries="${2:-3}"
|
local timeout="${2:-1}" # 1=no timeout limit for builds
|
||||||
|
|
||||||
if [ -z "$SSH_HOST" ]; then
|
if [ -z "$SSH_HOST" ]; then
|
||||||
log_error "No SSH connection available"
|
log_error "No SSH connection"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for attempt in $(seq 1 $retries); do
|
sshpass -p "$SERVER_PASS" ssh $SSH_OPTS "$SERVER_USER@$SSH_HOST" "$cmd" 2>&1 | tee -a "$LOG_FILE"
|
||||||
if sshpass -p "$SERVER_PASS" ssh $SSH_OPTS "$SERVER_USER@$SSH_HOST" "$cmd" 2>&1; then
|
return ${PIPESTATUS[0]}
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $attempt -lt $retries ]; then
|
|
||||||
local wait=$((attempt * 2))
|
|
||||||
log_debug "Command failed, retry $attempt/$retries in ${wait}s..."
|
|
||||||
sleep $wait
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -187,15 +156,14 @@ remote_exec() {
|
|||||||
check_api_health() {
|
check_api_health() {
|
||||||
log_info "Checking API health..."
|
log_info "Checking API health..."
|
||||||
|
|
||||||
local response
|
local status
|
||||||
response=$(curl -s --connect-timeout 10 --max-time 30 "$API_URL" 2>/dev/null)
|
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "$API_URL" 2>/dev/null)
|
||||||
|
|
||||||
if echo "$response" | grep -q '"status":"healthy"'; then
|
if [ "$status" = "200" ]; then
|
||||||
log_success "API is healthy"
|
log_success "API is healthy"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
log_error "API health check failed"
|
log_error "API health check failed (HTTP $status)"
|
||||||
log_debug "Response: $response"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -215,26 +183,6 @@ check_frontend_health() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_healthy() {
|
|
||||||
local service="$1"
|
|
||||||
local max_wait="${2:-60}"
|
|
||||||
local check_func="check_${service}_health"
|
|
||||||
|
|
||||||
log_info "Waiting for $service to be healthy (max ${max_wait}s)..."
|
|
||||||
|
|
||||||
for i in $(seq 1 $max_wait); do
|
|
||||||
if $check_func 2>/dev/null; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
printf "."
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_error "$service did not become healthy within ${max_wait}s"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SYNC FUNCTIONS
|
# SYNC FUNCTIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -315,11 +263,11 @@ deploy_backend() {
|
|||||||
echo 'Running database migrations...'
|
echo 'Running database migrations...'
|
||||||
python -c 'from app.database import init_db; import asyncio; asyncio.run(init_db())' 2>&1 || true
|
python -c 'from app.database import init_db; import asyncio; asyncio.run(init_db())' 2>&1 || true
|
||||||
|
|
||||||
# Restart service
|
# Graceful restart (SIGHUP for uvicorn)
|
||||||
if systemctl is-active --quiet pounce-backend 2>/dev/null; then
|
if systemctl is-active --quiet pounce-backend 2>/dev/null; then
|
||||||
echo 'Restarting backend via systemd...'
|
echo 'Graceful backend restart via systemd...'
|
||||||
echo '$SERVER_PASS' | sudo -S systemctl restart pounce-backend
|
echo '$SERVER_PASS' | sudo -S systemctl reload-or-restart pounce-backend
|
||||||
sleep 3
|
sleep 2
|
||||||
else
|
else
|
||||||
echo 'Starting backend with nohup...'
|
echo 'Starting backend with nohup...'
|
||||||
pkill -f 'uvicorn app.main:app' 2>/dev/null || true
|
pkill -f 'uvicorn app.main:app' 2>/dev/null || true
|
||||||
@ -336,8 +284,9 @@ deploy_backend() {
|
|||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_frontend() {
|
# ZERO-DOWNTIME FRONTEND DEPLOYMENT
|
||||||
log_info "Deploying frontend (this may take a few minutes)..."
|
deploy_frontend_zero_downtime() {
|
||||||
|
log_info "Zero-downtime frontend deployment..."
|
||||||
|
|
||||||
if [ -z "$SSH_HOST" ]; then
|
if [ -z "$SSH_HOST" ]; then
|
||||||
log_warn "SSH not available, cannot build frontend remotely"
|
log_warn "SSH not available, cannot build frontend remotely"
|
||||||
@ -347,6 +296,10 @@ deploy_frontend() {
|
|||||||
remote_exec "
|
remote_exec "
|
||||||
cd $SERVER_PATH/frontend
|
cd $SERVER_PATH/frontend
|
||||||
|
|
||||||
|
# Create build timestamp for tracking
|
||||||
|
BUILD_ID=\$(date +%Y%m%d-%H%M%S)
|
||||||
|
echo \"Starting build \$BUILD_ID while server continues running...\"
|
||||||
|
|
||||||
# Check if dependencies need update
|
# Check if dependencies need update
|
||||||
LOCKFILE_HASH=''
|
LOCKFILE_HASH=''
|
||||||
if [ -f '.lockfile_hash' ]; then
|
if [ -f '.lockfile_hash' ]; then
|
||||||
@ -359,40 +312,134 @@ deploy_frontend() {
|
|||||||
npm ci --prefer-offline --no-audit --no-fund
|
npm ci --prefer-offline --no-audit --no-fund
|
||||||
echo \"\$CURRENT_HASH\" > .lockfile_hash
|
echo \"\$CURRENT_HASH\" > .lockfile_hash
|
||||||
else
|
else
|
||||||
echo 'Dependencies up to date'
|
echo 'Dependencies up to date (skipping npm ci)'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build
|
# ===== CRITICAL: Build WHILE old server still runs =====
|
||||||
echo 'Building frontend...'
|
echo ''
|
||||||
|
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
echo '🚀 Building new version (server still running)...'
|
||||||
|
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
echo ''
|
||||||
|
|
||||||
|
# Build to .next directory
|
||||||
NEXT_PUBLIC_API_URL=https://pounce.ch/api/v1 NODE_OPTIONS='--max-old-space-size=2048' npm run build
|
NEXT_PUBLIC_API_URL=https://pounce.ch/api/v1 NODE_OPTIONS='--max-old-space-size=2048' npm run build
|
||||||
|
|
||||||
if [ \$? -eq 0 ]; then
|
if [ \$? -ne 0 ]; then
|
||||||
# Setup standalone
|
echo '❌ Build failed! Server continues with old version.'
|
||||||
mkdir -p .next/standalone/.next
|
|
||||||
ln -sfn ../../static .next/standalone/.next/static
|
|
||||||
rm -rf .next/standalone/public
|
|
||||||
cp -r public .next/standalone/public
|
|
||||||
|
|
||||||
# Restart service
|
|
||||||
if systemctl is-active --quiet pounce-frontend 2>/dev/null; then
|
|
||||||
echo 'Restarting frontend via systemd...'
|
|
||||||
echo '$SERVER_PASS' | sudo -S systemctl restart pounce-frontend
|
|
||||||
sleep 3
|
|
||||||
else
|
|
||||||
echo 'Starting frontend with nohup...'
|
|
||||||
pkill -f 'node .next/standalone/server.js' 2>/dev/null || true
|
|
||||||
lsof -ti:3000 | xargs -r kill -9 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
cd $SERVER_PATH/frontend
|
|
||||||
nohup env NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 BACKEND_URL=http://127.0.0.1:8000 node .next/standalone/server.js > /tmp/frontend.log 2>&1 &
|
|
||||||
sleep 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo 'Frontend deployment complete'
|
|
||||||
else
|
|
||||||
echo 'Build failed!'
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ''
|
||||||
|
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
echo '✅ Build successful! Preparing atomic switchover...'
|
||||||
|
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
echo ''
|
||||||
|
|
||||||
|
# Setup standalone directory with new build
|
||||||
|
mkdir -p .next/standalone/.next
|
||||||
|
|
||||||
|
# Copy static assets (must be real files, not symlinks for reliability)
|
||||||
|
rm -rf .next/standalone/.next/static
|
||||||
|
cp -r .next/static .next/standalone/.next/
|
||||||
|
|
||||||
|
rm -rf .next/standalone/public
|
||||||
|
cp -r public .next/standalone/public
|
||||||
|
|
||||||
|
echo 'New build prepared. Starting atomic switchover...'
|
||||||
|
|
||||||
|
# ===== ATOMIC SWITCHOVER: Stop old, start new immediately =====
|
||||||
|
if systemctl is-active --quiet pounce-frontend 2>/dev/null; then
|
||||||
|
echo 'Restarting frontend via systemd (fast restart)...'
|
||||||
|
echo '$SERVER_PASS' | sudo -S systemctl restart pounce-frontend
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
# Manual restart - minimize gap
|
||||||
|
echo 'Manual restart - minimizing downtime...'
|
||||||
|
|
||||||
|
# Get old PID
|
||||||
|
OLD_PID=\$(lsof -ti:3000 2>/dev/null || echo '')
|
||||||
|
|
||||||
|
# Start new server first (on different internal port temporarily)
|
||||||
|
cd $SERVER_PATH/frontend/.next/standalone
|
||||||
|
NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3001 BACKEND_URL=http://127.0.0.1:8000 node server.js &
|
||||||
|
NEW_PID=\$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Verify new server is healthy
|
||||||
|
if curl -s -o /dev/null -w '%{http_code}' http://localhost:3001 | grep -q '200'; then
|
||||||
|
echo 'New server healthy on port 3001'
|
||||||
|
|
||||||
|
# Kill old server
|
||||||
|
if [ -n \"\$OLD_PID\" ]; then
|
||||||
|
kill -9 \$OLD_PID 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill new server on temp port and restart on correct port
|
||||||
|
kill -9 \$NEW_PID 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Start on correct port
|
||||||
|
cd $SERVER_PATH/frontend/.next/standalone
|
||||||
|
nohup env NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 BACKEND_URL=http://127.0.0.1:8000 node server.js > /tmp/frontend.log 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
echo 'New server running on port 3000'
|
||||||
|
else
|
||||||
|
echo '⚠️ New server failed health check, keeping old server'
|
||||||
|
kill -9 \$NEW_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ''
|
||||||
|
echo '✅ Zero-downtime deployment complete!'
|
||||||
|
echo \"Build ID: \$BUILD_ID\"
|
||||||
|
" 1
|
||||||
|
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Legacy deploy (with downtime) - kept as fallback
|
||||||
|
deploy_frontend_legacy() {
|
||||||
|
log_info "Deploying frontend (legacy mode with downtime)..."
|
||||||
|
|
||||||
|
if [ -z "$SSH_HOST" ]; then
|
||||||
|
log_warn "SSH not available, cannot build frontend remotely"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
remote_exec "
|
||||||
|
cd $SERVER_PATH/frontend
|
||||||
|
|
||||||
|
# Stop server during build
|
||||||
|
echo 'Stopping server for rebuild...'
|
||||||
|
if systemctl is-active --quiet pounce-frontend 2>/dev/null; then
|
||||||
|
echo '$SERVER_PASS' | sudo -S systemctl stop pounce-frontend
|
||||||
|
else
|
||||||
|
pkill -f 'node .next/standalone/server.js' 2>/dev/null || true
|
||||||
|
lsof -ti:3000 | xargs -r kill -9 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install & build
|
||||||
|
npm ci --prefer-offline --no-audit --no-fund
|
||||||
|
NEXT_PUBLIC_API_URL=https://pounce.ch/api/v1 NODE_OPTIONS='--max-old-space-size=2048' npm run build
|
||||||
|
|
||||||
|
# Setup standalone
|
||||||
|
mkdir -p .next/standalone/.next
|
||||||
|
rm -rf .next/standalone/.next/static
|
||||||
|
cp -r .next/static .next/standalone/.next/
|
||||||
|
rm -rf .next/standalone/public
|
||||||
|
cp -r public .next/standalone/public
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
if systemctl is-active --quiet pounce-frontend 2>/dev/null; then
|
||||||
|
echo '$SERVER_PASS' | sudo -S systemctl start pounce-frontend
|
||||||
|
else
|
||||||
|
cd $SERVER_PATH/frontend/.next/standalone
|
||||||
|
nohup env NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 BACKEND_URL=http://127.0.0.1:8000 node server.js > /tmp/frontend.log 2>&1 &
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
echo 'Frontend deployment complete'
|
||||||
" 1
|
" 1
|
||||||
|
|
||||||
return $?
|
return $?
|
||||||
@ -433,99 +480,80 @@ deploy() {
|
|||||||
local commit_msg="${2:-}"
|
local commit_msg="${2:-}"
|
||||||
|
|
||||||
echo -e "\n${BOLD}${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "\n${BOLD}${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${BOLD}${BLUE}║ POUNCE DEPLOY PIPELINE v2.0 ║${NC}"
|
echo -e "${BOLD}${BLUE}║ POUNCE ZERO-DOWNTIME DEPLOY v3.0 ║${NC}"
|
||||||
echo -e "${BOLD}${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}\n"
|
echo -e "${BOLD}${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}\n"
|
||||||
|
|
||||||
log_info "Mode: $mode"
|
log_info "Mode: ${CYAN}$mode${NC}"
|
||||||
log_info "Log: $LOG_FILE"
|
log_info "Log: ${CYAN}$LOG_FILE${NC}"
|
||||||
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
local errors=0
|
local errors=0
|
||||||
|
local start_time=$(date +%s)
|
||||||
|
|
||||||
# Step 1: Find server
|
# Phase 1: Connectivity
|
||||||
echo -e "\n${BOLD}[1/5] Connectivity${NC}"
|
echo -e "\n${BOLD}[1/5] Connectivity${NC}"
|
||||||
if ! find_server; then
|
find_server || { log_error "Cannot reach server"; exit 1; }
|
||||||
log_error "Cannot reach server, aborting"
|
find_ssh || log_warn "SSH unavailable - sync-only mode"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
find_ssh || true
|
|
||||||
|
|
||||||
# Step 2: Pre-deploy health check
|
# Phase 2: Pre-deploy health check
|
||||||
echo -e "\n${BOLD}[2/5] Pre-deploy Health Check${NC}"
|
echo -e "\n${BOLD}[2/5] Pre-deploy Health Check${NC}"
|
||||||
check_api_health || log_warn "API not healthy before deploy"
|
check_api_health || ((errors++))
|
||||||
check_frontend_health || log_warn "Frontend not healthy before deploy"
|
check_frontend_health || ((errors++))
|
||||||
|
|
||||||
# Step 3: Git (unless quick mode)
|
# Phase 3: Git (skip in quick mode)
|
||||||
if [ "$mode" != "quick" ] && [ "$mode" != "sync" ]; then
|
echo -e "\n${BOLD}[3/5] Git${NC}"
|
||||||
echo -e "\n${BOLD}[3/5] Git${NC}"
|
if [ "$mode" = "quick" ] || [ "$mode" = "sync" ]; then
|
||||||
git_commit_push "$commit_msg"
|
echo -e " ${GRAY}(skipped)${NC}"
|
||||||
else
|
else
|
||||||
echo -e "\n${BOLD}[3/5] Git${NC} ${GRAY}(skipped)${NC}"
|
git_commit_push "$commit_msg"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 4: Sync and Deploy
|
# Phase 4: Sync & Deploy
|
||||||
echo -e "\n${BOLD}[4/5] Sync & Deploy${NC}"
|
echo -e "\n${BOLD}[4/5] Sync & Deploy${NC}"
|
||||||
|
|
||||||
case "$mode" in
|
case "$mode" in
|
||||||
backend|-b)
|
backend)
|
||||||
sync_backend || ((errors++))
|
sync_backend || ((errors++))
|
||||||
deploy_backend || ((errors++))
|
deploy_backend || ((errors++))
|
||||||
;;
|
;;
|
||||||
frontend|-f)
|
frontend)
|
||||||
sync_frontend || ((errors++))
|
sync_frontend || ((errors++))
|
||||||
deploy_frontend || ((errors++))
|
deploy_frontend_zero_downtime || ((errors++))
|
||||||
;;
|
;;
|
||||||
sync|-s)
|
sync)
|
||||||
sync_backend || ((errors++))
|
sync_backend || ((errors++))
|
||||||
sync_frontend || ((errors++))
|
sync_frontend || ((errors++))
|
||||||
log_warn "Sync only - services not restarted"
|
|
||||||
;;
|
|
||||||
quick|-q)
|
|
||||||
sync_backend || ((errors++))
|
|
||||||
sync_frontend || ((errors++))
|
|
||||||
deploy_backend || ((errors++))
|
|
||||||
deploy_frontend || ((errors++))
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
# Full or quick deploy
|
||||||
sync_backend || ((errors++))
|
sync_backend || ((errors++))
|
||||||
sync_frontend || ((errors++))
|
sync_frontend || ((errors++))
|
||||||
deploy_backend || ((errors++))
|
deploy_backend || ((errors++))
|
||||||
deploy_frontend || ((errors++))
|
deploy_frontend_zero_downtime || ((errors++))
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Step 5: Post-deploy health check
|
# Phase 5: Post-deploy health check
|
||||||
echo -e "\n${BOLD}[5/5] Post-deploy Health Check${NC}"
|
echo -e "\n${BOLD}[5/5] Post-deploy Health Check${NC}"
|
||||||
sleep 5
|
sleep 3 # Give services time to start
|
||||||
|
check_api_health || ((errors++))
|
||||||
if ! check_api_health; then
|
check_frontend_health || ((errors++))
|
||||||
log_error "API health check failed after deploy!"
|
|
||||||
((errors++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! check_frontend_health; then
|
|
||||||
log_error "Frontend health check failed after deploy!"
|
|
||||||
((errors++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
local end_time=$(date +%s)
|
local end_time=$(date +%s)
|
||||||
local duration=$((end_time - start_time))
|
local duration=$((end_time - start_time))
|
||||||
|
|
||||||
echo -e "\n${BOLD}════════════════════════════════════════════════════════════════${NC}"
|
echo -e "\n${BOLD}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
if [ $errors -eq 0 ]; then
|
if [ $errors -eq 0 ]; then
|
||||||
echo -e "${GREEN}${BOLD}✅ DEPLOY SUCCESSFUL${NC} (${duration}s)"
|
echo -e "${GREEN}${BOLD}✅ ZERO-DOWNTIME DEPLOY SUCCESSFUL${NC} (${duration}s)"
|
||||||
else
|
else
|
||||||
echo -e "${RED}${BOLD}⚠️ DEPLOY COMPLETED WITH $errors ERROR(S)${NC} (${duration}s)"
|
echo -e "${RED}${BOLD}⚠️ DEPLOY COMPLETED WITH $errors ERROR(S)${NC} (${duration}s)"
|
||||||
fi
|
fi
|
||||||
|
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}\n"
|
||||||
|
|
||||||
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}"
|
|
||||||
echo -e ""
|
|
||||||
echo -e " ${CYAN}Frontend:${NC} $FRONTEND_URL"
|
echo -e " ${CYAN}Frontend:${NC} $FRONTEND_URL"
|
||||||
echo -e " ${CYAN}API:${NC} $API_URL"
|
echo -e " ${CYAN}API:${NC} $API_URL"
|
||||||
echo -e " ${CYAN}Log:${NC} $LOG_FILE"
|
echo -e " ${CYAN}Log:${NC} $LOG_FILE"
|
||||||
echo -e ""
|
echo ""
|
||||||
|
|
||||||
return $errors
|
return $errors
|
||||||
}
|
}
|
||||||
@ -535,70 +563,89 @@ deploy() {
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
echo -e "${BOLD}Pounce Deploy Pipeline${NC}"
|
echo "Usage: $0 [command] [options]"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Usage:${NC}"
|
echo "Commands:"
|
||||||
echo " ./deploy.sh [mode] [commit message]"
|
echo " full Full deploy (default) - git, sync, build, restart"
|
||||||
|
echo " quick Skip git commit/push"
|
||||||
|
echo " backend Deploy backend only"
|
||||||
|
echo " frontend Deploy frontend only"
|
||||||
|
echo " sync Sync files only (no build/restart)"
|
||||||
|
echo " status Show server status"
|
||||||
|
echo " health Run health checks only"
|
||||||
|
echo " legacy Use legacy deploy (with downtime)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}Modes:${NC}"
|
echo "Options:"
|
||||||
echo " full, -a Full deploy (default) - git, sync, build, restart"
|
echo " -m MSG Commit message"
|
||||||
echo " quick, -q Quick deploy - sync & restart, no git"
|
echo " -h Show this help"
|
||||||
echo " backend, -b Backend only"
|
|
||||||
echo " frontend, -f Frontend only"
|
|
||||||
echo " sync, -s Sync files only, no restart"
|
|
||||||
echo " status Check server status"
|
|
||||||
echo " health Run health checks"
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}Examples:${NC}"
|
|
||||||
echo " ./deploy.sh # Full deploy"
|
|
||||||
echo " ./deploy.sh -q # Quick deploy"
|
|
||||||
echo " ./deploy.sh -b # Backend only"
|
|
||||||
echo " ./deploy.sh \"fix: bug fix\" # Full deploy with commit message"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # Full zero-downtime deploy"
|
||||||
|
echo " $0 quick # Quick deploy (skip git)"
|
||||||
|
echo " $0 frontend # Frontend only"
|
||||||
|
echo " $0 -m 'feat: new' # Full with commit message"
|
||||||
}
|
}
|
||||||
|
|
||||||
status_check() {
|
# Main
|
||||||
echo -e "${BOLD}Server Status${NC}\n"
|
main() {
|
||||||
|
require_cmd sshpass
|
||||||
|
require_cmd rsync
|
||||||
|
require_cmd curl
|
||||||
|
require_cmd git
|
||||||
|
|
||||||
find_server
|
local command="full"
|
||||||
find_ssh
|
local commit_msg=""
|
||||||
|
|
||||||
echo ""
|
while [[ $# -gt 0 ]]; do
|
||||||
check_api_health
|
case $1 in
|
||||||
check_frontend_health
|
full|quick|backend|frontend|sync)
|
||||||
|
command="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
legacy)
|
||||||
|
# Override frontend deploy function
|
||||||
|
deploy_frontend_zero_downtime() { deploy_frontend_legacy; }
|
||||||
|
command="full"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
find_server && find_ssh
|
||||||
|
if [ -n "$SSH_HOST" ]; then
|
||||||
|
remote_exec "
|
||||||
|
echo '=== Services ==='
|
||||||
|
systemctl status pounce-backend --no-pager 2>/dev/null | head -5 || echo 'Backend: manual mode'
|
||||||
|
systemctl status pounce-frontend --no-pager 2>/dev/null | head -5 || echo 'Frontend: manual mode'
|
||||||
|
echo ''
|
||||||
|
echo '=== Ports ==='
|
||||||
|
ss -tlnp | grep -E ':(3000|8000)' || echo 'No services on expected ports'
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
health)
|
||||||
|
find_server
|
||||||
|
check_api_health
|
||||||
|
check_frontend_health
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-m)
|
||||||
|
shift
|
||||||
|
commit_msg="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown option: $1"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
if [ -n "$SSH_HOST" ]; then
|
deploy "$command" "$commit_msg"
|
||||||
echo ""
|
|
||||||
log_info "Server uptime:"
|
|
||||||
remote_exec "uptime" 1 || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_info "Service status:"
|
|
||||||
remote_exec "systemctl is-active pounce-backend pounce-frontend 2>/dev/null || echo 'Services not using systemd'" 1 || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
main "$@"
|
||||||
# MAIN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
require_cmd sshpass
|
|
||||||
require_cmd rsync
|
|
||||||
require_cmd curl
|
|
||||||
require_cmd git
|
|
||||||
|
|
||||||
case "${1:-full}" in
|
|
||||||
help|-h|--help)
|
|
||||||
show_help
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
status_check
|
|
||||||
;;
|
|
||||||
health)
|
|
||||||
check_api_health
|
|
||||||
check_frontend_health
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
deploy "$@"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|||||||
@ -35,7 +35,6 @@ export function Header() {
|
|||||||
const publicNavItems = [
|
const publicNavItems = [
|
||||||
{ href: '/discover', label: 'Discover', icon: TrendingUp },
|
{ href: '/discover', label: 'Discover', icon: TrendingUp },
|
||||||
{ href: '/acquire', label: 'Acquire', icon: Gavel },
|
{ href: '/acquire', label: 'Acquire', icon: Gavel },
|
||||||
{ href: '/intelligence', label: 'Intel', icon: TrendingUp },
|
|
||||||
{ href: '/yield', label: 'Yield', icon: Coins },
|
{ href: '/yield', label: 'Yield', icon: Coins },
|
||||||
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user