Files
mercadodevida/project/scripts/monolith.sh

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