From 525f9928548bd5ad0eae91c995c73832ff75e4c1 Mon Sep 17 00:00:00 2001 From: nsatoshi Date: Sun, 26 Apr 2026 00:11:49 +0000 Subject: [PATCH 1/9] feat(deploy): add CROMERO dapp Phoenix deploy target (#12) Wires d-bis/CROMERO into the Phoenix Deploy API pipeline so pushes to main on that repo land at https://d-bis.org/ecosystem/cromero/. Companion: https://gitea.d-bis.org/d-bis/CROMERO/pulls/1 --- docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md | 70 ++++++++++ phoenix-deploy-api/deploy-targets.json | 23 +++ .../phoenix-deploy-cromero-from-workspace.sh | 131 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md create mode 100755 scripts/deployment/phoenix-deploy-cromero-from-workspace.sh diff --git a/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md new file mode 100644 index 00000000..72e92189 --- /dev/null +++ b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md @@ -0,0 +1,70 @@ +# CROMERO Dapp — Phoenix Deploy + +Deploys [`d-bis/CROMERO`](https://gitea.d-bis.org/d-bis/CROMERO) (a +Vite + React + thirdweb v5 dapp) to +`https://d-bis.org/ecosystem/cromero/`. + +## Pipeline + +1. Push to `main` on `d-bis/CROMERO` triggers + `.gitea/workflows/deploy-to-phoenix.yml`, which POSTs + `{repo, sha, branch, target: "default"}` to the Phoenix Deploy API. +2. Phoenix runs the registered target (see + `phoenix-deploy-api/deploy-targets.json`): + `bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh`. +3. The script builds the staged workspace (`npm ci && npm run build`) + and rsyncs `dist/` to + `${IP_NPMPLUS:-192.168.11.167}:/var/www/ecosystem/cromero/`. +4. Healthcheck: `https://d-bis.org/ecosystem/cromero/` must return + HTTP 200 with `
` in the body. + +## One-time nginx setup on the NPMplus host + +The deploy script does **not** modify nginx config — install the +location block once, then redeploys land static files only. + +Add the following to whichever nginx server block terminates +`d-bis.org` (or the NPMplus advanced-config tab for the d-bis.org +proxy host): + +```nginx +location /ecosystem/cromero/ { + alias /var/www/ecosystem/cromero/; + try_files $uri $uri/ /ecosystem/cromero/index.html; +} +``` + +Then `nginx -t && nginx -s reload` (or restart the NPMplus +container). + +The Vite app already builds with `base: "/ecosystem/cromero/"` so +hashed asset URLs resolve under that subpath. + +## Required Actions secrets/vars on `d-bis/CROMERO` + +| Name | Type | Value | +| --- | --- | --- | +| `PHOENIX_DEPLOY_URL` | secret | `http://192.168.11.59:4001/api/deploy` | +| `PHOENIX_DEPLOY_TOKEN` | secret | matches `PHOENIX_DEPLOY_SECRET` on the Phoenix host | +| `VITE_THIRDWEB_CLIENT_ID` | secret | thirdweb publishable Client ID | +| `VITE_PROJECT_WALLET_ADDRESS` | var | recipient `0x…` address | +| `VITE_CHAIN_138_RPC` | var (optional) | overrides default `https://rpc.d-bis.org` | +| `VITE_CHAIN_138_EXPLORER` | var (optional) | overrides default `https://explorer.d-bis.org` | + +## Manual trigger from a LAN box with `phoenix-deploy-api` access + +```bash +curl -sSf -X POST "http://192.168.11.59:4001/api/deploy" \ + -H "Authorization: Bearer ${PHOENIX_DEPLOY_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"repo":"d-bis/CROMERO","branch":"main","target":"default"}' +``` + +## Dry run + +From this repo: + +```bash +PHOENIX_DEPLOY_WORKSPACE=/path/to/staged/CROMERO \ + bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh --dry-run +``` diff --git a/phoenix-deploy-api/deploy-targets.json b/phoenix-deploy-api/deploy-targets.json index 5f9ffa01..f776fae5 100644 --- a/phoenix-deploy-api/deploy-targets.json +++ b/phoenix-deploy-api/deploy-targets.json @@ -125,6 +125,29 @@ "timeout_ms": 15000 } }, + { + "repo": "d-bis/CROMERO", + "branch": "main", + "target": "default", + "description": "Deploy CROMERO dapp from the staged Gitea workspace: build dist/, rsync to NPMplus host /var/www/ecosystem/cromero/, served at https://d-bis.org/ecosystem/cromero/.", + "cwd": "${PHOENIX_REPO_ROOT}", + "command": [ + "bash", + "scripts/deployment/phoenix-deploy-cromero-from-workspace.sh" + ], + "required_env": [ + "PHOENIX_REPO_ROOT", + "PHOENIX_DEPLOY_WORKSPACE" + ], + "healthcheck": { + "url": "https://d-bis.org/ecosystem/cromero/", + "expect_status": 200, + "expect_body_includes": "
", + "attempts": 12, + "delay_ms": 5000, + "timeout_ms": 15000 + } + }, { "repo": "d-bis/proxmox", "branch": "main", diff --git a/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh new file mode 100755 index 00000000..80503790 --- /dev/null +++ b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy d-bis/CROMERO (Vite + thirdweb v5 dapp) from the Phoenix-staged +# workspace to the NPMplus host's /var/www/ecosystem/cromero/, served by +# nginx under https://d-bis.org/ecosystem/cromero/. +# +# Phoenix Deploy API target: +# { repo: "d-bis/CROMERO", branch: "main", target: "default" } +# +# Required env (from Phoenix host): +# PHOENIX_DEPLOY_WORKSPACE Full staged CROMERO checkout +# +# Optional env (sane defaults from config/ip-addresses.conf): +# NPMPLUS_HOST Default: ${IP_NPMPLUS:-192.168.11.167} +# NPMPLUS_SSH_USER Default: root +# NPMPLUS_DEPLOY_ROOT Default: /var/www/ecosystem/cromero +# PUBLIC_URL Default: https://d-bis.org/ecosystem/cromero/ +# VITE_THIRDWEB_CLIENT_ID Build-time only; safe to ship. +# VITE_PROJECT_WALLET_ADDRESS +# VITE_CHAIN_138_RPC Default in app: https://rpc.d-bis.org +# VITE_CHAIN_138_EXPLORER Default in app: https://explorer.d-bis.org +# DRY_RUN=1 Print actions without executing them. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# shellcheck source=/dev/null +source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" +# shellcheck source=/dev/null +source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true + +PHOENIX_DEPLOY_WORKSPACE="${PHOENIX_DEPLOY_WORKSPACE:-}" +NPMPLUS_HOST="${NPMPLUS_HOST:-${IP_NPMPLUS:-192.168.11.167}}" +NPMPLUS_SSH_USER="${NPMPLUS_SSH_USER:-root}" +NPMPLUS_DEPLOY_ROOT="${NPMPLUS_DEPLOY_ROOT:-/var/www/ecosystem/cromero}" +PUBLIC_URL="${PUBLIC_URL:-https://d-bis.org/ecosystem/cromero/}" +DRY_RUN="${DRY_RUN:-0}" + +usage() { + cat <<'USAGE' +Usage: phoenix-deploy-cromero-from-workspace.sh [--dry-run] + +Builds the staged CROMERO workspace, rsyncs the dist/ output to the +NPMplus host's /var/www/ecosystem/cromero/, and verifies the public +URL. + +The nginx location block must be installed once on the host (out of +band) — see docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md. +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +log() { printf '[cromero-phoenix] %s\n' "$*" >&2; } +die() { printf '[cromero-phoenix][FATAL] %s\n' "$*" >&2; exit 1; } +run() { + if [[ "$DRY_RUN" -eq 1 ]]; then + printf '[dry-run] %s\n' "$*" >&2 + else + eval "$*" + fi +} +need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; } + +for cmd in ssh rsync tar curl jq mktemp node npm; do + need_cmd "$cmd" +done + +[[ -n "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "PHOENIX_DEPLOY_WORKSPACE is required" +[[ -d "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "staged workspace missing: $PHOENIX_DEPLOY_WORKSPACE" + +SSH_TARGET="${NPMPLUS_SSH_USER}@${NPMPLUS_HOST}" +SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new) + +log "building CROMERO from staged workspace: ${PHOENIX_DEPLOY_WORKSPACE}" +pushd "$PHOENIX_DEPLOY_WORKSPACE" >/dev/null + +if [[ -f package-lock.json ]]; then + run "npm ci --no-audit --no-fund" +else + run "npm install --no-audit --no-fund" +fi + +# Build env. Empty values are fine — src/config.ts has defaults for +# Chain 138 RPC/explorer, and the dapp prints in-UI banners when the +# Client ID or wallet address are missing. +run "VITE_THIRDWEB_CLIENT_ID='${VITE_THIRDWEB_CLIENT_ID:-}' \ + VITE_PROJECT_WALLET_ADDRESS='${VITE_PROJECT_WALLET_ADDRESS:-}' \ + VITE_CHAIN_138_RPC='${VITE_CHAIN_138_RPC:-}' \ + VITE_CHAIN_138_EXPLORER='${VITE_CHAIN_138_EXPLORER:-}' \ + npm run build" + +[[ -f dist/index.html ]] || die "build produced no dist/index.html" + +popd >/dev/null + +log "rsync dist/ -> ${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/" +run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' \"mkdir -p '${NPMPLUS_DEPLOY_ROOT}'\"" +run "rsync -az --delete -e 'ssh ${SSH_OPTS[*]}' \ + '${PHOENIX_DEPLOY_WORKSPACE}/dist/' \ + '${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/'" + +# Reload nginx if a config file matching cromero exists. We do NOT +# write the location block from this script — it is installed once, +# out of band (see CROMERO_DAPP_DEPLOYMENT.md). Skip silently if +# nginx is not present (NPMplus may run nginx inside a container). +log "attempting nginx reload on ${NPMPLUS_HOST} (best-effort)" +run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' ' + if command -v nginx >/dev/null 2>&1; then + nginx -t && nginx -s reload || true + fi + if command -v docker >/dev/null 2>&1; then + docker exec npmplus nginx -s reload >/dev/null 2>&1 || true + fi +'" + +log "verifying ${PUBLIC_URL}" +HTTP_STATUS="$(curl -sS -o /dev/null -m 15 -w '%{http_code}' "$PUBLIC_URL" || echo 000)" +if [[ "$HTTP_STATUS" == "200" ]]; then + log "OK: ${PUBLIC_URL} returned 200" +else + log "WARN: ${PUBLIC_URL} returned ${HTTP_STATUS} (nginx location block may not yet be installed)" +fi + +log "CROMERO Phoenix deploy completed from ${PHOENIX_DEPLOY_WORKSPACE}" From 88cad0cd341acf930e0f38ad74fd349c235cf730 Mon Sep 17 00:00:00 2001 From: nsatoshi Date: Sun, 26 Apr 2026 00:50:53 +0000 Subject: [PATCH 2/9] fix(deploy): make Cromero Phoenix deploy target Proxmox-aware --- .../phoenix-deploy-cromero-from-workspace.sh | 170 ++++++++---------- 1 file changed, 70 insertions(+), 100 deletions(-) diff --git a/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh index 80503790..cf6042a7 100755 --- a/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh +++ b/scripts/deployment/phoenix-deploy-cromero-from-workspace.sh @@ -1,131 +1,101 @@ #!/usr/bin/env bash set -euo pipefail -# Deploy d-bis/CROMERO (Vite + thirdweb v5 dapp) from the Phoenix-staged -# workspace to the NPMplus host's /var/www/ecosystem/cromero/, served by -# nginx under https://d-bis.org/ecosystem/cromero/. -# -# Phoenix Deploy API target: -# { repo: "d-bis/CROMERO", branch: "main", target: "default" } -# -# Required env (from Phoenix host): -# PHOENIX_DEPLOY_WORKSPACE Full staged CROMERO checkout -# -# Optional env (sane defaults from config/ip-addresses.conf): -# NPMPLUS_HOST Default: ${IP_NPMPLUS:-192.168.11.167} -# NPMPLUS_SSH_USER Default: root -# NPMPLUS_DEPLOY_ROOT Default: /var/www/ecosystem/cromero -# PUBLIC_URL Default: https://d-bis.org/ecosystem/cromero/ -# VITE_THIRDWEB_CLIENT_ID Build-time only; safe to ship. -# VITE_PROJECT_WALLET_ADDRESS -# VITE_CHAIN_138_RPC Default in app: https://rpc.d-bis.org -# VITE_CHAIN_138_EXPLORER Default in app: https://explorer.d-bis.org -# DRY_RUN=1 Print actions without executing them. - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# shellcheck source=/dev/null -source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" -# shellcheck source=/dev/null -source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true +if [[ -f "$PROJECT_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck source=/dev/null + source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" +fi +if [[ -f "$PROJECT_ROOT/config/ip-addresses.conf" ]]; then + # shellcheck source=/dev/null + source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true +fi PHOENIX_DEPLOY_WORKSPACE="${PHOENIX_DEPLOY_WORKSPACE:-}" -NPMPLUS_HOST="${NPMPLUS_HOST:-${IP_NPMPLUS:-192.168.11.167}}" -NPMPLUS_SSH_USER="${NPMPLUS_SSH_USER:-root}" +NPMPLUS_PROXMOX_HOST="${NPMPLUS_PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}" +NPMPLUS_VMID="${NPMPLUS_VMID:-10233}" NPMPLUS_DEPLOY_ROOT="${NPMPLUS_DEPLOY_ROOT:-/var/www/ecosystem/cromero}" +NPMPLUS_DATA_ROOT="${NPMPLUS_DATA_ROOT:-/opt/npmplus/html/ecosystem/cromero}" PUBLIC_URL="${PUBLIC_URL:-https://d-bis.org/ecosystem/cromero/}" DRY_RUN="${DRY_RUN:-0}" +SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new) +TMP_ARCHIVE="/tmp/cromero-dapp-dist-$$.tgz" -usage() { - cat <<'USAGE' -Usage: phoenix-deploy-cromero-from-workspace.sh [--dry-run] - -Builds the staged CROMERO workspace, rsyncs the dist/ output to the -NPMplus host's /var/www/ecosystem/cromero/, and verifies the public -URL. - -The nginx location block must be installed once on the host (out of -band) — see docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md. -USAGE -} +usage() { printf 'Usage: %s [--dry-run]\n' "$(basename "$0")"; } +log() { printf '[cromero-phoenix] %s\n' "$*" >&2; } +die() { printf '[cromero-phoenix][FATAL] %s\n' "$*" >&2; exit 1; } +need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; } +run() { if [[ "$DRY_RUN" == 1 ]]; then printf '[dry-run] %q ' "$@" >&2; printf '\n' >&2; else "$@"; fi; } +cleanup() { rm -f "$TMP_ARCHIVE"; } +trap cleanup EXIT while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; - *) echo "unknown arg: $1" >&2; usage; exit 2 ;; + *) die "unknown arg: $1" ;; esac done -log() { printf '[cromero-phoenix] %s\n' "$*" >&2; } -die() { printf '[cromero-phoenix][FATAL] %s\n' "$*" >&2; exit 1; } -run() { - if [[ "$DRY_RUN" -eq 1 ]]; then - printf '[dry-run] %s\n' "$*" >&2 - else - eval "$*" - fi -} -need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; } - -for cmd in ssh rsync tar curl jq mktemp node npm; do - need_cmd "$cmd" -done - +for cmd in ssh scp tar curl node npm; do need_cmd "$cmd"; done [[ -n "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "PHOENIX_DEPLOY_WORKSPACE is required" [[ -d "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "staged workspace missing: $PHOENIX_DEPLOY_WORKSPACE" -SSH_TARGET="${NPMPLUS_SSH_USER}@${NPMPLUS_HOST}" -SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new) - -log "building CROMERO from staged workspace: ${PHOENIX_DEPLOY_WORKSPACE}" +log "building CROMERO from staged workspace: $PHOENIX_DEPLOY_WORKSPACE" pushd "$PHOENIX_DEPLOY_WORKSPACE" >/dev/null - if [[ -f package-lock.json ]]; then - run "npm ci --no-audit --no-fund" + run npm ci --no-audit --no-fund else - run "npm install --no-audit --no-fund" + run npm install --no-audit --no-fund fi - -# Build env. Empty values are fine — src/config.ts has defaults for -# Chain 138 RPC/explorer, and the dapp prints in-UI banners when the -# Client ID or wallet address are missing. -run "VITE_THIRDWEB_CLIENT_ID='${VITE_THIRDWEB_CLIENT_ID:-}' \ - VITE_PROJECT_WALLET_ADDRESS='${VITE_PROJECT_WALLET_ADDRESS:-}' \ - VITE_CHAIN_138_RPC='${VITE_CHAIN_138_RPC:-}' \ - VITE_CHAIN_138_EXPLORER='${VITE_CHAIN_138_EXPLORER:-}' \ - npm run build" - +run env \ + VITE_THIRDWEB_CLIENT_ID="${VITE_THIRDWEB_CLIENT_ID:-}" \ + VITE_PROJECT_WALLET_ADDRESS="${VITE_PROJECT_WALLET_ADDRESS:-0x3E309b87fA79092767531a0A6F5B6c3480737c5e}" \ + VITE_CHAIN_138_RPC="${VITE_CHAIN_138_RPC:-https://rpc.d-bis.org}" \ + VITE_CHAIN_138_EXPLORER="${VITE_CHAIN_138_EXPLORER:-https://explorer.d-bis.org}" \ + npm run build [[ -f dist/index.html ]] || die "build produced no dist/index.html" - +tar -C "$PHOENIX_DEPLOY_WORKSPACE" -czf "$TMP_ARCHIVE" dist popd >/dev/null -log "rsync dist/ -> ${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/" -run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' \"mkdir -p '${NPMPLUS_DEPLOY_ROOT}'\"" -run "rsync -az --delete -e 'ssh ${SSH_OPTS[*]}' \ - '${PHOENIX_DEPLOY_WORKSPACE}/dist/' \ - '${SSH_TARGET}:${NPMPLUS_DEPLOY_ROOT}/'" - -# Reload nginx if a config file matching cromero exists. We do NOT -# write the location block from this script — it is installed once, -# out of band (see CROMERO_DAPP_DEPLOYMENT.md). Skip silently if -# nginx is not present (NPMplus may run nginx inside a container). -log "attempting nginx reload on ${NPMPLUS_HOST} (best-effort)" -run "ssh ${SSH_OPTS[*]} '${SSH_TARGET}' ' - if command -v nginx >/dev/null 2>&1; then - nginx -t && nginx -s reload || true - fi - if command -v docker >/dev/null 2>&1; then - docker exec npmplus nginx -s reload >/dev/null 2>&1 || true - fi -'" - -log "verifying ${PUBLIC_URL}" -HTTP_STATUS="$(curl -sS -o /dev/null -m 15 -w '%{http_code}' "$PUBLIC_URL" || echo 000)" -if [[ "$HTTP_STATUS" == "200" ]]; then - log "OK: ${PUBLIC_URL} returned 200" -else - log "WARN: ${PUBLIC_URL} returned ${HTTP_STATUS} (nginx location block may not yet be installed)" +log "deploying dist/ through $NPMPLUS_PROXMOX_HOST to CT $NPMPLUS_VMID:$NPMPLUS_DEPLOY_ROOT" +run scp "${SSH_OPTS[@]}" "$TMP_ARCHIVE" "root@$NPMPLUS_PROXMOX_HOST:/tmp/cromero-dapp-dist.tgz" +if [[ "$DRY_RUN" == 1 ]]; then + log "dry-run complete" + exit 0 fi +ssh "${SSH_OPTS[@]}" "root@$NPMPLUS_PROXMOX_HOST" bash -s -- "$NPMPLUS_VMID" "$NPMPLUS_DEPLOY_ROOT" "$NPMPLUS_DATA_ROOT" <<'INNER' +set -euo pipefail +vmid="$1" +deploy_root="$2" +data_root="$3" +pct exec "$vmid" -- bash -lc " +set -euo pipefail +mkdir -p '$data_root' /var/www/ecosystem +if [ -e '$deploy_root' ] && [ ! -L '$deploy_root' ]; then rm -rf '$deploy_root'; fi +ln -sfn '$data_root' '$deploy_root' +rm -rf /tmp/cromero-dist +" +pct push "$vmid" /tmp/cromero-dapp-dist.tgz /tmp/cromero-dapp-dist.tgz +pct exec "$vmid" -- bash -lc " +set -euo pipefail +rm -rf /tmp/cromero-dist +mkdir -p /tmp/cromero-dist '$data_root' +tar -xzf /tmp/cromero-dapp-dist.tgz -C /tmp/cromero-dist +find '$data_root' -mindepth 1 -maxdepth 1 -exec rm -rf {} + +cp -R /tmp/cromero-dist/dist/. '$data_root/' +chown -R root:root /opt/npmplus/html/ecosystem /var/www/ecosystem +chmod 755 /opt/npmplus/html /opt/npmplus/html/ecosystem '$data_root' /var/www /var/www/ecosystem +if command -v docker >/dev/null 2>&1; then + docker exec npmplus sh -lc 'rm -rf /var/www && ln -s /data/html /var/www && nginx -t && nginx -s reload' +fi +rm -rf /tmp/cromero-dist /tmp/cromero-dapp-dist.tgz +" +rm -f /tmp/cromero-dapp-dist.tgz +INNER -log "CROMERO Phoenix deploy completed from ${PHOENIX_DEPLOY_WORKSPACE}" +log "verifying $PUBLIC_URL" +body="$(curl -fsS --max-time 20 "$PUBLIC_URL")" || die "public URL failed: $PUBLIC_URL" +printf '%s' "$body" | grep -F '
' >/dev/null || die "public URL missing React root" +log "CROMERO Phoenix deploy completed" From c2560c5b380a4b50e1ab7fc049f02e63b4f96940 Mon Sep 17 00:00:00 2001 From: nsatoshi Date: Sun, 26 Apr 2026 00:50:54 +0000 Subject: [PATCH 3/9] docs(deploy): document live Cromero NPMplus topology --- docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md index 72e92189..c384dcd7 100644 --- a/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md +++ b/docs/03-deployment/CROMERO_DAPP_DEPLOYMENT.md @@ -12,33 +12,45 @@ Vite + React + thirdweb v5 dapp) to 2. Phoenix runs the registered target (see `phoenix-deploy-api/deploy-targets.json`): `bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh`. -3. The script builds the staged workspace (`npm ci && npm run build`) - and rsyncs `dist/` to - `${IP_NPMPLUS:-192.168.11.167}:/var/www/ecosystem/cromero/`. +3. The script builds the staged workspace (`npm ci && npm run build`), + copies `dist/` through `r630-01` into NPMplus CT `10233`, and lands + files in `/var/www/ecosystem/cromero/`. 4. Healthcheck: `https://d-bis.org/ecosystem/cromero/` must return - HTTP 200 with `
` in the body. + HTTP 200 with `
` in the body. + +## Live NPMplus topology + +The primary NPMplus ingress is Dockerized inside CT `10233`, reachable via +Proxmox host `r630-01` (`192.168.11.11`). Direct SSH to +`192.168.11.167` is not assumed. The deploy script therefore uses +`pct push` / `pct exec` through the Proxmox host and keeps these paths +aligned: + +- CT host path: `/var/www/ecosystem/cromero/` +- Persistent NPMplus data path: `/opt/npmplus/html/ecosystem/cromero/` +- Nginx container path: `/var/www -> /data/html` ## One-time nginx setup on the NPMplus host -The deploy script does **not** modify nginx config — install the -location block once, then redeploys land static files only. - -Add the following to whichever nginx server block terminates -`d-bis.org` (or the NPMplus advanced-config tab for the d-bis.org -proxy host): +Install this once in the NPMplus advanced-config tab for the `d-bis.org` +proxy host: ```nginx +location = /ecosystem/cromero { + return 301 /ecosystem/cromero/; +} + location /ecosystem/cromero/ { alias /var/www/ecosystem/cromero/; - try_files $uri $uri/ /ecosystem/cromero/index.html; + index index.html; + try_files $uri /ecosystem/cromero/index.html; } ``` -Then `nginx -t && nginx -s reload` (or restart the NPMplus -container). +Then run nginx config validation/reload inside the NPMplus container. -The Vite app already builds with `base: "/ecosystem/cromero/"` so -hashed asset URLs resolve under that subpath. +The Vite app builds with `base: "/ecosystem/cromero/"` so hashed asset URLs +resolve under that subpath. ## Required Actions secrets/vars on `d-bis/CROMERO` @@ -47,17 +59,14 @@ hashed asset URLs resolve under that subpath. | `PHOENIX_DEPLOY_URL` | secret | `http://192.168.11.59:4001/api/deploy` | | `PHOENIX_DEPLOY_TOKEN` | secret | matches `PHOENIX_DEPLOY_SECRET` on the Phoenix host | | `VITE_THIRDWEB_CLIENT_ID` | secret | thirdweb publishable Client ID | -| `VITE_PROJECT_WALLET_ADDRESS` | var | recipient `0x…` address | -| `VITE_CHAIN_138_RPC` | var (optional) | overrides default `https://rpc.d-bis.org` | -| `VITE_CHAIN_138_EXPLORER` | var (optional) | overrides default `https://explorer.d-bis.org` | +| `VITE_PROJECT_WALLET_ADDRESS` | var | recipient `0x...` address | +| `VITE_CHAIN_138_RPC` | var (optional) | defaults to `https://rpc.d-bis.org` | +| `VITE_CHAIN_138_EXPLORER` | var (optional) | defaults to `https://explorer.d-bis.org` | ## Manual trigger from a LAN box with `phoenix-deploy-api` access ```bash -curl -sSf -X POST "http://192.168.11.59:4001/api/deploy" \ - -H "Authorization: Bearer ${PHOENIX_DEPLOY_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"repo":"d-bis/CROMERO","branch":"main","target":"default"}' +curl -sSf -X POST "http://192.168.11.59:4001/api/deploy" -H "Authorization: Bearer ${PHOENIX_DEPLOY_TOKEN}" -H "Content-Type: application/json" -d '{"repo":"d-bis/CROMERO","branch":"main","target":"default"}' ``` ## Dry run @@ -65,6 +74,5 @@ curl -sSf -X POST "http://192.168.11.59:4001/api/deploy" \ From this repo: ```bash -PHOENIX_DEPLOY_WORKSPACE=/path/to/staged/CROMERO \ - bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh --dry-run +PHOENIX_DEPLOY_WORKSPACE=/path/to/staged/CROMERO bash scripts/deployment/phoenix-deploy-cromero-from-workspace.sh --dry-run ``` From ee165fb432f2e852158c43d92f72c779785bef0f Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 11:35:12 -0700 Subject: [PATCH 4/9] Add DBIS portal live deploy target --- phoenix-deploy-api/deploy-targets.json | 24 +++ ...-deploy-dbis-portal-live-from-workspace.sh | 167 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100755 scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh diff --git a/phoenix-deploy-api/deploy-targets.json b/phoenix-deploy-api/deploy-targets.json index f776fae5..182c1b19 100644 --- a/phoenix-deploy-api/deploy-targets.json +++ b/phoenix-deploy-api/deploy-targets.json @@ -102,6 +102,30 @@ "timeout_ms": 15000 } }, + { + "repo": "Gov_Web_Portals/DBIS", + "branch": "main", + "target": "dbis-portal-live", + "description": "Redeploy the DBIS public portal on CT 7804 from the staged DBIS checkout overlaid into the Gov Portals workspace.", + "cwd": "${PHOENIX_REPO_ROOT}", + "command": [ + "bash", + "scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh" + ], + "required_env": [ + "PHOENIX_REPO_ROOT", + "PHOENIX_DEPLOY_WORKSPACE" + ], + "timeout_sec": 2400, + "healthcheck": { + "url": "https://d-bis.org/.well-known/trust.json", + "expect_status": 200, + "expect_body_includes": "\"organization\"", + "attempts": 12, + "delay_ms": 5000, + "timeout_ms": 15000 + } + }, { "repo": "d-bis/CurrenciCombo", "branch": "main", diff --git a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh new file mode 100755 index 00000000..f417bca5 --- /dev/null +++ b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# Deploy the DBIS public portal from a Phoenix Deploy API staged DBIS checkout. +# +# The DBIS repo is normally a submodule of Gov_Web_Portals/gov-portals-monorepo +# and depends on the parent workspace package @public-web-portals/shared. This +# wrapper builds a temporary monorepo-shaped workspace, overlays the staged DBIS +# source into it, syncs that tree to CT 7804, then rebuilds/restarts DBIS. + +set -euo pipefail + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +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 +[ -f "$PROJECT_ROOT/.env" ] && set +u && source "$PROJECT_ROOT/.env" 2>/dev/null || true && set -u + +PHOENIX_REPO_ROOT="${PHOENIX_REPO_ROOT:-$PROJECT_ROOT}" +PHOENIX_DEPLOY_WORKSPACE="${PHOENIX_DEPLOY_WORKSPACE:-}" +GOV_PORTALS_REPO_URL="${GOV_PORTALS_REPO_URL:-https://gitea.d-bis.org/Gov_Web_Portals/gov-portals-monorepo.git}" +GOV_PORTALS_REF="${GOV_PORTALS_REF:-main}" + +VMID_GOV_PORTALS="${VMID_GOV_PORTALS:-7804}" +IP_GOV_PORTALS_DEV="${IP_GOV_PORTALS_DEV:-192.168.11.54}" +PROXMOX_HOST="${PROXMOX_HOST:-192.168.11.11}" +CT_APP_DIR="${DBIS_PORTAL_CT_DIR:-/srv/gov-portals}" +SERVICE_NAME="${DBIS_PORTAL_SERVICE:-gov-portal-DBIS}" +DBIS_PORT="${DBIS_PORT:-3001}" + +[[ -d "$PHOENIX_REPO_ROOT" ]] || die "PHOENIX_REPO_ROOT does not exist: $PHOENIX_REPO_ROOT" +[[ -n "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "PHOENIX_DEPLOY_WORKSPACE is required" +[[ -d "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "staged DBIS workspace missing: $PHOENIX_DEPLOY_WORKSPACE" +[[ "$CT_APP_DIR" != "/" ]] || die "refusing to deploy into /" + +TMP_DIR="$(mktemp -d)" +BUILD_CONTEXT="$TMP_DIR/gov-portals" +ARCHIVE="$TMP_DIR/gov-portals-dbis-live.tgz" +REMOTE_ARCHIVE="/tmp/gov-portals-dbis-live-${PHOENIX_DEPLOY_SHA:-manual}-$$.tgz" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +echo "Preparing DBIS live deploy context" +echo " DBIS source: $PHOENIX_DEPLOY_WORKSPACE" +echo " parent repo: $GOV_PORTALS_REPO_URL#$GOV_PORTALS_REF" +echo " target: CT $VMID_GOV_PORTALS ($IP_GOV_PORTALS_DEV), service $SERVICE_NAME, port $DBIS_PORT" + +git_auth_args=() +if [[ -n "${GITEA_TOKEN:-}" ]]; then + git_auth_args=(-c "http.extraHeader=Authorization: token ${GITEA_TOKEN}") +fi + +git "${git_auth_args[@]}" clone --depth 1 --branch "$GOV_PORTALS_REF" "$GOV_PORTALS_REPO_URL" "$BUILD_CONTEXT" + +rm -rf "$BUILD_CONTEXT/DBIS" +mkdir -p "$BUILD_CONTEXT/DBIS" +tar \ + --exclude=.git \ + --exclude=node_modules \ + --exclude=.next \ + --exclude='*.tsbuildinfo' \ + -C "$PHOENIX_DEPLOY_WORKSPACE" \ + -cf - . | tar -C "$BUILD_CONTEXT/DBIS" -xf - + +tar \ + --exclude=.git \ + --exclude=node_modules \ + --exclude=.next \ + --exclude='*.tsbuildinfo' \ + -C "$BUILD_CONTEXT" \ + -czf "$ARCHIVE" . + +echo "Uploading deploy archive to Proxmox host $PROXMOX_HOST" +scp -q -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "$ARCHIVE" "root@$PROXMOX_HOST:$REMOTE_ARCHIVE" + +echo "Pushing archive into CT $VMID_GOV_PORTALS" +ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "root@$PROXMOX_HOST" \ + "pct push $VMID_GOV_PORTALS '$REMOTE_ARCHIVE' '$REMOTE_ARCHIVE'" + +echo "Extracting, building, and restarting DBIS inside CT $VMID_GOV_PORTALS" +ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "root@$PROXMOX_HOST" \ + "pct exec $VMID_GOV_PORTALS -- bash -s" </dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs +fi + +if ! command -v pnpm >/dev/null 2>&1; then + npm install -g pnpm@8.15.0 +fi + +cd "\$CT_APP_DIR" +pnpm install --frozen-lockfile +pnpm --filter portal-dbis build + +cat > "/etc/systemd/system/\$SERVICE_NAME.service" </dev/null +CT_SCRIPT + +ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "root@$PROXMOX_HOST" "rm -f '$REMOTE_ARCHIVE'" >/dev/null 2>&1 || true + +echo "DBIS live deployment complete." +echo "Local origin check: http://$IP_GOV_PORTALS_DEV:$DBIS_PORT/" From f8938b2e4282acec3bb23ebc7b2e5816842b30fc Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 11:38:04 -0700 Subject: [PATCH 5/9] Point DBIS live deploy at gov portals host --- .../phoenix-deploy-dbis-portal-live-from-workspace.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh index f417bca5..190c9ee3 100755 --- a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh +++ b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh @@ -26,7 +26,7 @@ GOV_PORTALS_REF="${GOV_PORTALS_REF:-main}" VMID_GOV_PORTALS="${VMID_GOV_PORTALS:-7804}" IP_GOV_PORTALS_DEV="${IP_GOV_PORTALS_DEV:-192.168.11.54}" -PROXMOX_HOST="${PROXMOX_HOST:-192.168.11.11}" +PROXMOX_HOST="${DBIS_PORTAL_PROXMOX_HOST:-${PROXMOX_HOST_GOV_PORTALS:-192.168.11.14}}" CT_APP_DIR="${DBIS_PORTAL_CT_DIR:-/srv/gov-portals}" SERVICE_NAME="${DBIS_PORTAL_SERVICE:-gov-portal-DBIS}" DBIS_PORT="${DBIS_PORT:-3001}" From 34d378f6efe103251e0a470d6dc605b4c22f23ce Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 11:39:08 -0700 Subject: [PATCH 6/9] Resolve pnpm path in DBIS deploy --- ...nix-deploy-dbis-portal-live-from-workspace.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh index 190c9ee3..49756555 100755 --- a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh +++ b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh @@ -125,13 +125,25 @@ if ! command -v node >/dev/null 2>&1; then apt-get install -y nodejs fi +export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH" if ! command -v pnpm >/dev/null 2>&1; then npm install -g pnpm@8.15.0 + hash -r fi +PNPM_BIN="\$(command -v pnpm || true)" +if [ -z "\$PNPM_BIN" ]; then + for candidate in /usr/local/bin/pnpm /usr/bin/pnpm; do + if [ -x "\$candidate" ]; then + PNPM_BIN="\$candidate" + break + fi + done +fi +[ -n "\$PNPM_BIN" ] || { echo "pnpm is required but was not found after install" >&2; exit 1; } cd "\$CT_APP_DIR" -pnpm install --frozen-lockfile -pnpm --filter portal-dbis build +"\$PNPM_BIN" install --frozen-lockfile +"\$PNPM_BIN" --filter portal-dbis build cat > "/etc/systemd/system/\$SERVICE_NAME.service" < Date: Mon, 27 Apr 2026 11:41:42 -0700 Subject: [PATCH 7/9] Install curl for DBIS deploy smoke check --- .../phoenix-deploy-dbis-portal-live-from-workspace.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh index 49756555..1ed9f2d4 100755 --- a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh +++ b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh @@ -120,6 +120,11 @@ for env_file in .env .env.local .env.production; do done rm -rf "\$ENV_BACKUP" +if ! command -v curl >/dev/null 2>&1; then + apt-get update -qq + apt-get install -y -qq curl ca-certificates +fi + if ! command -v node >/dev/null 2>&1; then curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs From aa0457b7bf86227fb1b1099bc6403434b531bc67 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 11:44:38 -0700 Subject: [PATCH 8/9] Ensure Node 20 for DBIS deploy --- .../phoenix-deploy-dbis-portal-live-from-workspace.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh index 1ed9f2d4..b4eae43b 100755 --- a/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh +++ b/scripts/deployment/phoenix-deploy-dbis-portal-live-from-workspace.sh @@ -125,9 +125,11 @@ if ! command -v curl >/dev/null 2>&1; then apt-get install -y -qq curl ca-certificates fi -if ! command -v node >/dev/null 2>&1; then +NODE_MAJOR="\$(node -p 'process.versions.node.split(\".\")[0]' 2>/dev/null || echo 0)" +if [ "\$NODE_MAJOR" -lt 20 ]; then curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs + hash -r fi export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH" From baa28cc324d82e259a3f178ac1dd9abb567c294c Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 20:29:42 -0700 Subject: [PATCH 9/9] deployment: add NPMplus provision script for cybersecur.d-bis.org static upstream Made-with: Cursor --- .../provision-cybersecur-npmplus.sh | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 scripts/deployment/provision-cybersecur-npmplus.sh diff --git a/scripts/deployment/provision-cybersecur-npmplus.sh b/scripts/deployment/provision-cybersecur-npmplus.sh new file mode 100755 index 00000000..fbf8ec84 --- /dev/null +++ b/scripts/deployment/provision-cybersecur-npmplus.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Create NPMplus proxy host for cybersecur.d-bis.org → static upstream (default: MIM web nginx IP). +# Prerequisites: DNS A record for cybersecur.d-bis.org (Cloudflare → origin); static files on upstream (see CyberSecur-Global/deploy/). +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# shellcheck disable=1091 +source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true +_orig_npm_url="${NPM_URL:-}" +_orig_npm_email="${NPM_EMAIL:-}" +_orig_npm_password="${NPM_PASSWORD:-}" +if [[ -f "${PROJECT_ROOT}/.env" ]]; then + set +u + set -a + # shellcheck disable=1091 + source "${PROJECT_ROOT}/.env" 2>/dev/null || true + set +a + set -u + [[ -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" +fi + +NPM_URL="${NPM_URL:-https://${IP_NPMPLUS:-192.168.11.167}:81}" +NPM_EMAIL="${NPM_EMAIL:?Set NPM_EMAIL}" +NPM_PASSWORD="${NPM_PASSWORD:?Set NPM_PASSWORD}" + +DOMAIN="${CYBERSECUR_DOMAIN:-cybersecur.d-bis.org}" +FORWARD_HOST="${CYBERSECUR_FORWARD_HOST:-${IP_MIM_WEB:-192.168.11.37}}" +FORWARD_PORT="${CYBERSECUR_FORWARD_PORT:-80}" + +curl_npm() { curl -s -k -L --connect-timeout 10 --max-time "${NPM_CURL_MAX_TIME:-120}" "$@"; } + +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 || true) +if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then + echo "❌ NPM authentication failed" >&2 + exit 1 +fi + +PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" -H "Authorization: Bearer $TOKEN") +HOST_ID=$(echo "$PROXY_HOSTS_JSON" | jq -r --arg d "$DOMAIN" '.[] | select(.domain_names[]? == $d) | .id' 2>/dev/null | head -1 || true) + +if [[ -n "${HOST_ID:-}" && "$HOST_ID" != "null" ]]; then + echo "✓ Proxy host already exists: $DOMAIN (id=$HOST_ID)" + exit 0 +fi + +CREATE_PAYLOAD=$(jq -n \ + --arg domain "$DOMAIN" \ + --arg forward_host "$FORWARD_HOST" \ + --argjson forward_port "$FORWARD_PORT" \ + '{ + domain_names: [$domain], + forward_scheme: "http", + forward_host: $forward_host, + forward_port: ($forward_port | tonumber), + allow_websocket_upgrade: false, + certificate_id: null, + ssl_forced: false + }') + +RESPONSE=$(curl_npm -X POST "$NPM_URL/api/nginx/proxy-hosts" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$CREATE_PAYLOAD") + +NEW_ID=$(echo "$RESPONSE" | jq -r '.id // empty' 2>/dev/null || true) +if [[ -n "$NEW_ID" && "$NEW_ID" != "null" ]]; then + echo "✓ Created $DOMAIN → http://${FORWARD_HOST}:${FORWARD_PORT} (proxy host id=$NEW_ID)" + echo " Next: deploy static files to upstream (see CyberSecur-Global/deploy/) and request SSL in NPM or run request-npmplus-certificates.sh" +else + echo "❌ Failed: $(echo "$RESPONSE" | jq -c . 2>/dev/null || echo "$RESPONSE")" >&2 + exit 1 +fi