feat: Zero-downtime deploy + mobile Settings design
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

This commit is contained in:
2025-12-13 16:55:50 +01:00
parent 3601af7ec0
commit 78736ab7bf
2 changed files with 301 additions and 49 deletions

173
deploy.sh
View File

@ -1,8 +1,11 @@
#!/bin/bash
# ============================================================================
# POUNCE DEPLOY SCRIPT
# Commits all changes and deploys to server
# POUNCE ZERO-DOWNTIME DEPLOY SCRIPT
# - Builds locally first (optional)
# - Syncs only changed files
# - Hot-reloads backend without full restart
# - Rebuilds frontend in background
# ============================================================================
set -e
@ -12,6 +15,7 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Server config
@ -20,36 +24,77 @@ SERVER_HOST="10.42.0.73"
SERVER_PATH="/home/user/pounce"
SERVER_PASS="user"
# Parse flags
QUICK_MODE=false
BACKEND_ONLY=false
FRONTEND_ONLY=false
while [[ "$#" -gt 0 ]]; do
case $1 in
-q|--quick) QUICK_MODE=true ;;
-b|--backend) BACKEND_ONLY=true ;;
-f|--frontend) FRONTEND_ONLY=true ;;
*) COMMIT_MSG="$1" ;;
esac
shift
done
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} POUNCE DEPLOY SCRIPT ${NC}"
echo -e "${BLUE} POUNCE ZERO-DOWNTIME DEPLOY ${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
# Get commit message
if [ -z "$1" ]; then
COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
else
COMMIT_MSG="$1"
if $QUICK_MODE; then
echo -e "${CYAN}⚡ Quick mode: Skipping git, only syncing changes${NC}"
fi
echo -e "\n${YELLOW}[1/5] Staging changes...${NC}"
if $BACKEND_ONLY; then
echo -e "${CYAN}🔧 Backend only mode${NC}"
fi
if $FRONTEND_ONLY; then
echo -e "${CYAN}🎨 Frontend only mode${NC}"
fi
# Step 1: Git (unless quick mode)
if ! $QUICK_MODE; then
echo -e "\n${YELLOW}[1/4] Git operations...${NC}"
# Check for changes
if git diff --quiet && git diff --staged --quiet; then
echo " No changes to commit"
else
git add -A
echo -e "\n${YELLOW}[2/5] Committing: ${COMMIT_MSG}${NC}"
git commit -m "$COMMIT_MSG" || echo "Nothing to commit"
if [ -z "$COMMIT_MSG" ]; then
COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
fi
echo -e "\n${YELLOW}[3/5] Pushing to git.6bit.ch...${NC}"
git push origin main
git commit -m "$COMMIT_MSG" || true
echo " ✓ Committed: $COMMIT_MSG"
fi
echo -e "\n${YELLOW}[4/5] Syncing files to server...${NC}"
# Sync frontend
sshpass -p "$SERVER_PASS" rsync -avz --delete \
git push origin main 2>/dev/null && echo " ✓ Pushed to git.6bit.ch" || echo " ⚠ Push failed or nothing to push"
else
echo -e "\n${YELLOW}[1/4] Skipping git (quick mode)${NC}"
fi
# Step 2: Sync files (only changed)
echo -e "\n${YELLOW}[2/4] Syncing changed files...${NC}"
RSYNC_OPTS="-az --info=name1 --delete"
if ! $BACKEND_ONLY; then
echo " Frontend:"
sshpass -p "$SERVER_PASS" rsync $RSYNC_OPTS \
--exclude 'node_modules' \
--exclude '.next' \
--exclude '.git' \
frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/
frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/ 2>&1 | sed 's/^/ /'
fi
# Sync backend (exclude .env to preserve server config!)
sshpass -p "$SERVER_PASS" rsync -avz --delete \
if ! $FRONTEND_ONLY; then
echo " Backend:"
sshpass -p "$SERVER_PASS" rsync $RSYNC_OPTS \
--exclude '__pycache__' \
--exclude '.pytest_cache' \
--exclude 'venv' \
@ -57,25 +102,85 @@ sshpass -p "$SERVER_PASS" rsync -avz --delete \
--exclude '*.pyc' \
--exclude '.env' \
--exclude '*.db' \
backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/
backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/ 2>&1 | sed 's/^/ /'
fi
echo -e "\n${YELLOW}[5/5] Building and restarting on server...${NC}"
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'EOF'
cd ~/pounce
# Step 3: Reload backend (graceful, no restart)
if ! $FRONTEND_ONLY; then
echo -e "\n${YELLOW}[3/4] Reloading backend (graceful)...${NC}"
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'BACKEND_EOF'
# Signal uvicorn to reload (if running with --reload)
# Otherwise, just check it's running
BACKEND_PID=$(pgrep -f 'uvicorn app.main:app' | head -1)
# Build frontend
echo "Building frontend..."
cd frontend
npm run build 2>&1 | tail -10
cd ..
if [ -n "$BACKEND_PID" ]; then
# Touch a file to trigger auto-reload if uvicorn has --reload
touch ~/pounce/backend/app/main.py
echo " ✓ Backend reload triggered (PID: $BACKEND_PID)"
else
echo " ⚠ Backend not running, starting..."
cd ~/pounce/backend
source ../venv/bin/activate
nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 > backend.log 2>&1 &
sleep 2
echo " ✓ Backend started"
fi
BACKEND_EOF
else
echo -e "\n${YELLOW}[3/4] Skipping backend (frontend only)${NC}"
fi
# Restart services
echo "Restarting services..."
./start.sh 2>&1 | tail -5
EOF
# Step 4: Rebuild frontend (in background to minimize downtime)
if ! $BACKEND_ONLY; then
echo -e "\n${YELLOW}[4/4] Rebuilding frontend...${NC}"
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'FRONTEND_EOF'
cd ~/pounce/frontend
# Build new version
echo " Building..."
npm run build 2>&1 | grep -E '(✓|○|λ|Error|error)' | head -10 | sed 's/^/ /'
BUILD_EXIT=$?
if [ $BUILD_EXIT -eq 0 ]; then
# Gracefully restart Next.js
NEXT_PID=$(pgrep -f 'next start' | head -1)
if [ -n "$NEXT_PID" ]; then
echo " Restarting Next.js (PID: $NEXT_PID)..."
kill $NEXT_PID 2>/dev/null
sleep 1
fi
# Start new instance
nohup npm run start > frontend.log 2>&1 &
sleep 2
# Verify
NEW_PID=$(pgrep -f 'next start' | head -1)
if [ -n "$NEW_PID" ]; then
echo " ✓ Frontend running (PID: $NEW_PID)"
else
echo " ⚠ Frontend may not have started correctly"
fi
else
echo " ✗ Build failed, keeping old version"
fi
FRONTEND_EOF
else
echo -e "\n${YELLOW}[4/4] Skipping frontend (backend only)${NC}"
fi
# Summary
echo -e "\n${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✅ DEPLOY COMPLETE! ${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "\nFrontend: ${BLUE}http://$SERVER_HOST:3000${NC}"
echo -e "Backend: ${BLUE}http://$SERVER_HOST:8000${NC}"
echo -e ""
echo -e " Frontend: ${BLUE}https://pounce.ch${NC} / http://$SERVER_HOST:3000"
echo -e " Backend: ${BLUE}https://pounce.ch/api${NC} / http://$SERVER_HOST:8000"
echo -e ""
echo -e "${CYAN}Quick commands:${NC}"
echo -e " ./deploy.sh -q # Quick sync, no git"
echo -e " ./deploy.sh -b # Backend only"
echo -e " ./deploy.sh -f # Frontend only"
echo -e " ./deploy.sh \"message\" # Full deploy with commit message"

