feat(explorer): dual-chain wallet metadata, native coin pricing, and UI refresh.
Add Chain 138 wallet network metadata and stats coin-price enrichment; sync frontend explorer SPA, command center, and address/token pages with backend config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
103
scripts/cron/ensure-explorer-nginx-next-routes.sh
Executable file
103
scripts/cron/ensure-explorer-nginx-next-routes.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs INSIDE VMID 5000. Idempotent nginx guard for Next.js /addresses/* and catch-all /.
|
||||
# Installed to /usr/local/bin by explorer-monorepo/scripts/cron/install-explorer-cron.sh
|
||||
# and proxmox/scripts/deployment/patch-explorer-nginx-next-routes.sh.
|
||||
set -euo pipefail
|
||||
|
||||
SITE="${EXPLORER_NGINX_SITE:-/etc/nginx/sites-enabled/blockscout}"
|
||||
AVAILABLE="${EXPLORER_NGINX_AVAILABLE:-/etc/nginx/sites-available/blockscout}"
|
||||
BACKUP_DIR="${EXPLORER_NGINX_BACKUP_DIR:-/etc/nginx/sites-available/backups}"
|
||||
PROBE_PATH="${EXPLORER_NGINX_PROBE_PATH:-/addresses/0x582b82fbf721348ee490487dc8d99846a687806d}"
|
||||
MODE="${1:-repair}"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
for stray in /etc/nginx/sites-enabled/blockscout.bak.*; do
|
||||
[ -e "$stray" ] || continue
|
||||
mv "$stray" "$BACKUP_DIR/$(basename "$stray")"
|
||||
done
|
||||
|
||||
verify_nginx_routes() {
|
||||
local code
|
||||
code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 \
|
||||
-H 'Host: explorer.d-bis.org' \
|
||||
-H 'X-Forwarded-Proto: https' \
|
||||
"http://127.0.0.1${PROBE_PATH}" 2>/dev/null || echo '000')"
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
apply_nginx_routes() {
|
||||
if [ ! -f "$SITE" ]; then
|
||||
echo "ensure-explorer-nginx-next-routes: missing $SITE" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp "$SITE" "${BACKUP_DIR}/blockscout.bak.next-routes-$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
site = Path("/etc/nginx/sites-enabled/blockscout")
|
||||
text = site.read_text()
|
||||
|
||||
needle = "location ~ ^/(address|tx|"
|
||||
replacement = "location ~ ^/(address|addresses|tx|"
|
||||
if "address|addresses|" not in text:
|
||||
if needle not in text:
|
||||
raise SystemExit("nginx app-route regex anchor not found")
|
||||
text = text.replace(needle, replacement, 1)
|
||||
|
||||
catch_all = '''
|
||||
# Catch-all goes to the Next frontend after API/static exclusions.
|
||||
location / {
|
||||
if ($redirect_http_to_https = 1) { return 301 https://$host$request_uri; }
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
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_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_hide_header Cache-Control;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
'''
|
||||
|
||||
if "Catch-all goes to the Next frontend" not in text:
|
||||
marker = "\n}\n\n\nmap $http_upgrade $connection_upgrade {"
|
||||
if marker not in text:
|
||||
marker = "\n}\n\nmap $http_upgrade $connection_upgrade {"
|
||||
if marker not in text:
|
||||
raise SystemExit("server block closing anchor not found")
|
||||
text = text.replace(marker, catch_all + marker, 1)
|
||||
|
||||
site.write_text(text)
|
||||
Path("/etc/nginx/sites-available/blockscout").write_text(text)
|
||||
print("nginx: ensured /addresses route and Next.js catch-all")
|
||||
PY
|
||||
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
check)
|
||||
verify_nginx_routes
|
||||
;;
|
||||
repair)
|
||||
if verify_nginx_routes; then
|
||||
exit 0
|
||||
fi
|
||||
apply_nginx_routes
|
||||
verify_nginx_routes
|
||||
;;
|
||||
force)
|
||||
apply_nginx_routes
|
||||
verify_nginx_routes
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [check|repair|force]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -13,6 +13,15 @@ log() { echo "$(date -Iseconds) $*" >> "$LOG" 2>/dev/null || true; }
|
||||
# 1) Ensure PostgreSQL is running
|
||||
docker start blockscout-postgres 2>/dev/null || true
|
||||
|
||||
# 1b) Keep explorer-config-api DATABASE_URL aligned with blockscout-postgres bridge IP
|
||||
if [ -x /usr/local/bin/sync-explorer-config-api-database-url.sh ]; then
|
||||
if /usr/local/bin/sync-explorer-config-api-database-url.sh >>"$LOG" 2>&1; then
|
||||
:
|
||||
else
|
||||
log "sync-explorer-config-api-database-url failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2) Blockscout API health: if not 200, restart or start container
|
||||
CODE=$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1:4000/api/v2/stats 2>/dev/null || echo "000")
|
||||
if [ "$CODE" != "200" ]; then
|
||||
@@ -35,6 +44,14 @@ if [ "$NGINX" != "active" ]; then
|
||||
systemctl start nginx 2>>"$LOG" || true
|
||||
fi
|
||||
|
||||
# 3b) Next.js route guard: /addresses/* must proxy to port 3000 (not nginx 404)
|
||||
if [ -x /usr/local/bin/ensure-explorer-nginx-next-routes.sh ]; then
|
||||
if ! /usr/local/bin/ensure-explorer-nginx-next-routes.sh check >>"$LOG" 2>&1; then
|
||||
log "Explorer nginx /addresses route broken; repairing"
|
||||
/usr/local/bin/ensure-explorer-nginx-next-routes.sh repair >>"$LOG" 2>&1 || log "ensure-explorer-nginx-next-routes repair failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4) Safe disk prune (only if env RUN_PRUNE=1 or first run on Sunday 3am - use cron for daily)
|
||||
# Do NOT prune containers (would remove stopped Blockscout). Prune only unused images and build cache.
|
||||
if [ "${RUN_PRUNE:-0}" = "1" ]; then
|
||||
|
||||
@@ -31,16 +31,32 @@ fi
|
||||
|
||||
EXEC_PREFIX="pct exec $VMID --"
|
||||
MAINTAIN_SCRIPT="/usr/local/bin/explorer-maintain.sh"
|
||||
SYNC_DB_SCRIPT="/usr/local/bin/sync-explorer-config-api-database-url.sh"
|
||||
SYNC_SRC="$REPO_ROOT/scripts/sync-explorer-config-api-database-url.sh"
|
||||
NGINX_ENSURE_SRC="$SCRIPT_DIR/ensure-explorer-nginx-next-routes.sh"
|
||||
NGINX_ENSURE_SCRIPT="/usr/local/bin/ensure-explorer-nginx-next-routes.sh"
|
||||
|
||||
echo "=============================================="
|
||||
echo "Install explorer maintenance cron (VMID $VMID)"
|
||||
echo "=============================================="
|
||||
|
||||
# Copy script into VM
|
||||
# Copy scripts into VM
|
||||
pct push $VMID "$SCRIPT_DIR/explorer-maintain.sh" "$MAINTAIN_SCRIPT"
|
||||
$EXEC_PREFIX chmod +x "$MAINTAIN_SCRIPT"
|
||||
echo "✅ Installed $MAINTAIN_SCRIPT"
|
||||
|
||||
if [ -f "$SYNC_SRC" ]; then
|
||||
pct push $VMID "$SYNC_SRC" "$SYNC_DB_SCRIPT"
|
||||
$EXEC_PREFIX chmod +x "$SYNC_DB_SCRIPT"
|
||||
echo "✅ Installed $SYNC_DB_SCRIPT"
|
||||
fi
|
||||
|
||||
if [ -f "$NGINX_ENSURE_SRC" ]; then
|
||||
pct push $VMID "$NGINX_ENSURE_SRC" "$NGINX_ENSURE_SCRIPT"
|
||||
$EXEC_PREFIX chmod +x "$NGINX_ENSURE_SCRIPT"
|
||||
echo "✅ Installed $NGINX_ENSURE_SCRIPT"
|
||||
fi
|
||||
|
||||
# Install crontab (append to existing)
|
||||
$EXEC_PREFIX bash -c '(crontab -l 2>/dev/null | grep -v explorer-maintain | grep -v /usr/local/bin/explorer-maintain.sh || true; echo "# explorer-maintain"; echo "*/5 * * * * /usr/local/bin/explorer-maintain.sh >> /var/log/explorer-maintain.log 2>&1"; echo "15 3 * * * RUN_PRUNE=1 /usr/local/bin/explorer-maintain.sh >> /var/log/explorer-maintain.log 2>&1") | crontab -'
|
||||
echo "✅ Cron installed:"
|
||||
|
||||
@@ -32,6 +32,7 @@ STATIC_SYNC_FILES=(
|
||||
"terms.html"
|
||||
"acknowledgments.html"
|
||||
"chain138-command-center.html"
|
||||
"chain138-command-center.meta.json"
|
||||
"favicon.ico"
|
||||
"apple-touch-icon.png"
|
||||
"explorer-spa.js"
|
||||
@@ -105,6 +106,8 @@ acquire_build_lock() {
|
||||
}
|
||||
|
||||
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
|
||||
echo "== Refreshing command-center bundle metadata =="
|
||||
bash "${REPO_ROOT}/scripts/refresh-chain138-command-center-meta.sh"
|
||||
echo "== Building frontend =="
|
||||
acquire_build_lock
|
||||
rm -rf "${FRONTEND_ROOT}/.next"
|
||||
@@ -214,6 +217,22 @@ echo "== Verification =="
|
||||
run_in_vmid "systemctl is-active ${SERVICE_NAME}.service"
|
||||
run_in_vmid "curl -fsS --max-time 5 http://127.0.0.1:${FRONTEND_PORT}/ | grep -qiE 'DBIS Explorer|Chain 138 Explorer by DBIS'"
|
||||
echo "Service ${SERVICE_NAME} is running on 127.0.0.1:${FRONTEND_PORT}"
|
||||
|
||||
PATCH_NGINX="${WORKSPACE_ROOT}/proxmox/scripts/deployment/patch-explorer-nginx-next-routes.sh"
|
||||
if [[ -f "${PATCH_NGINX}" ]]; then
|
||||
echo ""
|
||||
echo "== Ensure nginx proxies /addresses to Next.js =="
|
||||
bash "${PATCH_NGINX}" || echo "WARN: nginx next-routes patch failed — run manually from proxmox repo"
|
||||
fi
|
||||
|
||||
SMOKE_SCRIPT="${WORKSPACE_ROOT}/proxmox/scripts/verify/smoke-explorer-institutional-grade.sh"
|
||||
if [[ -f "${SMOKE_SCRIPT}" ]]; then
|
||||
echo ""
|
||||
echo "== Institutional smoke gate =="
|
||||
EXPLORER_BASE="${EXPLORER_BASE:-https://explorer.d-bis.org}" bash "${SMOKE_SCRIPT}" || {
|
||||
echo "WARN: institutional smoke failed — see ${SMOKE_SCRIPT}" >&2
|
||||
}
|
||||
fi
|
||||
echo ""
|
||||
echo "Nginx follow-up:"
|
||||
echo " Switch the explorer server block to proxy / and /_next/ to 127.0.0.1:${FRONTEND_PORT}"
|
||||
|
||||
35
scripts/refresh-chain138-command-center-meta.sh
Executable file
35
scripts/refresh-chain138-command-center-meta.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Refresh chain138-command-center.meta.json before deploy (bundle version + optional route-matrix stamp).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
META="${REPO_ROOT}/frontend/public/chain138-command-center.meta.json"
|
||||
ROUTE_MATRIX="${REPO_ROOT}/config/aggregator-route-matrix.json"
|
||||
|
||||
python3 - "$META" "$ROUTE_MATRIX" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
meta_path = Path(sys.argv[1])
|
||||
route_matrix_path = Path(sys.argv[2])
|
||||
now = datetime.now(timezone.utc)
|
||||
route_updated = None
|
||||
if route_matrix_path.is_file():
|
||||
try:
|
||||
data = json.loads(route_matrix_path.read_text(encoding="utf-8"))
|
||||
route_updated = data.get("updated") or data.get("generatedAt")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
payload = {
|
||||
"bundleVersion": now.strftime("%Y-%m-%d"),
|
||||
"updatedAt": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"sourceDoc": "explorer-monorepo/docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md",
|
||||
"routeMatrixUpdated": route_updated,
|
||||
}
|
||||
meta_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"Updated {meta_path}")
|
||||
PY
|
||||
58
scripts/sync-explorer-config-api-database-url.sh
Executable file
58
scripts/sync-explorer-config-api-database-url.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync explorer-config-api DATABASE_URL to the live blockscout-postgres container IP.
|
||||
# Run inside VMID 5000 (or via pct exec). Safe to run from cron every 5 minutes.
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE="${EXPLORER_CONFIG_API_SERVICE:-explorer-config-api}"
|
||||
DROPIN_DIR="/etc/systemd/system/${SERVICE}.service.d"
|
||||
DROPIN_FILE="${DROPIN_DIR}/database.conf"
|
||||
CONTAINER="${BLOCKSCOUT_POSTGRES_CONTAINER:-blockscout-postgres}"
|
||||
DB_USER="${BLOCKSCOUT_DB_USER:-blockscout}"
|
||||
DB_PASSWORD="${BLOCKSCOUT_DB_PASSWORD:-blockscout}"
|
||||
DB_NAME="${BLOCKSCOUT_DB_NAME:-blockscout}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker not available" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||
echo "postgres container not running: $CONTAINER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CONTAINER" 2>/dev/null || true)"
|
||||
if [ -z "$DB_IP" ]; then
|
||||
echo "could not resolve IP for $CONTAINER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DESIRED_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_IP}:5432/${DB_NAME}?sslmode=disable"
|
||||
CURRENT_URL=""
|
||||
if [ -f "$DROPIN_FILE" ]; then
|
||||
CURRENT_URL="$(grep -E '^Environment=DATABASE_URL=' "$DROPIN_FILE" | head -1 | sed 's/^Environment=DATABASE_URL=//' || true)"
|
||||
fi
|
||||
|
||||
mkdir -p "$DROPIN_DIR"
|
||||
if [ "$CURRENT_URL" = "$DESIRED_URL" ]; then
|
||||
echo "DATABASE_URL already current ($DB_IP)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat > "$DROPIN_FILE" <<EOF
|
||||
[Service]
|
||||
Environment=DATABASE_URL=${DESIRED_URL}
|
||||
EOF
|
||||
chmod 600 "$DROPIN_FILE"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart "$SERVICE"
|
||||
sleep 2
|
||||
|
||||
if ! systemctl is-active --quiet "$SERVICE"; then
|
||||
echo "restart failed for $SERVICE" >&2
|
||||
systemctl status "$SERVICE" --no-pager -l || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "updated DATABASE_URL -> ${DB_IP} and restarted ${SERVICE}"
|
||||
@@ -169,7 +169,7 @@ else
|
||||
fi
|
||||
|
||||
# 11) Visual Command Center contains expected explorer architecture content
|
||||
if grep -qE 'Visual Command Center|Mission Control|mainnet cW mint corridor' /tmp/chain138-command-center.verify.$$ 2>/dev/null; then
|
||||
if grep -qE 'Mission Control|mainnet cW mint corridor|Stack A|0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895|mainnet_weth' /tmp/chain138-command-center.verify.$$ 2>/dev/null; then
|
||||
echo "✅ $BASE_URL/chain138-command-center.html contains command-center content"
|
||||
((PASS++)) || true
|
||||
else
|
||||
@@ -178,6 +178,27 @@ else
|
||||
fi
|
||||
rm -f /tmp/chain138-command-center.verify.$$
|
||||
|
||||
# 11b) Topology alias redirects to command center
|
||||
TOPO_CODE="$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 10 "$BASE_URL/topology" 2>/dev/null || echo 000)"
|
||||
if [ "$TOPO_CODE" = "200" ] || [ "$TOPO_CODE" = "307" ] || [ "$TOPO_CODE" = "308" ]; then
|
||||
echo "✅ $BASE_URL/topology reachable ($TOPO_CODE)"
|
||||
((PASS++)) || true
|
||||
else
|
||||
echo "❌ $BASE_URL/topology returned $TOPO_CODE"
|
||||
((FAIL++)) || true
|
||||
fi
|
||||
|
||||
# 11c) Command center bundle metadata
|
||||
META_CODE="$(curl -sS -o /tmp/chain138-command-center.meta.verify.$$ -w "%{http_code}" --connect-timeout 10 "$BASE_URL/chain138-command-center.meta.json" 2>/dev/null || echo 000)"
|
||||
if [ "$META_CODE" = "200" ] && grep -q 'bundleVersion' /tmp/chain138-command-center.meta.verify.$$ 2>/dev/null; then
|
||||
echo "✅ $BASE_URL/chain138-command-center.meta.json returns bundle metadata"
|
||||
((PASS++)) || true
|
||||
else
|
||||
echo "❌ $BASE_URL/chain138-command-center.meta.json missing or invalid ($META_CODE)"
|
||||
((FAIL++)) || true
|
||||
fi
|
||||
rm -f /tmp/chain138-command-center.meta.verify.$$
|
||||
|
||||
# 12) Mission Control SSE stream returns 200 with text/event-stream
|
||||
MC_STREAM_HEADERS="/tmp/mission-control-stream.headers.$$"
|
||||
MC_STREAM_BODY="/tmp/mission-control-stream.body.$$"
|
||||
|
||||
Reference in New Issue
Block a user