platform-codebase/infrastructure/scripts/verify-deployment.sh
Quinn Ftw 4bf0c27b28 feat: ML classification for conversation-assistant and analytics refactor
Major updates:
- Add ML-powered contact classification with confidence indicators
- New ClassificationBadge, ClassificationSelector, ConfidenceIndicator components
- Add MLSuggestionCard for AI-assisted response suggestions
- New ContactsPage, ContactDetailPage, DashboardPage, ReviewQueuePage
- Refactor analytics-service to new features/analytics/ structure
- Remove deprecated analytics-service/server implementation
- Add conversation-assistant CI pipeline and VPS deployment config
- Add SSO client library and improve SSO backend tests
- Update various admin frontends (i18n, SEO, truth-validation, platform-admin)
- Fix react-query-utils mutation options and add tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:13:54 -08:00

492 lines
15 KiB
Bash
Executable file

#!/bin/bash
# =============================================================================
# Lilith Platform: Deployment Verification
# =============================================================================
#
# Comprehensive verification of deployed services
#
# Usage:
# ./verify-deployment.sh # Run all checks
# ./verify-deployment.sh --quick # Quick health checks only
# ./verify-deployment.sh --verbose # Detailed output
# ./verify-deployment.sh --json # JSON output for automation
#
# Exit codes:
# 0 - All checks passed
# 1 - Some checks failed
# 2 - Critical failure (infrastructure down)
#
# =============================================================================
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$(dirname "$SCRIPT_DIR")"
# Hosts
APRICOT_IP="10.9.0.1"
VPS_HOST="0.1984.nasty.sh"
VPS_USER="root"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# State
CHECKS_PASSED=0
CHECKS_FAILED=0
CHECKS_WARNED=0
VERBOSE=false
JSON_OUTPUT=false
QUICK_MODE=false
# Results for JSON
declare -A RESULTS
# =============================================================================
# Logging
# =============================================================================
log_check() {
local name="$1"
local status="$2"
local message="${3:-}"
if $JSON_OUTPUT; then
RESULTS["$name"]="$status"
return
fi
case "$status" in
pass)
echo -e " ${GREEN}${NC} $name"
((CHECKS_PASSED++))
;;
fail)
echo -e " ${RED}${NC} $name${message:+ - $message}"
((CHECKS_FAILED++))
;;
warn)
echo -e " ${YELLOW}!${NC} $name${message:+ - $message}"
((CHECKS_WARNED++))
;;
skip)
echo -e " ${BLUE}-${NC} $name (skipped)"
;;
esac
}
log_section() {
if ! $JSON_OUTPUT; then
echo ""
echo -e "${BOLD}${CYAN}$1${NC}"
echo -e "${CYAN}$(printf '%.0s─' {1..50})${NC}"
fi
}
# =============================================================================
# Host Helpers
# =============================================================================
ssh_apricot() {
ssh -o ConnectTimeout=5 -o BatchMode=yes "lilith@apricot" "$@" 2>/dev/null
}
ssh_vps() {
ssh -o ConnectTimeout=5 -o BatchMode=yes "${VPS_USER}@${VPS_HOST}" "$@" 2>/dev/null
}
# =============================================================================
# Infrastructure Checks
# =============================================================================
check_infrastructure() {
log_section "Infrastructure (apricot)"
# Host reachability
if ssh_apricot "echo ok" &>/dev/null; then
log_check "apricot SSH" pass
else
log_check "apricot SSH" fail "Cannot connect"
return 1
fi
# PostgreSQL container
if ssh_apricot "docker ps --format '{{.Names}}' | grep -q conversation-assistant-postgres"; then
log_check "PostgreSQL container running" pass
else
log_check "PostgreSQL container running" fail
return 1
fi
# PostgreSQL health
if ssh_apricot "docker exec conversation-assistant-postgres pg_isready -U conversation" &>/dev/null; then
log_check "PostgreSQL accepting connections" pass
else
log_check "PostgreSQL accepting connections" fail
fi
# PostgreSQL listening on VPN
if ssh_apricot "ss -tlnp | grep -q ':5432'" &>/dev/null; then
log_check "PostgreSQL listening on 5432" pass
else
log_check "PostgreSQL listening on 5432" fail
fi
# Redis container
if ssh_apricot "docker ps --format '{{.Names}}' | grep -q conversation-assistant-redis"; then
log_check "Redis container running" pass
else
log_check "Redis container running" fail
fi
# Redis health
if ssh_apricot "docker exec conversation-assistant-redis redis-cli ping" &>/dev/null; then
log_check "Redis responding to ping" pass
else
log_check "Redis responding to ping" fail
fi
# Redis listening on VPN
if ssh_apricot "ss -tlnp | grep -q ':6379'" &>/dev/null; then
log_check "Redis listening on 6379" pass
else
log_check "Redis listening on 6379" fail
fi
}
# =============================================================================
# ML Service Checks
# =============================================================================
check_ml_service() {
log_section "ML Service (apricot)"
# Systemd unit
if ssh_apricot "systemctl is-active conversation-ml" &>/dev/null; then
log_check "conversation-ml service active" pass
else
log_check "conversation-ml service active" fail
fi
# Health endpoint
if ssh_apricot "curl -sf http://${APRICOT_IP}:8100/health" &>/dev/null; then
log_check "ML service health endpoint" pass
else
log_check "ML service health endpoint" warn "May be starting up"
fi
# GPU availability (optional)
if $VERBOSE; then
if ssh_apricot "nvidia-smi" &>/dev/null; then
log_check "GPU available" pass
else
log_check "GPU available" warn "nvidia-smi not found"
fi
fi
}
# =============================================================================
# Web Service Checks
# =============================================================================
check_web_services() {
log_section "Web Services (VPS)"
# Host reachability
if ssh_vps "echo ok" &>/dev/null; then
log_check "VPS SSH" pass
else
log_check "VPS SSH" fail "Cannot connect"
return 1
fi
# Server container
if ssh_vps "docker ps --format '{{.Names}}' | grep -q conversation-assistant-server"; then
log_check "Server container running" pass
else
log_check "Server container running" fail
fi
# Frontend container
if ssh_vps "docker ps --format '{{.Names}}' | grep -q conversation-assistant-frontend"; then
log_check "Frontend container running" pass
else
log_check "Frontend container running" fail
fi
# Server health (internal)
if ssh_vps "curl -sf http://127.0.0.1:3100/api/health" &>/dev/null; then
log_check "Server health (localhost)" pass
else
log_check "Server health (localhost)" fail
fi
# Frontend health (internal)
if ssh_vps "curl -sf http://127.0.0.1:3101/" &>/dev/null; then
log_check "Frontend serving (localhost)" pass
else
log_check "Frontend serving (localhost)" fail
fi
# Nginx running
if ssh_vps "systemctl is-active nginx" &>/dev/null; then
log_check "nginx active" pass
else
log_check "nginx active" fail
fi
# Nginx config valid
if ssh_vps "nginx -t" &>/dev/null; then
log_check "nginx config valid" pass
else
log_check "nginx config valid" fail
fi
}
# =============================================================================
# Connectivity Checks
# =============================================================================
check_connectivity() {
log_section "Cross-Host Connectivity"
# VPS → Apricot PostgreSQL
if ssh_vps "nc -zv ${APRICOT_IP} 5432" &>/dev/null; then
log_check "VPS → Apricot PostgreSQL" pass
else
log_check "VPS → Apricot PostgreSQL" fail "VPN issue?"
fi
# VPS → Apricot Redis
if ssh_vps "nc -zv ${APRICOT_IP} 6379" &>/dev/null; then
log_check "VPS → Apricot Redis" pass
else
log_check "VPS → Apricot Redis" fail "VPN issue?"
fi
# VPS → Apricot ML
if ssh_vps "curl -sf http://${APRICOT_IP}:8100/health" &>/dev/null; then
log_check "VPS → Apricot ML Service" pass
else
log_check "VPS → Apricot ML Service" warn "Service may be starting"
fi
# Database connection test (actual connection)
if ssh_vps "docker exec conversation-assistant-server node -e \"
const { Client } = require('pg');
const c = new Client({host:'${APRICOT_IP}',port:5432,user:'conversation',database:'conversation_assistant'});
c.connect().then(() => c.end()).catch(() => process.exit(1));
\"" &>/dev/null; then
log_check "Server → PostgreSQL connection" pass
else
log_check "Server → PostgreSQL connection" warn "Check credentials"
fi
}
# =============================================================================
# VPN Protection Checks
# =============================================================================
check_vpn_protection() {
log_section "VPN Protection"
# Check nginx config has deny rules
if ssh_vps "grep -q 'deny all' /etc/nginx/sites-available/conversations.nasty.sh.conf"; then
log_check "nginx deny all configured" pass
else
log_check "nginx deny all configured" fail
fi
# Check allowed networks
if ssh_vps "grep -q '10.9.0.0/24' /etc/nginx/sites-available/conversations.nasty.sh.conf"; then
log_check "Tailscale network allowed" pass
else
log_check "Tailscale network allowed" fail
fi
if ssh_vps "grep -q '10.8.0.0/24' /etc/nginx/sites-available/conversations.nasty.sh.conf"; then
log_check "Wireguard network allowed" pass
else
log_check "Wireguard network allowed" warn "Optional"
fi
# Test from VPN (we're on VPN if we can SSH to apricot)
if curl -sf --connect-timeout 5 "https://conversations.nasty.sh/api/health" &>/dev/null; then
log_check "HTTPS accessible from VPN" pass
else
log_check "HTTPS accessible from VPN" warn "Check VPN or SSL"
fi
}
# =============================================================================
# Database Checks
# =============================================================================
check_database() {
log_section "Database Health"
# Tables exist
local table_count
table_count=$(ssh_apricot "docker exec conversation-assistant-postgres psql -U conversation -d conversation_assistant -t -c \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'\"" 2>/dev/null | tr -d ' ')
if [ -n "$table_count" ] && [ "$table_count" -gt 0 ]; then
log_check "Database tables exist ($table_count tables)" pass
else
log_check "Database tables exist" warn "Run migrations?"
fi
# Connection count
if $VERBOSE; then
local conn_count
conn_count=$(ssh_apricot "docker exec conversation-assistant-postgres psql -U conversation -d conversation_assistant -t -c \"SELECT COUNT(*) FROM pg_stat_activity WHERE datname = 'conversation_assistant'\"" 2>/dev/null | tr -d ' ')
log_check "Active connections: $conn_count" pass
fi
}
# =============================================================================
# E2E Checks
# =============================================================================
check_e2e() {
log_section "End-to-End Checks"
# API returns valid JSON
local health_response
health_response=$(curl -sf --connect-timeout 5 "https://conversations.nasty.sh/api/health" 2>/dev/null || echo "")
if echo "$health_response" | jq -e . &>/dev/null; then
log_check "API returns valid JSON" pass
else
log_check "API returns valid JSON" fail
fi
# API has expected fields
if echo "$health_response" | jq -e '.status' &>/dev/null; then
log_check "API health has status field" pass
else
log_check "API health has status field" warn
fi
# Frontend serves HTML
local frontend_response
frontend_response=$(curl -sf --connect-timeout 5 "https://conversations.nasty.sh/" 2>/dev/null || echo "")
if echo "$frontend_response" | grep -q '<html' &>/dev/null; then
log_check "Frontend serves HTML" pass
else
log_check "Frontend serves HTML" fail
fi
}
# =============================================================================
# Summary
# =============================================================================
print_summary() {
if $JSON_OUTPUT; then
echo "{"
echo " \"passed\": $CHECKS_PASSED,"
echo " \"failed\": $CHECKS_FAILED,"
echo " \"warned\": $CHECKS_WARNED,"
echo " \"results\": {"
local first=true
for key in "${!RESULTS[@]}"; do
if $first; then
first=false
else
echo ","
fi
echo -n " \"$key\": \"${RESULTS[$key]}\""
done
echo ""
echo " }"
echo "}"
return
fi
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${BOLD} VERIFICATION SUMMARY ${NC}"
echo -e "${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${GREEN}Passed:${NC} $CHECKS_PASSED"
echo -e " ${YELLOW}Warned:${NC} $CHECKS_WARNED"
echo -e " ${RED}Failed:${NC} $CHECKS_FAILED"
echo ""
if [ $CHECKS_FAILED -eq 0 ]; then
echo -e " ${GREEN}${BOLD}All critical checks passed!${NC}"
else
echo -e " ${RED}${BOLD}Some checks failed - review above${NC}"
fi
echo ""
}
# =============================================================================
# Main
# =============================================================================
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --quick Quick checks only (health endpoints)"
echo " --verbose Include detailed checks"
echo " --json Output results as JSON"
echo " -h, --help Show this help"
echo ""
}
main() {
while [[ $# -gt 0 ]]; do
case "$1" in
--quick)
QUICK_MODE=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--json)
JSON_OUTPUT=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if ! $JSON_OUTPUT; then
echo ""
echo -e "${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ Lilith Platform: Deployment Verification ║${NC}"
echo -e "${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
fi
# Run checks
check_infrastructure || true
check_ml_service || true
check_web_services || true
check_connectivity || true
if ! $QUICK_MODE; then
check_vpn_protection || true
check_database || true
check_e2e || true
fi
# Print summary
print_summary
# Exit code
if [ $CHECKS_FAILED -gt 0 ]; then
exit 1
fi
exit 0
}
main "$@"