#!/usr/bin/env bash # Security: do not use `bash -x` or `curl -v` when debugging auth in production — logs may capture secrets. # Auth failures: only a short error message is printed by default. For a redacted JSON snippet set NPM_DEBUG_AUTH=1. set -euo pipefail # Load IP configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true # Update existing NPMplus proxy hosts via API with correct VMIDs and IPs # This script updates existing proxy hosts, not creates new ones. # PUT payload includes only forward_* / websocket / block_exploits — existing certificate_id and ssl_forced are preserved by NPMplus. set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # Preserve NPM credentials from environment so "export NPM_PASSWORD=...; ./script" works _orig_npm_url="${NPM_URL:-}" _orig_npm_email="${NPM_EMAIL:-}" _orig_npm_password="${NPM_PASSWORD:-}" # Load dotenv: repo root .env then smom-dbis-138/.env (operator creds) if [ -f "$PROJECT_ROOT/.env" ]; then set +u # shellcheck source=/dev/null source "$PROJECT_ROOT/.env" set -u fi if [ -f "$PROJECT_ROOT/smom-dbis-138/.env" ]; then set +u # shellcheck source=/dev/null source "$PROJECT_ROOT/smom-dbis-138/.env" set -u fi [ -n "$_orig_npm_url" ] && NPM_URL="$_orig_npm_url" [ -n "$_orig_npm_email" ] && NPM_EMAIL="$_orig_npm_email" [ -n "$_orig_npm_password" ] && NPM_PASSWORD="$_orig_npm_password" [ -f "$PROJECT_ROOT/config/ip-addresses.conf" ] && source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true # Default: NPMplus (VMID 10233) — IP_NPMPLUS from config/ip-addresses.conf (prefer eth1 / .167); override with NPM_URL in .env NPM_URL="${NPM_URL:-https://${IP_NPMPLUS}:81}" NPM_EMAIL="${NPM_EMAIL:-nsatoshi2007@hotmail.com}" NPM_PASSWORD="${NPM_PASSWORD:-}" if [ -z "$NPM_PASSWORD" ]; then echo "❌ NPM_PASSWORD is required. Set it in .env or export NPM_PASSWORD=..." echo " Example: echo 'NPM_PASSWORD=your-password' >> $PROJECT_ROOT/.env" exit 1 fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "🔄 Updating NPMplus Proxy Hosts via API" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # NPMplus API can stall indefinitely without --max-time (override e.g. NPM_CURL_MAX_TIME=300) NPM_CURL_MAX_TIME="${NPM_CURL_MAX_TIME:-120}" curl_npm() { curl -s -k --connect-timeout 10 --max-time "$NPM_CURL_MAX_TIME" "$@"; } # Connection check (NPMplus is on LAN 192.168.11.x). Try HTTP if HTTPS fails; try alternate IP .166/.167 if unreachable. echo "🔐 Authenticating to NPMplus..." try_connect() { curl -s -k -o /dev/null --connect-timeout 5 --max-time 15 "$1" 2>/dev/null; } if ! try_connect "$NPM_URL/"; then # Try HTTP instead of HTTPS (NPM admin often listens on HTTP only on port 81) http_url="${NPM_URL/https:/http:}" if try_connect "$http_url/"; then NPM_URL="$http_url" echo " Using HTTP (HTTPS timed out): $NPM_URL" else alt_url="" if [[ "$NPM_URL" == *"${IP_NPMPLUS_ETH0}"* ]]; then alt_url="https://${IP_NPMPLUS}:81" elif [[ "$NPM_URL" == *"${IP_NPMPLUS}"* ]] || [[ "$NPM_URL" == *"${IP_NPMPLUS_ETH1}"* ]]; then alt_url="https://${IP_NPMPLUS_ETH0}:81" fi connected="" if [ -n "$alt_url" ] && try_connect "$alt_url/"; then connected="$alt_url"; fi if [ -z "$connected" ] && [ -n "$alt_url" ] && try_connect "${alt_url/https:/http:}/"; then connected="${alt_url/https:/http:}"; fi if [ -n "$connected" ]; then NPM_URL="$connected" echo " Using alternate NPMplus URL: $NPM_URL" else echo "❌ Cannot connect to NPMplus at $NPM_URL" [ -n "$alt_url" ] && echo " Tried alternate: $alt_url and ${alt_url/https:/http:}" echo " Try in browser: http://${IP_NPMPLUS}:81 (not https). If not on LAN, use SSH tunnel: ssh -L 8181:${IP_NPMPLUS}:81 -N root@${PROXMOX_HOST_R630_01} then http://127.0.0.1:8181" echo " Run this script from a host on the same LAN as NPMplus. Ensure container 10233 is running." exit 1 fi fi fi AUTH_JSON=$(jq -n --arg identity "$NPM_EMAIL" --arg secret "$NPM_PASSWORD" '{identity:$identity,secret:$secret}') TOKEN_RESPONSE=$(curl_npm -X POST "$NPM_URL/api/tokens" \ -H "Content-Type: application/json" \ -d "$AUTH_JSON") TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token // empty' 2>/dev/null || echo "") if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then ERROR_MSG=$(echo "$TOKEN_RESPONSE" | jq -r '.message // .error.message // .error // "Unknown error"' 2>/dev/null || echo "") echo "❌ Authentication failed: ${ERROR_MSG:-No token in response}" if [ "${NPM_DEBUG_AUTH:-0}" = "1" ]; then # Strip .token from body before preview (never print bearer tokens) RESP_PREVIEW=$(echo "$TOKEN_RESPONSE" | jq -c 'del(.token) | del(.data)' 2>/dev/null | head -c 300) [ -z "$RESP_PREVIEW" ] && RESP_PREVIEW=$(echo "$TOKEN_RESPONSE" | head -c 200) echo " Debug (NPM_DEBUG_AUTH=1, token field stripped if jq OK): $RESP_PREVIEW" fi exit 1 fi echo "✅ Authentication successful" echo "" # Get all proxy hosts echo "📋 Fetching existing proxy hosts..." PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \ -H "Authorization: Bearer $TOKEN") if [ $? -ne 0 ]; then echo "❌ Failed to fetch proxy hosts" exit 1 fi # Match proxy host id by domain (case-insensitive). Avoids "not found" when NPM stores different casing. resolve_proxy_host_id() { local domain="$1" local json="${2:-$PROXY_HOSTS_JSON}" echo "$json" | jq -r --arg d "$domain" ' .[] | select(.domain_names | type == "array") | select(any(.domain_names[]; (. | tostring | ascii_downcase) == ($d | ascii_downcase))) | .id' 2>/dev/null | head -n1 } # Function to add proxy host (POST) when domain does not exist add_proxy_host() { local domain=$1 local forward_host=$2 local forward_port=$3 local websocket=$4 local block_exploits=${5:-false} local payload payload=$(jq -n \ --arg domain "$domain" \ --arg host "$forward_host" \ --argjson port "$forward_port" \ --argjson ws "$websocket" \ --argjson block_exploits "$([ "$block_exploits" = "true" ] && echo true || echo false)" \ '{ domain_names: [$domain], forward_scheme: "http", forward_host: $host, forward_port: $port, allow_websocket_upgrade: $ws, block_exploits: $block_exploits, certificate_id: null, ssl_forced: false }' 2>/dev/null) if [ -z "$payload" ]; then echo " ❌ Failed to build payload for $domain" return 1 fi local resp resp=$(curl_npm -X POST "$NPM_URL/api/nginx/proxy-hosts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$payload" 2>/dev/null) local id id=$(echo "$resp" | jq -r '.id // empty' 2>/dev/null) if [ -n "$id" ] && [ "$id" != "null" ]; then echo " ✅ Added: $domain -> http://${forward_host}:${forward_port} (WebSocket: $websocket)" return 0 else local err err=$(echo "$resp" | jq -r '.message // .error // "unknown"' 2>/dev/null) echo " ❌ Add failed for $domain: $err" if echo "$err" | grep -qiE 'already|in use|exist|duplicate|unique'; then echo " ↪ Host likely exists; refreshing list and attempting PUT update..." PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \ -H "Authorization: Bearer $TOKEN") if update_proxy_host "$domain" "http://${forward_host}:${forward_port}" "$websocket" "$block_exploits"; then echo " ✅ Updated after duplicate-create error: $domain" return 0 fi fi return 1 fi } # Function to update proxy host # block_exploits: set false for RPC hosts (JSON-RPC uses POST to /; block_exploits can cause 405) update_proxy_host() { local domain=$1 local target=$2 local websocket=$3 local block_exploits=${4:-true} # Parse target URL local scheme=$(echo "$target" | sed -E 's|^([^:]+):.*|\1|') local hostname=$(echo "$target" | sed -E 's|^[^/]+//([^:]+):.*|\1|') local port=$(echo "$target" | sed -E 's|^[^:]+://[^:]+:([0-9]+).*|\1|') # Handle https URLs if [[ "$target" == https://* ]]; then scheme="https" hostname=$(echo "$target" | sed -E 's|^https://([^:]+):.*|\1|') port=$(echo "$target" | sed -E 's|^https://[^:]+:([0-9]+).*|\1|' || echo "443") fi # Get host ID (case-insensitive); refresh once if missing (stale list / race with other writers) HOST_ID=$(resolve_proxy_host_id "$domain" "$PROXY_HOSTS_JSON") if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then echo "📋 Refreshing proxy host list (retry lookup for $domain)..." PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \ -H "Authorization: Bearer $TOKEN") HOST_ID=$(resolve_proxy_host_id "$domain" "$PROXY_HOSTS_JSON") fi if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then echo "⚠️ Domain $domain not found (skipping)" return 1 fi echo "📋 Updating $domain (ID: $HOST_ID)..." # Create minimal update payload - NPMplus API only accepts specific fields # block_exploits must be false for RPC so POST to / is allowed (JSON-RPC); explicit false fixes 405 local be_json="false" [ "$block_exploits" = "true" ] && be_json="true" UPDATE_PAYLOAD=$(jq -n \ --arg scheme "$scheme" \ --arg hostname "$hostname" \ --argjson port "$(echo "$port" | sed 's/[^0-9]//g' || echo "80")" \ --argjson websocket "$websocket" \ --argjson block_exploits "$be_json" \ '{ forward_scheme: $scheme, forward_host: $hostname, forward_port: $port, allow_websocket_upgrade: $websocket, block_exploits: $block_exploits }' 2>/dev/null || echo "") UPDATE_RESPONSE=$(curl_npm -X PUT "$NPM_URL/api/nginx/proxy-hosts/$HOST_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$UPDATE_PAYLOAD") UPDATE_ID=$(echo "$UPDATE_RESPONSE" | jq -r '.id // empty' 2>/dev/null || echo "") if [ -n "$UPDATE_ID" ] && [ "$UPDATE_ID" != "null" ]; then echo " ✅ Updated: $scheme://$hostname:$port (WebSocket: $websocket)" return 0 else ERROR=$(echo "$UPDATE_RESPONSE" | jq -r '.error.message // .error // "Unknown error"' 2>/dev/null || echo "$UPDATE_RESPONSE") echo " ❌ Failed: $ERROR" return 1 fi } # Update all domains updated_count=0 failed_count=0 # Blockscout / SolaceScanScout (VMID 5000) - Port 80 (nginx serves web UI, proxies /api/* to 4000 internally) update_proxy_host "explorer.d-bis.org" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1)) update_proxy_host "blockscout.defi-oracle.io" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "blockscout.defi-oracle.io" "${IP_BLOCKSCOUT}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # docs.d-bis.org — same backend as explorer; nginx on VMID 5000 must serve /transaction-explanation/ (see deploy README) update_proxy_host "docs.d-bis.org" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "docs.d-bis.org" "${IP_BLOCKSCOUT}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # RPC hosts: block_exploits must be false so POST to / works (JSON-RPC). Add if missing to avoid 405. update_proxy_host "rpc-http-pub.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-http-pub.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "rpc-ws-pub.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-ws-pub.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "rpc-http-prv.d-bis.org" "http://${RPC_CORE_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-http-prv.d-bis.org" "${RPC_CORE_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "rpc-ws-prv.d-bis.org" "http://${RPC_CORE_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-ws-prv.d-bis.org" "${RPC_CORE_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # ThirdWeb Admin Core RPC — VMID ${RPC_THIRDWEB_ADMIN_CORE_VMID:-2103} @ ${RPC_THIRDWEB_ADMIN_CORE} (HTTPS + WSS via NPMplus; block_exploits off for JSON-RPC POST) RPC_THIRDWEB_ADMIN_CORE="${RPC_THIRDWEB_ADMIN_CORE:-192.168.11.217}" update_proxy_host "rpc.tw-core.d-bis.org" "http://${RPC_THIRDWEB_ADMIN_CORE}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.tw-core.d-bis.org" "${RPC_THIRDWEB_ADMIN_CORE}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "wss.tw-core.d-bis.org" "http://${RPC_THIRDWEB_ADMIN_CORE}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "wss.tw-core.d-bis.org" "${RPC_THIRDWEB_ADMIN_CORE}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # Catch-all for foo.tw-core.d-bis.org → Besu HTTP JSON-RPC :8545 (exact rpc./wss. hosts above take precedence for nginx server_name) update_proxy_host '*.tw-core.d-bis.org' "http://${RPC_THIRDWEB_ADMIN_CORE}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host '*.tw-core.d-bis.org' "${RPC_THIRDWEB_ADMIN_CORE}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # RPC Core-2 (Nathan) is on the THIRD NPMplus (192.168.11.169) — use add-rpc-core-2-npmplus-proxy.sh and update-npmplus-alltra-hybx-proxy-hosts.sh update_proxy_host "rpc.public-0138.defi-oracle.io" "https://${RPC_THIRDWEB_PRIMARY}:443" true false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1)) # rpc.defi-oracle.io / wss.defi-oracle.io → same backend as rpc-http-pub / rpc-ws-pub (VMID 2201) update_proxy_host "rpc.defi-oracle.io" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.defi-oracle.io" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "wss.defi-oracle.io" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "wss.defi-oracle.io" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # rpc.d-bis.org / rpc2.d-bis.org and WS variants → VMID 2201 (besu-rpc-public-1); add if missing to fix 405 update_proxy_host "rpc.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "rpc2.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc2.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "ws.rpc.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "ws.rpc2.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc2.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # Fireblocks-dedicated RPC (VMID 2301) update_proxy_host "rpc-fireblocks.d-bis.org" "http://${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-fireblocks.d-bis.org" "${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "ws.rpc-fireblocks.d-bis.org" "http://${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc-fireblocks.d-bis.org" "${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "dbis-admin.d-bis.org" "http://${IP_DBIS_FRONTEND:-192.168.11.130}:80" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1)) update_proxy_host "dbis-api.d-bis.org" "http://${IP_DBIS_API:-192.168.11.155}:3000" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1)) update_proxy_host "dbis-api-2.d-bis.org" "http://${IP_DBIS_API_2:-192.168.11.156}:3000" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dbis-api-2.d-bis.org" "${IP_DBIS_API_2:-192.168.11.156}" 3000 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "secure.d-bis.org" "http://${IP_DBIS_FRONTEND:-192.168.11.130}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "secure.d-bis.org" "${IP_DBIS_FRONTEND:-192.168.11.130}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # DApp (VMID 5801) — frontend-dapp for Chain 138 bridge update_proxy_host "dapp.d-bis.org" "http://${IP_DAPP_LXC:-192.168.11.58}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dapp.d-bis.org" "${IP_DAPP_LXC:-192.168.11.58}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # MIM4U - VMID 7810 (mim-web-1) @ ${IP_MIM_WEB:-192.168.11.37} - Web Frontend serves main site and proxies /api/* to 7811 update_proxy_host "mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "www.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "www.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "secure.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "secure.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "training.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "training.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) # Gov Portals xom-dev (VMID 7804) — public https:// at NPM (LE); upstream HTTP IP_GOV_PORTALS_DEV="${IP_GOV_PORTALS_DEV:-192.168.11.54}" update_proxy_host "dbis.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3001" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dbis.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3001 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "iccc.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3002" false && updated_count=$((updated_count + 1)) || { add_proxy_host "iccc.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3002 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "omnl.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3003" false && updated_count=$((updated_count + 1)) || { add_proxy_host "omnl.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3003 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) update_proxy_host "xom.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3004" false && updated_count=$((updated_count + 1)) || { add_proxy_host "xom.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3004 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1)) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📊 Summary" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "✅ Updated: $updated_count" echo "❌ Failed: $failed_count" echo ""