2026-04-07 23:22:12 -07:00
#!/usr/bin/env bash
# Deploy the current Next.js standalone frontend to VMID 5000.
2026-04-30 03:06:49 -07:00
# This is the canonical deployment path for the current DBIS Explorer frontend.
2026-04-07 23:22:12 -07:00
# It builds the local frontend, uploads the standalone bundle, installs a systemd
# service, and starts the Node server on 127.0.0.1:3000 inside the container.
set -euo pipefail
VMID = " ${ VMID :- 5000 } "
FRONTEND_PORT = " ${ FRONTEND_PORT :- 3000 } "
2026-04-23 09:51:01 -07:00
FORCE_REMOTE_PCT = " ${ FORCE_REMOTE_PCT :- 0 } "
2026-04-07 23:22:12 -07:00
SERVICE_NAME = "solacescanscout-frontend"
APP_ROOT = "/opt/solacescanscout/frontend"
PROXMOX_R630_02 = " ${ PROXMOX_HOST_R630_02 :- 192 .168.11.12 } "
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
REPO_ROOT = " $( cd " $SCRIPT_DIR /.. " && pwd ) "
WORKSPACE_ROOT = " $( cd " $REPO_ROOT /.. " && pwd ) "
FRONTEND_ROOT = " ${ REPO_ROOT } /frontend "
SERVICE_TEMPLATE = " ${ REPO_ROOT } /deployment/systemd/solacescanscout-frontend.service "
NGINX_SNIPPET = " ${ REPO_ROOT } /deployment/common/nginx-next-frontend-proxy.conf "
VERIFY_SCRIPT = " ${ WORKSPACE_ROOT } /scripts/verify/check-explorer-e2e.sh "
RELEASE_ID = " $( date +%Y%m%d_%H%M%S) "
TMP_DIR = " $( mktemp -d) "
ARCHIVE_NAME = " solacescanscout-next- ${ RELEASE_ID } .tar "
2026-04-12 06:33:54 -07:00
BUILD_LOCK_DIR = " ${ FRONTEND_ROOT } /.next-build-lock "
2026-04-25 23:45:07 -07:00
STANDALONE_ROOT = " ${ FRONTEND_ROOT } /.next/standalone "
2026-04-10 12:52:17 -07:00
STATIC_SYNC_FILES = (
"index.html"
"docs.html"
"privacy.html"
"terms.html"
"acknowledgments.html"
"chain138-command-center.html"
2026-06-19 16:16:17 -07:00
"chain138-command-center.meta.json"
2026-04-10 12:52:17 -07:00
"favicon.ico"
"apple-touch-icon.png"
"explorer-spa.js"
2026-05-22 21:55:42 -07:00
"legacy/index.html"
2026-04-10 12:52:17 -07:00
)
2026-04-07 23:22:12 -07:00
cleanup( ) {
2026-04-12 06:33:54 -07:00
if [ [ -d " $BUILD_LOCK_DIR " ] ] ; then
rmdir " $BUILD_LOCK_DIR " 2>/dev/null || true
fi
2026-04-07 23:22:12 -07:00
rm -rf " $TMP_DIR "
}
trap cleanup EXIT
if [ [ ! -f " $SERVICE_TEMPLATE " ] ] ; then
echo " Missing service template: $SERVICE_TEMPLATE " >& 2
exit 1
fi
push_into_vmid( ) {
local source_path = " $1 "
local destination_path = " $2 "
local perms = " ${ 3 :- 0644 } "
2026-04-23 09:51:01 -07:00
if [ [ " $FORCE_REMOTE_PCT " != "1" ] ] && [ [ -f /proc/1/cgroup ] ] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
2026-04-07 23:22:12 -07:00
install -D -m " $perms " " $source_path " " $destination_path "
elif command -v pct >/dev/null 2>& 1; then
pct push " $VMID " " $source_path " " $destination_path " --perms " $perms "
else
local remote_tmp = " /tmp/ $( basename " $source_path " ) . $$ "
scp -o ConnectTimeout = 10 -o StrictHostKeyChecking = no " $source_path " " root@ ${ PROXMOX_R630_02 } : ${ remote_tmp } "
ssh -o ConnectTimeout = 10 -o StrictHostKeyChecking = no " root@ ${ PROXMOX_R630_02 } " \
" pct push ${ VMID } ${ remote_tmp } ${ destination_path } --perms ${ perms } && rm -f ${ remote_tmp } "
fi
}
run_in_vmid( ) {
local command = " $1 "
2026-04-23 09:51:01 -07:00
if [ [ " $FORCE_REMOTE_PCT " != "1" ] ] && [ [ -f /proc/1/cgroup ] ] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
2026-04-07 23:22:12 -07:00
bash -lc " $command "
elif command -v pct >/dev/null 2>& 1; then
pct exec " $VMID " -- bash -lc " $command "
else
ssh -o ConnectTimeout = 10 -o StrictHostKeyChecking = no " root@ ${ PROXMOX_R630_02 } " \
" pct exec ${ VMID } -- bash -lc $( printf '%q' " $command " ) "
fi
}
echo "=========================================="
2026-04-30 03:06:49 -07:00
echo "Deploying Next DBIS Explorer Frontend"
2026-04-07 23:22:12 -07:00
echo "=========================================="
echo " VMID: $VMID "
echo " Frontend root: $FRONTEND_ROOT "
echo " Release: $RELEASE_ID "
echo ""
2026-04-12 06:33:54 -07:00
acquire_build_lock( ) {
local attempts = 0
until mkdir " $BUILD_LOCK_DIR " 2>/dev/null; do
attempts = $(( attempts + 1 ))
if ( ( attempts = = 1 ) ) ; then
echo "Waiting for another frontend build to finish..."
fi
if ( ( attempts >= 120 ) ) ; then
echo " Timed out waiting for frontend build lock: $BUILD_LOCK_DIR " >& 2
exit 1
fi
sleep 1
done
}
2026-04-07 23:22:12 -07:00
if [ [ " ${ SKIP_BUILD :- 0 } " != "1" ] ] ; then
2026-06-19 16:16:17 -07:00
echo "== Refreshing command-center bundle metadata =="
bash " ${ REPO_ROOT } /scripts/refresh-chain138-command-center-meta.sh "
2026-04-07 23:22:12 -07:00
echo "== Building frontend =="
2026-04-12 06:33:54 -07:00
acquire_build_lock
rm -rf " ${ FRONTEND_ROOT } /.next "
2026-04-07 23:22:12 -07:00
( cd " $FRONTEND_ROOT " && npm run build)
echo ""
fi
2026-04-25 23:45:07 -07:00
APP_RELATIVE_DIR = "."
APP_SERVER_PATH = " ${ STANDALONE_ROOT } /server.js "
if [ [ ! -f " ${ APP_SERVER_PATH } " ] ] ; then
2026-04-29 06:19:32 -07:00
ALT_SERVER_PATH = " $( find " ${ STANDALONE_ROOT } " \
-path '*/node_modules' -prune -o \
-path '*/server.js' -print | head -n 1 || true ) "
2026-04-25 23:45:07 -07:00
if [ [ -n " ${ ALT_SERVER_PATH } " ] ] ; then
APP_SERVER_PATH = " ${ ALT_SERVER_PATH } "
APP_RELATIVE_DIR = " $( dirname " ${ ALT_SERVER_PATH # ${ STANDALONE_ROOT } / } " ) "
fi
fi
if [ [ ! -f " ${ APP_SERVER_PATH } " ] ] ; then
2026-04-07 23:22:12 -07:00
echo " Missing standalone server build. Run \`npm run build\` in ${ FRONTEND_ROOT } first. " >& 2
exit 1
fi
STAGE_DIR = " ${ TMP_DIR } /stage "
2026-04-25 23:45:07 -07:00
APP_STAGE_DIR = " ${ STAGE_DIR } "
if [ [ " ${ APP_RELATIVE_DIR } " != "." ] ] ; then
APP_STAGE_DIR = " ${ STAGE_DIR } / ${ APP_RELATIVE_DIR } "
fi
mkdir -p " ${ APP_STAGE_DIR } /.next "
cp -R " ${ STANDALONE_ROOT } /. " " $STAGE_DIR / "
cp -R " ${ FRONTEND_ROOT } /.next/static " " ${ APP_STAGE_DIR } /.next/static "
cp -R " ${ FRONTEND_ROOT } /public " " ${ APP_STAGE_DIR } /public "
2026-04-07 23:22:12 -07:00
tar -C " $STAGE_DIR " -cf " ${ TMP_DIR } / ${ ARCHIVE_NAME } " .
cp " $SERVICE_TEMPLATE " " ${ TMP_DIR } / ${ SERVICE_NAME } .service "
sed -i " s|/opt/solacescanscout/frontend/current| ${ APP_ROOT } /current|g " " ${ TMP_DIR } / ${ SERVICE_NAME } .service "
sed -i " s|Environment=PORT=3000|Environment=PORT= ${ FRONTEND_PORT } |g " " ${ TMP_DIR } / ${ SERVICE_NAME } .service "
2026-04-30 03:06:49 -07:00
if [ [ " ${ APP_RELATIVE_DIR } " != "." ] ] ; then
sed -i " s|WorkingDirectory= ${ APP_ROOT } /current|WorkingDirectory= ${ APP_ROOT } /current/ ${ APP_RELATIVE_DIR } |g " " ${ TMP_DIR } / ${ SERVICE_NAME } .service "
sed -i " s|ExecStart=/usr/bin/node ${ APP_ROOT } /current/server.js|ExecStart=/usr/bin/node ${ APP_ROOT } /current/ ${ APP_RELATIVE_DIR } /server.js|g " " ${ TMP_DIR } / ${ SERVICE_NAME } .service "
fi
2026-04-07 23:22:12 -07:00
cat > " ${ TMP_DIR } /install-next-frontend.sh " <<EOF
#!/usr/bin/env bash
set -euo pipefail
APP_ROOT = " ${ APP_ROOT } "
RELEASE_DIR = " \${APP_ROOT}/releases/ ${ RELEASE_ID } "
SERVICE_NAME = " ${ SERVICE_NAME } "
FRONTEND_PORT = " ${ FRONTEND_PORT } "
mkdir -p "\${APP_ROOT}/releases"
mkdir -p "\${RELEASE_DIR}"
tar -xf " /tmp/ ${ ARCHIVE_NAME } " -C "\${RELEASE_DIR}"
ln -sfn "\${RELEASE_DIR}" "\${APP_ROOT}/current"
chown -R www-data:www-data "\${APP_ROOT}"
install -m 0644 " /tmp/ ${ SERVICE_NAME } .service " "/etc/systemd/system/\${SERVICE_NAME}.service"
systemctl daemon-reload
systemctl enable "\${SERVICE_NAME}.service" >/dev/null
systemctl restart "\${SERVICE_NAME}.service"
2026-04-10 12:52:17 -07:00
for attempt in \$ ( seq 1 45) ; do
2026-04-07 23:22:12 -07:00
if curl -fsS --max-time 5 "http://127.0.0.1:\${FRONTEND_PORT}/" > /tmp/\$ { SERVICE_NAME} -health.out; then
break
fi
sleep 1
done
2026-04-30 03:06:49 -07:00
if ! grep -qiE "DBIS Explorer|Chain 138 Explorer by DBIS" /tmp/\$ { SERVICE_NAME} -health.out; then
2026-04-10 12:52:17 -07:00
systemctl status "\${SERVICE_NAME}.service" --no-pager || true
journalctl -u "\${SERVICE_NAME}.service" -n 50 --no-pager || true
2026-04-30 03:06:49 -07:00
echo "Frontend health check did not find the expected DBIS Explorer marker." >& 2
2026-04-10 12:52:17 -07:00
exit 1
fi
mkdir -p /var/www/html
for relpath in ${ STATIC_SYNC_FILES [*] } ; do
if [ [ -f "\${RELEASE_DIR}/public/\${relpath}" ] ] ; then
install -D -m 0644 "\${RELEASE_DIR}/public/\${relpath}" "/var/www/html/\${relpath}"
fi
done
if [ [ -d "\${RELEASE_DIR}/public/thirdparty" ] ] ; then
mkdir -p /var/www/html/thirdparty
cp -a "\${RELEASE_DIR}/public/thirdparty/." /var/www/html/thirdparty/
fi
if [ [ -d "\${RELEASE_DIR}/public/config" ] ] ; then
mkdir -p /var/www/html/config
cp -a "\${RELEASE_DIR}/public/config/." /var/www/html/config/
fi
2026-04-07 23:22:12 -07:00
EOF
chmod +x " ${ TMP_DIR } /install-next-frontend.sh "
echo "== Uploading release bundle =="
push_into_vmid " ${ TMP_DIR } / ${ ARCHIVE_NAME } " " /tmp/ ${ ARCHIVE_NAME } " 0644
push_into_vmid " ${ TMP_DIR } / ${ SERVICE_NAME } .service " " /tmp/ ${ SERVICE_NAME } .service " 0644
push_into_vmid " ${ TMP_DIR } /install-next-frontend.sh " "/tmp/install-next-frontend.sh" 0755
echo ""
echo "== Installing release and restarting service =="
run_in_vmid "/tmp/install-next-frontend.sh"
echo ""
echo "== Verification =="
run_in_vmid " systemctl is-active ${ SERVICE_NAME } .service "
2026-04-30 03:06:49 -07:00
run_in_vmid " curl -fsS --max-time 5 http://127.0.0.1: ${ FRONTEND_PORT } / | grep -qiE 'DBIS Explorer|Chain 138 Explorer by DBIS' "
2026-04-07 23:22:12 -07:00
echo " Service ${ SERVICE_NAME } is running on 127.0.0.1: ${ FRONTEND_PORT } "
2026-06-19 16:16:17 -07:00
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
2026-06-22 15:52:47 -07:00
SMOKE_ROUTES = " ${ FRONTEND_ROOT } /scripts/smoke-routes.mjs "
SMOKE_SCROLL = " ${ FRONTEND_ROOT } /scripts/smoke-scroll-height.mjs "
if [ [ -f " ${ SMOKE_ROUTES } " ] ] && [ [ " ${ EXPLORER_SKIP_ROUTE_SMOKE :- } " != "1" ] ] ; then
echo ""
echo "== Playwright route smoke (optional) =="
if ( cd " ${ FRONTEND_ROOT } " && npm exec -- playwright install chromium >/dev/null 2>& 1 && BASE_URL = " ${ EXPLORER_BASE :- https : //explorer.d-bis.org } " npm run smoke:routes) ; then
echo "Route smoke: PASS"
else
echo "WARN: route smoke skipped or failed — set EXPLORER_SKIP_ROUTE_SMOKE=1 to silence; run npm install in frontend first" >& 2
fi
fi
if [ [ -f " ${ SMOKE_SCROLL } " ] ] && [ [ " ${ EXPLORER_SKIP_SCROLL_SMOKE :- } " != "1" ] ] && [ [ " ${ EXPLORER_SKIP_ROUTE_SMOKE :- } " != "1" ] ] ; then
echo ""
echo "== Playwright scroll-height smoke (optional) =="
if ( cd " ${ FRONTEND_ROOT } " && BASE_URL = " ${ EXPLORER_BASE :- https : //explorer.d-bis.org } " npm run smoke:scroll) ; then
echo "Scroll-height smoke: PASS"
else
echo "WARN: scroll-height smoke failed — set EXPLORER_SKIP_SCROLL_SMOKE=1 to silence" >& 2
fi
fi
2026-04-07 23:22:12 -07:00
echo ""
echo "Nginx follow-up:"
echo " Switch the explorer server block to proxy / and /_next/ to 127.0.0.1: ${ FRONTEND_PORT } "
echo " while preserving /api/, /api/config/*, /explorer-api/v1/, /token-aggregation/api/v1/, /snap/, and /health."
echo " Snippet: ${ NGINX_SNIPPET } "
if [ [ -f " ${ VERIFY_SCRIPT } " ] ] ; then
echo " After nginx/NPMplus cutover, verify with:"
2026-04-10 12:52:17 -07:00
echo " bash ${ VERIFY_SCRIPT } https://blockscout.defi-oracle.io "
2026-04-07 23:22:12 -07:00
fi
echo ""
echo "Next frontend deployment complete."