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

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:
2025-12-18 11:20:18 +01:00
parent f807f2d2bc
commit 52ee772391
4 changed files with 410 additions and 226 deletions

View File

@ -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...")

View File

@ -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
View File

@ -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

View File

@ -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 },
] ]