spb/earth-now Public License
earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.
TypeScript 93%
Shell 2.3%
SQL 1.4%
JavaScript 1.3%
Dockerfile 1.2%
CSS 0.8%
1#!/usr/bin/env bash2# earth-now.co3# Author: Simon-Pierre Boucher4# Contact: contact@spboucher.ai5# File: infra/deploy-m3u96b.sh6# Purpose: Deploy to production node m3u96b via PM2 + ngrok (node's standard pattern; Docker variant in deploy-m3u96b-docker.sh), then MANDATORY health checks78set -euo pipefail910REMOTE="M3U96b"11REMOTE_DIR="apps/earth-now"12BASE_URL="https://www.earth-now.co"13API_PORT=400014WEB_PORT=410015PROXY_PORT=80801617ROOT="$(cd "$(dirname "$0")/.." && pwd)"1819echo "==> rsync source to ${REMOTE}:~/${REMOTE_DIR}"20rsync -az --delete \21 --exclude node_modules --exclude .next --exclude dist --exclude .turbo \22 --exclude .git --exclude data --exclude .env \23 "${ROOT}/" "${REMOTE}:${REMOTE_DIR}/"2425echo "==> install + build on ${REMOTE}"26# NEXT_PUBLIC_API_URL="" is inlined at build time → client fetches stay same-origin27# (routed by infra/proxy.mjs behind the single ngrok domain).28ssh "${REMOTE}" "cd ${REMOTE_DIR} && pnpm install --frozen-lockfile=false && NEXT_PUBLIC_API_URL='' pnpm build"2930echo "==> (re)start PM2 processes"31ssh "${REMOTE}" bash -s <<EOF32set -euo pipefail33cd ${REMOTE_DIR}34pm2 delete earthnow-api earthnow-web earthnow-proxy earthnow-ngrok 2>/dev/null || true35PORT=${API_PORT} pm2 start /opt/homebrew/bin/node --name earthnow-api --interpreter none -- apps/api/dist/server.js36cd apps/web && API_URL=http://127.0.0.1:${API_PORT} pm2 start ./node_modules/.bin/next --name earthnow-web --interpreter none -- start -H 127.0.0.1 -p ${WEB_PORT} && cd ../..37PROXY_PORT=${PROXY_PORT} API_PORT=${API_PORT} WEB_PORT=${WEB_PORT} pm2 start /opt/homebrew/bin/node --name earthnow-proxy --interpreter none -- infra/proxy.mjs38pm2 start /opt/homebrew/bin/ngrok --name earthnow-ngrok --interpreter none -- http --url=www.earth-now.co ${PROXY_PORT}39pm2 save40EOF4142echo "==> health checks (deploy is NOT done until both pass)"43sleep 84445ok=046if curl -fsS --max-time 15 "${BASE_URL}/api/health" | grep -q '"status":"ok"'; then47 echo " ✓ ${BASE_URL}/api/health"48else49 echo " ✗ ${BASE_URL}/api/health FAILED"50 ok=151fi5253# /sse/health must hold an SSE connection open >= 10 s (CLAUDE.md requirement).54# curl exiting 28 (max-time reached) is the EXPECTED outcome — the stream never ends.55sse_bytes=$( (curl -sN --max-time 12 "${BASE_URL}/sse/health" || true) | head -c 200 | wc -c | tr -d ' ')56if [ "${sse_bytes}" -gt 0 ]; then57 echo " ✓ ${BASE_URL}/sse/health held an SSE stream (received ${sse_bytes} bytes over ~12 s)"58else59 echo " ✗ ${BASE_URL}/sse/health FAILED (no SSE bytes received)"60 ok=161fi6263if [ "${ok}" -ne 0 ]; then64 echo "DEPLOY FAILED — health checks did not pass."65 exit 166fi67echo "==> DEPLOY OK — ${BASE_URL} is live."68