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
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:
173
deploy.sh
173
deploy.sh
@ -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"
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user