View File

@ -30,8 +30,15 @@ import {
BarChart3,
MessageSquare,
Database,
Menu,
Eye,
Gavel,
Tag,
Coins,
LogOut,
} from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
import clsx from 'clsx'
type SettingsTab = 'profile' | 'notifications' | 'billing' | 'security'
@ -111,12 +118,13 @@ const PLANS = [
export default function SettingsPage() {
const router = useRouter()
const { user, isAuthenticated, isLoading, checkAuth, subscription } = useStore()
const { user, isAuthenticated, isLoading, checkAuth, subscription, logout } = useStore()
const [activeTab, setActiveTab] = useState<SettingsTab>('profile')
const [saving, setSaving] = useState(false)
const [success, setSuccess] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const [profileForm, setProfileForm] = useState({
name: '',
@ -303,6 +311,41 @@ export default function SettingsPage() {
{ id: 'security' as const, label: 'Security', icon: Shield },
]
// Mobile Navigation
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/settings', label: 'Settings', icon: Settings, active: true },
]
const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
const drawerNavSections = [
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]
},
{
title: 'Manage',
items: [
{ href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
{ href: '/terminal/sniper', label: 'Sniper', icon: Bell },
]
},
{
title: 'Monetize',
items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins },
{ href: '/terminal/listing', label: 'For Sale', icon: Tag },
]
},
]
return (
<CommandCenterLayout minimal>
{/* ═══════════════════════════════════════════════════════════════════════ */}
@ -820,6 +863,110 @@ export default function SettingsPage() {
</div>
</div>
</section>
{/* Mobile Bottom Padding */}
<div className="h-20 lg:hidden" />
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE BOTTOM NAVIGATION */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<nav className="lg:hidden fixed bottom-0 left-0 right-0 z-40 bg-[#020202]/95 backdrop-blur-md border-t border-white/[0.06]">
<div className="flex items-center justify-around h-14">
{mobileNavItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={clsx(
"flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors",
item.active ? "text-accent" : "text-white/40"
)}
>
<item.icon className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
{item.active && <div className="absolute bottom-0 w-8 h-0.5 bg-accent" />}
</Link>
))}
<button
onClick={() => setMenuOpen(true)}
className="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-white/40"
>
<Menu className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
</button>
</div>
</nav>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* NAVIGATION DRAWER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{menuOpen && (
<>
{/* Backdrop */}
<div
className="lg:hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
onClick={() => setMenuOpen(false)}
/>
{/* Drawer */}
<div className="lg:hidden fixed right-0 top-0 bottom-0 w-72 bg-[#020202] border-l border-white/[0.08] z-50 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-2">
<Image src="/logo.svg" alt="Pounce" width={20} height={20} />
<span className="text-xs font-mono text-white/40">Terminal v1.0</span>
</div>
<button
onClick={() => setMenuOpen(false)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Nav Sections */}
<div className="flex-1 overflow-y-auto py-4">
{drawerNavSections.map((section) => (
<div key={section.title} className="mb-4">
<div className="px-4 py-1">
<span className="text-[9px] font-mono text-white/30 uppercase tracking-widest">{section.title}</span>
</div>
{section.items.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMenuOpen(false)}
className="flex items-center gap-3 px-4 py-2.5 text-white/60 hover:text-white hover:bg-white/[0.03] transition-colors"
>
<item.icon className="w-4 h-4" />
<span className="text-xs font-mono">{item.label}</span>
</Link>
))}
</div>
))}
</div>
{/* User Section */}
<div className="border-t border-white/[0.08] p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
<TierIcon className="w-4 h-4 text-accent" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-mono text-white truncate">{user?.name || user?.email}</p>
<p className="text-[10px] text-accent">{tierName}</p>
</div>
</div>
<button
onClick={() => { setMenuOpen(false); logout(); }}
className="w-full flex items-center justify-center gap-2 py-2 text-xs font-mono text-white/40 border border-white/10 hover:border-red-500/30 hover:text-red-400 transition-all"
>
<LogOut className="w-3.5 h-3.5" />
Sign Out
</button>
</div>
</div>
</>
)}
</CommandCenterLayout>
)
}