613 lines
23 KiB
Bash
Executable File
613 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Manage the complete MercadoDeVida monolith on one development/LAN host.
|
|
# Usage: ./scripts/monolith.sh <dev|prod> <start|restart|status|stop|logs|urls>
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
MODE="${1:-}"
|
|
ACTION="${2:-}"
|
|
RUNTIME_ROOT="${MDV_RUNTIME_DIR:-$PROJECT_DIR/.runtime}"
|
|
RUNTIME_DIR="$RUNTIME_ROOT/$MODE"
|
|
BACKEND_PORT="${BACKEND_PORT:-3000}"
|
|
ADMIN_PORT="${ADMIN_PORT:-3001}"
|
|
TPV_PORT="${TPV_PORT:-3002}"
|
|
FRONTEND_PORT="${FRONTEND_PORT:-3003}"
|
|
STOREFRONT_PORT="${STOREFRONT_PORT:-3004}"
|
|
START_TIMEOUT="${START_TIMEOUT:-90}"
|
|
WATCH_INTERVAL="${WATCH_INTERVAL:-5}"
|
|
|
|
SERVICES=(backend admin tpv frontend storefront)
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./scripts/monolith.sh <dev|prod> <command>
|
|
|
|
Commands:
|
|
start Install/build when needed, migrate, and start every service
|
|
restart Stop managed processes, then start every service
|
|
status Show PID, process state, HTTP status, and URLs
|
|
check Verify all services are responding (exit 0 if all OK)
|
|
stop Gracefully stop every managed HTTP process
|
|
logs Follow all service logs (Ctrl-C exits without stopping services)
|
|
urls Print localhost and LAN URLs
|
|
watch Auto-respawn any dead service every WATCH_INTERVAL seconds
|
|
|
|
Environment overrides:
|
|
LAN_IP, BACKEND_PORT, ADMIN_PORT, TPV_PORT, FRONTEND_PORT, STOREFRONT_PORT
|
|
MDV_RUNTIME_DIR, START_TIMEOUT, WATCH_INTERVAL
|
|
EOF
|
|
}
|
|
|
|
if [[ "$MODE" != "dev" && "$MODE" != "prod" ]]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
case "$ACTION" in
|
|
start|restart|status|check|stop|logs|urls|watch) ;;
|
|
*) usage >&2; exit 2 ;;
|
|
esac
|
|
|
|
mkdir -p "$RUNTIME_DIR"
|
|
|
|
lan_ip() {
|
|
if [[ -n "${LAN_IP:-}" ]]; then
|
|
printf '%s\n' "$LAN_IP"
|
|
return
|
|
fi
|
|
|
|
local iface ip
|
|
if command -v route >/dev/null 2>&1 && command -v ipconfig >/dev/null 2>&1; then
|
|
iface="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')"
|
|
if [[ -n "$iface" ]]; then
|
|
ip="$(ipconfig getifaddr "$iface" 2>/dev/null || true)"
|
|
if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
|
|
fi
|
|
fi
|
|
if command -v hostname >/dev/null 2>&1; then
|
|
ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
|
if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
|
|
fi
|
|
printf '127.0.0.1\n'
|
|
}
|
|
|
|
LAN_ADDRESS="$(lan_ip)"
|
|
API_PUBLIC_URL="${API_PUBLIC_URL:-http://$LAN_ADDRESS:$BACKEND_PORT}"
|
|
|
|
service_port() {
|
|
case "$1" in
|
|
backend) echo "$BACKEND_PORT" ;;
|
|
admin) echo "$ADMIN_PORT" ;;
|
|
tpv) echo "$TPV_PORT" ;;
|
|
frontend) echo "$FRONTEND_PORT" ;;
|
|
storefront) echo "$STOREFRONT_PORT" ;;
|
|
esac
|
|
}
|
|
|
|
service_path() {
|
|
case "$1" in
|
|
backend) echo "/health" ;;
|
|
*) echo "/" ;;
|
|
esac
|
|
}
|
|
|
|
service_url() {
|
|
local service="$1" host="${2:-127.0.0.1}"
|
|
printf 'http://%s:%s%s\n' "$host" "$(service_port "$service")" "$(service_path "$service")"
|
|
}
|
|
|
|
service_domain() {
|
|
# Returns the subdomain for Traefik/production access
|
|
case "$1" in
|
|
backend) echo "api-mv.rikrdo.com" ;;
|
|
admin) echo "admin-mv.rikrdo.com" ;;
|
|
tpv) echo "tpv-mv.rikrdo.com" ;;
|
|
frontend) echo "shop-mv.rikrdo.com" ;;
|
|
storefront) echo "seo-mv.rikrdo.com" ;;
|
|
esac
|
|
}
|
|
|
|
pid_file() { printf '%s/%s.pid\n' "$RUNTIME_DIR" "$1"; }
|
|
log_file() { printf '%s/%s.log\n' "$RUNTIME_DIR" "$1"; }
|
|
|
|
read_pid() {
|
|
local file
|
|
file="$(pid_file "$1")"
|
|
[[ -f "$file" ]] && tr -dc '0-9' < "$file" || true
|
|
}
|
|
|
|
is_running() {
|
|
local pid
|
|
pid="$(read_pid "$1")"
|
|
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
|
}
|
|
|
|
port_pid() {
|
|
local port="$1"
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true
|
|
fi
|
|
}
|
|
|
|
port_pids() {
|
|
local port="$1"
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | sort -u || true
|
|
fi
|
|
}
|
|
|
|
stop_pid() {
|
|
local pid="$1" label="$2" deadline
|
|
[[ -n "$pid" ]] || return 0
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
echo "[INFO] Stopping $label (PID $pid)..."
|
|
kill -TERM "$pid" 2>/dev/null || true
|
|
deadline=$((SECONDS + 15))
|
|
while kill -0 "$pid" 2>/dev/null && (( SECONDS < deadline )); do sleep 1; done
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
echo "[WARN] $label did not stop gracefully; sending KILL"
|
|
kill -KILL "$pid" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
stop_service_port_listeners() {
|
|
local service="$1" port pid found=0
|
|
port="$(service_port "$service")"
|
|
while IFS= read -r pid; do
|
|
[[ -n "$pid" ]] || continue
|
|
found=1
|
|
stop_pid "$pid" "stale $service listener on port $port"
|
|
done < <(port_pids "$port")
|
|
if [[ "$found" == 1 ]]; then
|
|
rm -f "$(pid_file "$service")"
|
|
fi
|
|
}
|
|
|
|
stop_stale_project_processes() {
|
|
local pid cmd
|
|
while IFS= read -r line; do
|
|
pid="${line%% *}"
|
|
cmd="${line#* }"
|
|
[[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]] || continue
|
|
[[ "$pid" == "$$" || "$pid" == "$BASHPID" ]] && continue
|
|
case "$cmd" in
|
|
*"$PROJECT_DIR"*"tsx"*"watch src/infrastructure/http/server.ts"*|\
|
|
*"$PROJECT_DIR"*"node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts"*|\
|
|
*"$PROJECT_DIR"*"next start"*|\
|
|
*"$PROJECT_DIR"*"node_modules/next/dist/bin/next start"*)
|
|
stop_pid "$pid" "stale project process"
|
|
;;
|
|
esac
|
|
done < <(ps -axo pid=,command= 2>/dev/null | sed 's/^ *//')
|
|
}
|
|
|
|
clear_stale_deployments() {
|
|
echo '[INFO] Clearing old service deployments from managed ports...'
|
|
stop_stale_project_processes
|
|
local service
|
|
for service in "${SERVICES[@]}"; do
|
|
stop_service_port_listeners "$service"
|
|
done
|
|
}
|
|
|
|
assert_port_available() {
|
|
local service="$1" port existing managed
|
|
port="$(service_port "$service")"
|
|
existing="$(port_pid "$port")"
|
|
managed="$(read_pid "$service")"
|
|
if [[ -n "$existing" && "$existing" != "$managed" ]]; then
|
|
echo "[FAIL] $service port $port is already used by unmanaged PID $existing" >&2
|
|
echo " Stop it explicitly before retrying; this script never broad-kills processes." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
wait_http() {
|
|
local service="$1" url deadline code
|
|
url="$(service_url "$service")"
|
|
deadline=$((SECONDS + START_TIMEOUT))
|
|
while (( SECONDS < deadline )); do
|
|
if ! is_running "$service"; then
|
|
echo "[FAIL] $service exited during startup. Last log lines:" >&2
|
|
tail -40 "$(log_file "$service")" >&2 || true
|
|
return 1
|
|
fi
|
|
code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
|
if [[ "$code" =~ ^[23] ]]; then
|
|
echo "[OK] $service ready ($code) — $url"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo "[FAIL] $service did not become healthy at $url in ${START_TIMEOUT}s" >&2
|
|
tail -40 "$(log_file "$service")" >&2 || true
|
|
return 1
|
|
}
|
|
|
|
ensure_env() {
|
|
if [[ ! -f "$PROJECT_DIR/.env" ]]; then
|
|
cat > "$PROJECT_DIR/.env" <<'EOF'
|
|
DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida
|
|
REDIS_URL=redis://localhost:6379
|
|
COOKIE_SECURE=false
|
|
EOF
|
|
echo "[INFO] Created project/.env for local development"
|
|
fi
|
|
}
|
|
|
|
start_infrastructure() {
|
|
command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; }
|
|
echo '[INFO] Starting PostgreSQL and Redis...'
|
|
|
|
# Always go through Compose instead of plain `docker start` so changes such
|
|
# as Redis AOF or explicit volume names are reconciled without losing named
|
|
# volumes. Compose recreates containers when needed but keeps
|
|
# project_mdv_pg_data/project_mdv_redis_data attached.
|
|
(cd "$PROJECT_DIR" && docker compose up -d postgres redis)
|
|
|
|
local deadline=$((SECONDS + 60))
|
|
until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do
|
|
(( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; }
|
|
sleep 1
|
|
done
|
|
until docker exec mdv-dev-redis redis-cli ping >/dev/null 2>&1; do
|
|
(( SECONDS < deadline )) || { echo '[FAIL] Redis did not become ready' >&2; exit 1; }
|
|
sleep 1
|
|
done
|
|
}
|
|
|
|
install_dependencies() {
|
|
local install_command=(npm install)
|
|
[[ "$MODE" == "prod" ]] && install_command=(npm ci)
|
|
for dir in "$PROJECT_DIR" "$PROJECT_DIR/apps/admin" "$PROJECT_DIR/apps/pos" "$PROJECT_DIR/frontend" "$PROJECT_DIR/storefront"; do
|
|
echo "[INFO] ${install_command[*]} — ${dir#$PROJECT_DIR/}"
|
|
(cd "$dir" && "${install_command[@]}")
|
|
done
|
|
}
|
|
|
|
migrate() {
|
|
echo '[INFO] Applying database migrations...'
|
|
(cd "$PROJECT_DIR" && node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations)
|
|
}
|
|
|
|
build_prod() {
|
|
echo '[INFO] Building backend...'
|
|
(cd "$PROJECT_DIR" && npm run build)
|
|
echo '[INFO] Building admin...'
|
|
(cd "$PROJECT_DIR/apps/admin" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
|
echo '[INFO] Building customer frontend...'
|
|
(cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
|
echo '[INFO] Building SEO storefront...'
|
|
(cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
|
sync_uploads
|
|
}
|
|
|
|
sync_uploads() {
|
|
# Mirror admin's public/uploads to frontend and storefront so that product
|
|
# images are reachable from any of the three apps. Also mirrors every
|
|
# generated thumbnail subdir (40/, 200/, ...) so the static cached files
|
|
# reach all three apps in lockstep.
|
|
local src="$PROJECT_DIR/apps/admin/public/uploads"
|
|
local peers=(
|
|
"$PROJECT_DIR/frontend/public/uploads"
|
|
"$PROJECT_DIR/storefront/public/uploads"
|
|
)
|
|
if [[ ! -d "$src" ]]; then
|
|
return 0
|
|
fi
|
|
for peer in "${peers[@]}"; do
|
|
mkdir -p "$peer"
|
|
find "$src" -maxdepth 2 -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
local rel
|
|
rel="${f#$src/}"
|
|
if [[ ! -f "$peer/$rel" ]]; then
|
|
mkdir -p "$(dirname "$peer/$rel")"
|
|
cp "$f" "$peer/$rel" 2>/dev/null || true
|
|
fi
|
|
done
|
|
done
|
|
# Ensure thumbnails are up to date for the current image set.
|
|
"$PROJECT_DIR/scripts/generate-thumbnails.sh" --quiet
|
|
}
|
|
|
|
spawn_service() {
|
|
local service="$1" dir log pidfile port
|
|
dir="$PROJECT_DIR"
|
|
log="$(log_file "$service")"
|
|
pidfile="$(pid_file "$service")"
|
|
port="$(service_port "$service")"
|
|
: > "$log"
|
|
|
|
case "$MODE:$service" in
|
|
dev:backend)
|
|
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
prod:backend)
|
|
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
dev:admin)
|
|
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
prod:admin)
|
|
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
dev:tpv)
|
|
(cd "$PROJECT_DIR/apps/pos" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
prod:tpv)
|
|
(cd "$PROJECT_DIR/apps/pos" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
dev:frontend)
|
|
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
prod:frontend)
|
|
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
dev:storefront)
|
|
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
prod:storefront)
|
|
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
|
|
;;
|
|
esac
|
|
sleep 1
|
|
wait_http "$service"
|
|
local listener_pid
|
|
listener_pid="$(port_pid "$port")"
|
|
if [[ -n "$listener_pid" ]]; then
|
|
echo "$listener_pid" > "$pidfile"
|
|
fi
|
|
}
|
|
|
|
# Markers exclusive to next dev (Turbopack). Any hit means the server is
|
|
# serving the dev build on a port that prod should own. Add new markers here
|
|
# if a future Next.js version uses different ones.
|
|
HMR_MARKERS=(
|
|
'/__next_hmr'
|
|
'react-refresh'
|
|
'Download the React DevTools'
|
|
'webpack-hmr'
|
|
'__webpack_require__'
|
|
'_devPagesManifest'
|
|
)
|
|
# Services powered by Next.js (backend uses Fastify, so it is excluded).
|
|
NEXT_SERVICES=(admin tpv frontend storefront)
|
|
|
|
# smoke_check_no_dev_markers
|
|
# For each Next.js service in prod mode, verify the server is NOT next dev:
|
|
# 1. Root HTML must not contain any HMR_MARKERS substring.
|
|
# 2. GET /_next/hmr must respond 404 or 426 (not 200/101/405).
|
|
# 3. Startup log must not contain "(Turbopack)".
|
|
# Returns 0 if clean, 1 if any check fails.
|
|
smoke_check_no_dev_markers() {
|
|
local service port url log body hmr_code marker failed=0
|
|
echo '[INFO] Smoke check: verifying Next.js services are NOT serving next dev (HMR-free)...'
|
|
for service in "${NEXT_SERVICES[@]}"; do
|
|
port="$(service_port "$service")"
|
|
url="$(service_url "$service")"
|
|
log="$(log_file "$service")"
|
|
|
|
# 1) HTML root must not contain dev-only markers
|
|
if body="$(curl --max-time 5 -fsS "$url" 2>/dev/null)"; then
|
|
for marker in "${HMR_MARKERS[@]}"; do
|
|
if printf '%s' "$body" | grep -qF "$marker"; then
|
|
echo "[FAIL] $service root HTML contains dev-only marker: $marker" >&2
|
|
failed=1
|
|
fi
|
|
done
|
|
else
|
|
echo "[FAIL] $service root not reachable at $url" >&2
|
|
failed=1
|
|
fi
|
|
|
|
# 2) HMR endpoint must reject (404 or 426 expected in prod)
|
|
hmr_code="$(curl --max-time 5 -sS -o /dev/null -w '%{http_code}' "$url/_next/hmr" 2>/dev/null || true)"
|
|
case "$hmr_code" in
|
|
404|426) ;;
|
|
'')
|
|
echo "[FAIL] $service /_next/hmr did not respond (curl returned empty)" >&2
|
|
failed=1
|
|
;;
|
|
*)
|
|
echo "[FAIL] $service /_next/hmr returned HTTP $hmr_code (expected 404 or 426; a dev server is likely active)" >&2
|
|
failed=1
|
|
;;
|
|
esac
|
|
|
|
# 3) Startup banner must not include "(Turbopack)"
|
|
if [[ -f "$log" ]] && head -20 "$log" 2>/dev/null | grep -qF '(Turbopack)'; then
|
|
echo "[FAIL] $service startup banner shows (Turbopack) — dev server is active" >&2
|
|
failed=1
|
|
fi
|
|
done
|
|
|
|
if (( failed )); then
|
|
echo '[FAIL] DEV MODE DETECTED on prod start. Refusing to continue.' >&2
|
|
echo ' Stop every next dev process on these ports and rerun.' >&2
|
|
echo ' See docs/HOWTO-monolith.md §3.1 for the recovery runbook.' >&2
|
|
return 1
|
|
fi
|
|
echo '[OK] All Next.js services are HMR-free (prod start confirmed).'
|
|
return 0
|
|
}
|
|
|
|
start_all() {
|
|
ensure_env
|
|
clear_stale_deployments
|
|
for service in "${SERVICES[@]}"; do
|
|
rm -f "$(pid_file "$service")"
|
|
assert_port_available "$service"
|
|
done
|
|
start_infrastructure
|
|
install_dependencies
|
|
migrate
|
|
[[ "$MODE" == "prod" ]] && build_prod
|
|
sync_uploads
|
|
for service in "${SERVICES[@]}"; do spawn_service "$service"; done
|
|
if [[ "$MODE" == "prod" ]]; then
|
|
if ! smoke_check_no_dev_markers; then
|
|
echo '[FAIL] Smoke check failed; stopping just-started services to leave a clean state.' >&2
|
|
stop_all
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo
|
|
print_urls
|
|
}
|
|
|
|
stop_service() {
|
|
local service="$1" pid
|
|
pid="$(read_pid "$service")"
|
|
stop_pid "$pid" "$service"
|
|
rm -f "$(pid_file "$service")"
|
|
}
|
|
|
|
stop_all() {
|
|
local i
|
|
for ((i=${#SERVICES[@]}-1; i>=0; i--)); do stop_service "${SERVICES[$i]}"; done
|
|
}
|
|
|
|
status_all() {
|
|
ensure_alive silent
|
|
local service pid state code url lan_url failed=0
|
|
printf '%-11s %-8s %-10s %-6s %s\n' SERVICE PID PROCESS HTTP URL
|
|
for service in "${SERVICES[@]}"; do
|
|
pid="$(read_pid "$service")"
|
|
state='stopped'
|
|
[[ -n "$pid" ]] || pid='-'
|
|
if [[ "$pid" != '-' ]] && kill -0 "$pid" 2>/dev/null; then state='running'; fi
|
|
url="$(service_url "$service")"
|
|
code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
|
[[ -n "$code" && "$code" != '000' ]] || code='-'
|
|
lan_url="$(service_url "$service" "$LAN_ADDRESS")"
|
|
printf '%-11s %-8s %-10s %-6s %s\n' "$service" "$pid" "$state" "$code" "$lan_url"
|
|
[[ "$state" == 'running' && "$code" =~ ^[23] ]] || failed=1
|
|
done
|
|
echo
|
|
(cd "$PROJECT_DIR" && docker compose ps) || true
|
|
return "$failed"
|
|
}
|
|
|
|
# ensure_alive [silent|verbose]
|
|
# Re-spawn any service whose tracked PID is not running. Used by `status`
|
|
# so an operator who runs `monolith.sh status` always sees live processes
|
|
# even after a backend crash with no error trace.
|
|
ensure_alive() {
|
|
local verbose="${1:-silent}" service pid
|
|
[[ "$verbose" == "verbose" ]] || verbose='silent'
|
|
for service in "${SERVICES[@]}"; do
|
|
pid="$(read_pid "$service")"
|
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
if [[ "$verbose" == "verbose" ]]; then
|
|
echo "[WATCHDOG] $service not running (last PID: ${pid:-none}); respawning..."
|
|
fi
|
|
rm -f "$(pid_file "$service")"
|
|
# Only respawn if infrastructure is reachable; otherwise let `start` handle it.
|
|
if (cd "$PROJECT_DIR" && docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^(mdv-dev-postgres|mdv-dev-redis)$'); then
|
|
spawn_service "$service" || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
watch_loop() {
|
|
echo "[WATCHDOG] watching services every ${WATCH_INTERVAL}s (Ctrl+C to stop)"
|
|
ensure_alive verbose
|
|
while true; do
|
|
sleep "$WATCH_INTERVAL"
|
|
ensure_alive verbose
|
|
done
|
|
}
|
|
|
|
check_services() {
|
|
# Verify all services are responding with HTTP 2xx or 3xx
|
|
# Exit 0 if all OK, exit 1 if any service is down
|
|
local service url code failed=0
|
|
echo "Checking all services..."
|
|
for service in "${SERVICES[@]}"; do
|
|
url="$(service_url "$service")"
|
|
code="$(curl --max-time 5 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
|
if [[ "$code" =~ ^[23] ]]; then
|
|
echo "[OK] $service ($code) — $url"
|
|
else
|
|
echo "[FAIL] $service (HTTP $code) — $url"
|
|
failed=1
|
|
fi
|
|
done
|
|
echo
|
|
if [[ $failed -eq 0 ]]; then
|
|
echo "All services are up!"
|
|
return 0
|
|
else
|
|
echo "Some services are down."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
print_urls() {
|
|
# Detect if Traefik/production domains are reachable
|
|
local api_domain="$(service_domain backend)"
|
|
local admin_domain="$(service_domain admin)"
|
|
local tpv_domain="$(service_domain tpv)"
|
|
local shop_domain="$(service_domain frontend)"
|
|
local seo_domain="$(service_domain storefront)"
|
|
|
|
cat <<EOF
|
|
Mode: $MODE
|
|
LAN IP: $LAN_ADDRESS
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
PORTS (local/dev)
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Service │ Port │ Local URL │
|
|
├─────────────────────────────────────────────────────────────────────┤
|
|
│ Backend (API) │ $BACKEND_PORT │ http://127.0.0.1:$BACKEND_PORT/ │
|
|
│ Admin │ $ADMIN_PORT │ http://127.0.0.1:$ADMIN_PORT/ │
|
|
│ POS (TPV) │ $TPV_PORT │ http://127.0.0.1:$TPV_PORT/ │
|
|
│ Frontend │ $FRONTEND_PORT │ http://127.0.0.1:$FRONTEND_PORT/ │
|
|
│ Storefront │ $STOREFRONT_PORT │ http://127.0.0.1:$STOREFRONT_PORT/ │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
DOMAINS (Traefik/production)
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Service │ Domain │ Auth │ Port │
|
|
├─────────────────────────────────────────────────────────────────────┤
|
|
│ Backend (API) │ https://$api_domain │ Authelia │ $BACKEND_PORT │
|
|
│ Admin │ https://$admin_domain │ Authelia │ $ADMIN_PORT │
|
|
│ POS (TPV) │ https://$tpv_domain │ Authelia │ $TPV_PORT │
|
|
│ Frontend │ https://$shop_domain │ Public │ $FRONTEND_PORT │
|
|
│ Storefront │ https://$seo_domain │ Public │ $STOREFRONT_PORT │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
|
|
Backend health: https://$api_domain/health
|
|
Swagger docs: https://$api_domain/docs
|
|
EOF
|
|
}
|
|
|
|
follow_logs() {
|
|
local files=() service
|
|
for service in "${SERVICES[@]}"; do
|
|
touch "$(log_file "$service")"
|
|
files+=("$(log_file "$service")")
|
|
done
|
|
tail -n 100 -F "${files[@]}"
|
|
}
|
|
|
|
case "$ACTION" in
|
|
start) start_all ;;
|
|
restart) stop_all; start_all ;;
|
|
status) status_all ;;
|
|
check) check_services ;;
|
|
stop) stop_all ;;
|
|
logs) follow_logs ;;
|
|
urls) print_urls ;;
|
|
watch) watch_loop ;;
|
|
esac
|