#!/bin/bash set -euo pipefail VMID="${VMID:-5000}" PROXMOX_HOST="${PROXMOX_HOST_R630_02:-192.168.11.12}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BACKEND_DIR="$REPO_ROOT/explorer-monorepo/backend" TMP_DIR="$(mktemp -d)" JWT_SECRET_VALUE="${JWT_SECRET_VALUE:-}" EXPLORER_AI_MODEL_VALUE="${EXPLORER_AI_MODEL_VALUE:-grok-3}" EXPLORER_DATABASE_URL_VALUE="${EXPLORER_DATABASE_URL_VALUE:-}" SECURE_AI_ENV_FILE="${SECURE_AI_ENV_FILE:-$HOME/.secure-secrets/explorer-ai.env}" ACCESS_ADMIN_EMAILS_VALUE="${ACCESS_ADMIN_EMAILS:-}" ACCESS_INTERNAL_SECRET_VALUE="${ACCESS_INTERNAL_SECRET:-}" if [ -f "$SECURE_AI_ENV_FILE" ]; then set -a # Source the local secrets file so deploys do not depend on repo-stored API keys. source "$SECURE_AI_ENV_FILE" set +a fi cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT echo "==========================================" echo "Deploying Explorer AI Backend to VMID $VMID" echo "==========================================" echo "=== Step 1: Build explorer backend ===" ( cd "$BACKEND_DIR" go build -o "$TMP_DIR/explorer-config-api" ./api/rest/cmd ) echo "✅ Backend built" echo "=== Step 2: Prepare AI docs bundle ===" mkdir -p "$TMP_DIR/explorer-ai-docs/docs/11-references" "$TMP_DIR/explorer-ai-docs/explorer-monorepo/docs" cp "$REPO_ROOT/docs/11-references/ADDRESS_MATRIX_AND_STATUS.md" "$TMP_DIR/explorer-ai-docs/docs/11-references/" cp "$REPO_ROOT/docs/11-references/LIQUIDITY_POOLS_MASTER_MAP.md" "$TMP_DIR/explorer-ai-docs/docs/11-references/" cp "$REPO_ROOT/docs/11-references/DEPLOYED_TOKENS_BRIDGES_LPS_AND_ROUTING_STATUS.md" "$TMP_DIR/explorer-ai-docs/docs/11-references/" cp "$REPO_ROOT/docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md" "$TMP_DIR/explorer-ai-docs/docs/11-references/" cp "$REPO_ROOT/explorer-monorepo/docs/EXPLORER_API_ACCESS.md" "$TMP_DIR/explorer-ai-docs/explorer-monorepo/docs/" tar -C "$TMP_DIR" -czf "$TMP_DIR/explorer-ai-docs.tar.gz" explorer-ai-docs echo "✅ Docs bundle prepared" echo "=== Step 3: Upload artifacts ===" scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TMP_DIR/explorer-config-api" root@"$PROXMOX_HOST":/tmp/explorer-config-api scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TMP_DIR/explorer-ai-docs.tar.gz" root@"$PROXMOX_HOST":/tmp/explorer-ai-docs.tar.gz echo "✅ Artifacts uploaded" echo "=== Step 4: Install backend, refresh docs, and ensure env ===" if [ -z "$JWT_SECRET_VALUE" ]; then JWT_SECRET_VALUE="$(openssl rand -hex 32)" fi if [ -z "$ACCESS_INTERNAL_SECRET_VALUE" ]; then ACCESS_INTERNAL_SECRET_VALUE="$(openssl rand -hex 32)" fi export JWT_SECRET_VALUE export EXPLORER_AI_MODEL_VALUE export XAI_API_KEY_VALUE="${XAI_API_KEY:-}" export EXPLORER_DATABASE_URL_VALUE export ACCESS_ADMIN_EMAILS_VALUE export ACCESS_INTERNAL_SECRET_VALUE ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@"$PROXMOX_HOST" \ "JWT_SECRET_VALUE='$JWT_SECRET_VALUE' EXPLORER_AI_MODEL_VALUE='$EXPLORER_AI_MODEL_VALUE' XAI_API_KEY_VALUE='$XAI_API_KEY_VALUE' EXPLORER_DATABASE_URL_VALUE='$EXPLORER_DATABASE_URL_VALUE' ACCESS_ADMIN_EMAILS_VALUE='$ACCESS_ADMIN_EMAILS_VALUE' ACCESS_INTERNAL_SECRET_VALUE='$ACCESS_INTERNAL_SECRET_VALUE' bash -s" <<'REMOTE' set -euo pipefail VMID=5000 DB_URL="$EXPLORER_DATABASE_URL_VALUE" if [ -z "$DB_URL" ]; then DB_CONTAINER_IP="$(pct exec "$VMID" -- docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' blockscout-postgres 2>/dev/null || true)" if [ -n "$DB_CONTAINER_IP" ]; then DB_URL="postgresql://blockscout:blockscout@${DB_CONTAINER_IP}:5432/blockscout?sslmode=disable" fi fi pct exec "$VMID" -- bash -lc 'mkdir -p /opt/explorer-ai-docs /etc/systemd/system/explorer-config-api.service.d' pct push "$VMID" /tmp/explorer-ai-docs.tar.gz /tmp/explorer-ai-docs.tar.gz --perms 0644 pct push "$VMID" /tmp/explorer-config-api /usr/local/bin/explorer-config-api.new --perms 0755 pct exec "$VMID" -- env \ DB_URL="$DB_URL" \ EXPLORER_AI_MODEL_VALUE="$EXPLORER_AI_MODEL_VALUE" \ JWT_SECRET_VALUE="$JWT_SECRET_VALUE" \ XAI_API_KEY_VALUE="$XAI_API_KEY_VALUE" \ bash -lc ' set -euo pipefail rm -rf /opt/explorer-ai-docs/* tar -xzf /tmp/explorer-ai-docs.tar.gz -C /opt rm -f /tmp/explorer-ai-docs.tar.gz mv /usr/local/bin/explorer-config-api.new /usr/local/bin/explorer-config-api chmod 0755 /usr/local/bin/explorer-config-api cat > /etc/systemd/system/explorer-config-api.service < /etc/systemd/system/explorer-config-api.service.d/ai.conf < /etc/systemd/system/explorer-config-api.service.d/security.conf < /etc/systemd/system/explorer-config-api.service.d/access.conf < /etc/systemd/system/explorer-config-api.service.d/database.conf < /etc/systemd/system/explorer-config-api.service.d/xai.conf < str: first = text.find(marker) if first == -1: return text second = text.find(marker, first + len(marker)) if second == -1: return text next_positions = [text.find(candidate, second) for candidate in next_markers] next_positions = [pos for pos in next_positions if pos != -1] if not next_positions: return text return text[:first] + text[second:min(next_positions)] + text[min(next_positions):] text = dedupe_named_location_block( text, ' # Explorer backend API (auth, features, AI, explorer-owned v1 helpers)\n', [ ' # Blockscout API endpoint - MUST come before the redirect location\n', ' # API endpoint - MUST come before the redirect location\n', ' # Token-aggregation API for the explorer SPA live route-tree and pool intelligence.\n', ' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n', ], ) text = dedupe_named_location_block( text, ' # Explorer stats override: keep freshness/completeness metadata on the explorer-owned backend.\n', [ ' # Explorer backend API (auth, features, AI, explorer-owned v1 helpers)\n', ' # Blockscout API endpoint - MUST come before the redirect location\n', ' # API endpoint - MUST come before the redirect location\n', ' # Token-aggregation API for the explorer SPA live route-tree and pool intelligence.\n', ' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n', ], ) text = dedupe_named_location_block( text, ' # Enriched explorer stats come from the Go-side API on 8081.\n', [ ' # Explorer stats override: keep freshness/completeness metadata on the explorer-owned backend.\n', ' # Explorer backend API (auth, features, AI, explorer-owned v1 helpers)\n', ' # Blockscout API endpoint - MUST come before the redirect location\n', ' # API endpoint - MUST come before the redirect location\n', ' # Token-aggregation API for the explorer SPA live route-tree and pool intelligence.\n', ' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n', ], ) legacy_patterns = [ r"\n\s*# Explorer AI endpoints on the explorer backend service \(HTTP\)\n\s*location /api/v1/ai/ \{.*?\n\s*\}\n", r"\n\s*location = /api/v1/features \{.*?\n\s*\}\n", r"\n\s*# Explorer AI endpoints on the explorer backend service\n\s*location /api/v1/ai/ \{.*?\n\s*\}\n", ] for pattern in legacy_patterns: text = re.sub(pattern, "\n", text, flags=re.S) http_needle = ' # Blockscout API endpoint - MUST come before the redirect location\n' legacy_http_needle = ' # API endpoint - MUST come before the redirect location\n' if stats_block not in text: if http_needle in text: text = text.replace(http_needle, stats_block + http_needle, 1) elif legacy_http_needle in text: text = text.replace(legacy_http_needle, stats_block + ' # Blockscout API endpoint - MUST come before the redirect location\n', 1) if explorer_block not in text: if http_needle in text: text = text.replace(http_needle, explorer_block + http_needle, 1) elif legacy_http_needle in text: text = text.replace(legacy_http_needle, explorer_block + ' # Blockscout API endpoint - MUST come before the redirect location\n', 1) https_needle = ' # Token-aggregation API for the explorer SPA live route-tree and pool intelligence.\n' if stats_block not in text[text.find('# HTTPS server - Blockscout Explorer'):]: text = text.replace(' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n location /api/v1/ {\n proxy_pass http://127.0.0.1:3001/api/v1/;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 60s;\n add_header Access-Control-Allow-Origin *;\n }\n\n', stats_block + ' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n location /api/v1/ {\n proxy_pass http://127.0.0.1:3001/api/v1/;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 60s;\n add_header Access-Control-Allow-Origin *;\n }\n\n', 1) if explorer_block not in text[text.find('# HTTPS server - Blockscout Explorer'):]: text = text.replace(' # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001.\n location /api/v1/ {\n proxy_pass http://127.0.0.1:3001/api/v1/;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 60s;\n add_header Access-Control-Allow-Origin *;\n }\n\n', explorer_block, 1) path.write_text(text) PY pct exec "$VMID" -- bash -lc 'nginx -t && nginx -s reload' REMOTE echo "✅ Nginx normalized" echo "=== Step 6: Verify core explorer AI routes ===" curl -fsS "https://explorer.d-bis.org/explorer-api/v1/features" >/dev/null curl -fsS "https://explorer.d-bis.org/explorer-api/v1/ai/context?q=cUSDT" >/dev/null echo "✅ Explorer AI routes respond publicly" echo "" echo "Deployment complete."