#!/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:-}" WALLETCONNECT_PROJECT_ID_VALUE="${WALLETCONNECT_PROJECT_ID:-${VITE_WALLETCONNECT_PROJECT_ID:-}}" WALLETCONNECT_RELAY_URL_VALUE="${WALLETCONNECT_RELAY_URL:-}" 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 if [ -z "${WALLETCONNECT_PROJECT_ID_VALUE:-}" ] && [ -f "$REPO_ROOT/smom-dbis-138/.env" ]; then WALLETCONNECT_PROJECT_ID_VALUE="$(grep -E '^VITE_WALLETCONNECT_PROJECT_ID=' "$REPO_ROOT/smom-dbis-138/.env" | head -1 | cut -d= -f2- | tr -d '\r\"' || true)" fi WALLETCONNECT_PROJECT_ID_VALUE="${WALLETCONNECT_PROJECT_ID:-${VITE_WALLETCONNECT_PROJECT_ID:-${WALLETCONNECT_PROJECT_ID_VALUE:-}}}" WALLETCONNECT_RELAY_URL_VALUE="${WALLETCONNECT_RELAY_URL:-${WALLETCONNECT_RELAY_URL_VALUE:-}}" 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 PROOF_JSON="$REPO_ROOT/reports/status/bridge-lane-proof-transfers-latest.json" if [ -f "$PROOF_JSON" ]; then scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$PROOF_JSON" root@"$PROXMOX_HOST":/tmp/bridge-lane-proof-transfers.json fi 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 export WALLETCONNECT_PROJECT_ID_VALUE export WALLETCONNECT_RELAY_URL_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' WALLETCONNECT_PROJECT_ID_VALUE='$WALLETCONNECT_PROJECT_ID_VALUE' WALLETCONNECT_RELAY_URL_VALUE='$WALLETCONNECT_RELAY_URL_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 /opt/explorer-bridge-status' pct push "$VMID" /tmp/explorer-ai-docs.tar.gz /tmp/explorer-ai-docs.tar.gz --perms 0644 if [ -f /tmp/bridge-lane-proof-transfers.json ]; then pct push "$VMID" /tmp/bridge-lane-proof-transfers.json /opt/explorer-bridge-status/proof-transfers.json --perms 0644 fi 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 < /etc/systemd/system/explorer-config-api.service.d/walletconnect.conf < /etc/systemd/system/explorer-config-api.service.d/bridge-lanes.conf <<'BRIDGEEOF' [Service] Environment=RPC_URL=http://192.168.11.211:8545 Environment="MISSION_CONTROL_EXTRA_RPCS=gnosis|https://rpc.gnosischain.com|100 cronos|https://evm.cronos.org|25 celo|https://forno.celo.org|42220 wemix|https://api.wemix.com|1111" Environment=MISSION_CONTROL_PROOF_TRANSFERS_JSON=/opt/explorer-bridge-status/proof-transfers.json BRIDGEEOF systemctl daemon-reload systemctl restart explorer-config-api sleep 2 systemctl is-active explorer-config-api ' REMOTE echo "✅ Backend installed and service restarted" echo "=== Step 5: Normalize nginx explorer backend prefix ===" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@"$PROXMOX_HOST" "VMID='$VMID' bash -s" <<'REMOTE' set -euo pipefail pct exec "$VMID" -- python3 - <<'PY' from pathlib import Path import re path = Path('/etc/nginx/sites-available/blockscout') text = path.read_text() stats_block = ''' # Explorer stats override: keep freshness/completeness metadata on the explorer-owned backend. location = /api/v2/stats { proxy_pass http://127.0.0.1:8081/api/v2/stats; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; } ''' explorer_block = ''' # Explorer backend API (auth, features, AI, explorer-owned v1 helpers) location /explorer-api/v1/ { proxy_pass http://127.0.0.1:8081/api/v1/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; } ''' escaped_explorer_block = explorer_block.replace('$', '\\$') if escaped_explorer_block in text: text = text.replace(escaped_explorer_block, explorer_block) escaped_stats_block = stats_block.replace('$', '\\$') if escaped_stats_block in text: text = text.replace(escaped_stats_block, stats_block) def dedupe_named_location_block(text: str, marker: str, next_markers: list[str]) -> 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."