#!/bin/bash set -euo pipefail # ----------------------------- # CONFIG # ----------------------------- NODE_VERSION="v22.14.0" NVM_DIR="/root/.nvm" APP_NAME="erpfrontend" # PM2 process name used in ecosystem.cjs MEDIASERVER_NAME="mediaserver" # PM2 process name for media server SRC_DIR="/root/test/workspace/erp-frontend-pipeline-production" STAGE_DIR="/opt/erp-nuxt-frontend-temp" LIVE_DIR="/opt/erp-nuxt-frontend" ROLLBACK_DIR="/opt/erp-nuxt-frontend-rollback" MEDIASERVER_DIR="/opt/mediaserver" DB_FILE="push-subscriptions.db" # Every SQLite DB under data/ that must survive a deploy. data/*.db is gitignored, # so it is NOT in the build artifact — anything missing here is recreated empty # on the next deploy (this is what silently wiped chat.db before). # amazon-ads.db = Amazon invoices page: uploaded-but-not-imported PDFs + PDF cache index (booked # invoices are listed from iDempiere, so losing it only drops PENDING uploads — it was wiped by # every deploy until 2026-09-18 because it was missing here). DB_FILES=("push-subscriptions.db" "chat.db" "email-verify.db" "fcm-tokens.db" "legal-docs.db" "contract-signing.db" "amazon-ads.db") # Other frontend-owned runtime data under data/ that must survive too (all gitignored): # - contract-signing/ PDF packages of digital signing links (one dir per token) — # the signing portal serves these; losing them breaks open links # - ebay-signing-keys.json eBay digital-signature key pair # mobile-releases = LogShip Mobile APK channel (server/utils/mobileReleases.ts) — published at # runtime, NOT part of the build; dropping it here would wipe every APK on the next deploy. # - amazon-ads/ cached original PDFs of the Amazon invoices page (pairs with amazon-ads.db) DATA_EXTRA=("contract-signing" "ebay-signing-keys.json" "mobile-releases" "amazon-ads") DB_LIVE_DIR="$LIVE_DIR/data" DB_BACKUP_DIR="/opt/erp-nuxt-frontend-db-backups" ENV_SRC="$SRC_DIR/.env-prod" ENV_DST="$STAGE_DIR/.env" # ----------------------------- # NVM + NODE 22 # ----------------------------- export NVM_DIR="$NVM_DIR" # shellcheck disable=SC1090 [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvm install "$NODE_VERSION" nvm alias default "$NODE_VERSION" nvm use "$NODE_VERSION" # Ensure pm2 is installed for THIS Node version npm install -g pm2 NODE_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/node" NPM_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/npm" PM2_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/pm2" echo "Using Node: $("$NODE_BIN" -v)" echo "ABI: $("$NODE_BIN" -p 'process.versions.modules')" echo "PM2: $("$PM2_BIN" -v)" # ----------------------------- # STAGE WORKSPACE # ----------------------------- mkdir -p "$STAGE_DIR" rm -rf "$STAGE_DIR"/* cp -r "$SRC_DIR"/* "$STAGE_DIR"/ cp "$ENV_SRC" "$ENV_DST" # Inject build metadata cd "$SRC_DIR" git_hash=$(git rev-parse --short HEAD || echo "unknown") current_datetime=$(date '+%d.%m.%Y %H:%M') cd "$STAGE_DIR" sed -i "s//$git_hash/g" "$ENV_DST" sed -i "s//$current_datetime/g" "$ENV_DST" export NODE_OPTIONS="--max-old-space-size=12288" # Clean install + build under Node 22. # npm ci installs EXACTLY the committed package-lock.json — no re-resolution # against the live registry (a lockfile-less `npm install` here let a fresh # @vitejs/devtools publish crash npm's resolver on 2026-07-25). rm -rf node_modules .nuxt .output $NPM_BIN ci $NPM_BIN run build # ----------------------------- # VERSION SERVICE WORKER CACHE # ----------------------------- echo "Versioning service worker cache with git hash: $git_hash" if [ -f "$STAGE_DIR/.output/public/sw-notifications.js" ]; then sed -i "s/const CACHE_NAME = 'logship-erp-v1'/const CACHE_NAME = 'logship-erp-$git_hash'/" \ "$STAGE_DIR/.output/public/sw-notifications.js" echo "Service worker cache versioned: logship-erp-$git_hash" else echo "Warning: Service worker file not found at expected location" fi # ----------------------------- # BACKUP SQLITE DBs # ----------------------------- echo "Backing up SQLite databases..." mkdir -p "$DB_BACKUP_DIR" backup_ts="$(date +%Y%m%d-%H%M%S)" for db in "${DB_FILES[@]}"; do if [ -f "$LIVE_DIR/data/$db" ]; then base="${db%.db}" cp "$LIVE_DIR/data/$db" "$DB_BACKUP_DIR/${base}-${backup_ts}.db" echo "Database backed up: $DB_BACKUP_DIR/${base}-${backup_ts}.db" # keep last 10 backups per database (cd "$DB_BACKUP_DIR" && ls -t "${base}-"*.db 2>/dev/null | tail -n +11 | xargs -r rm) else echo "No existing $db found - fresh deployment for it" fi done # ----------------------------- # DEPLOY (with rollback) # ----------------------------- rm -rf "$ROLLBACK_DIR" if [ -d "$LIVE_DIR" ]; then cp -rf "$LIVE_DIR" "$ROLLBACK_DIR" fi rm -rf "$LIVE_DIR" mkdir -p "$LIVE_DIR" cp -r "$STAGE_DIR"/. "$LIVE_DIR"/ # ----------------------------- # RESTORE SQLITE DBs # ----------------------------- echo "Restoring SQLite databases..." mkdir -p "$DB_LIVE_DIR" for db in "${DB_FILES[@]}"; do if [ -f "$ROLLBACK_DIR/data/$db" ]; then cp "$ROLLBACK_DIR/data/$db" "$DB_LIVE_DIR"/ # Carry over WAL sidecars too: chat.db runs in WAL mode, so the most recent # messages may still live in -wal and not yet be checkpointed into the .db. [ -f "$ROLLBACK_DIR/data/$db-wal" ] && cp "$ROLLBACK_DIR/data/$db-wal" "$DB_LIVE_DIR"/ || true [ -f "$ROLLBACK_DIR/data/$db-shm" ] && cp "$ROLLBACK_DIR/data/$db-shm" "$DB_LIVE_DIR"/ || true echo "Database $db restored from rollback" else echo "No previous $db found - will be created on first run" fi done # Extra data files/dirs (see DATA_EXTRA above) for item in "${DATA_EXTRA[@]}"; do if [ -e "$ROLLBACK_DIR/data/$item" ]; then cp -rf "$ROLLBACK_DIR/data/$item" "$DB_LIVE_DIR"/ echo "Data item $item restored from rollback" else echo "No previous data/$item found - will be created on first use" fi done chown -R root:root "$DB_LIVE_DIR" chmod 755 "$DB_LIVE_DIR" for db in "${DB_FILES[@]}"; do for f in "$db" "$db-wal" "$db-shm"; do [ -f "$DB_LIVE_DIR/$f" ] && chmod 644 "$DB_LIVE_DIR/$f" || true done done # ----------------------------- # REBUILD NATIVE MODULES FOR NODE 22 # ----------------------------- cd "$LIVE_DIR/.output/server" echo "Rebuilding better-sqlite3 for Node $NODE_VERSION..." $NPM_BIN rebuild better-sqlite3 # ----------------------------- # COPY RUNTIME ASSETS NITRO DOES NOT BUNDLE # Nitro externalizes tesseract.js-core / pdfjs-dist but traces only their JS # entry — NOT tesseract's *.wasm or pdfjs' worker. The incoming-invoice OCR # pipeline (Eingangsrechnungen) needs them at runtime, so copy them from the # real node_modules into .output/server/node_modules. Idempotent; safe to re-run. # ----------------------------- echo "Copying OCR/PDF runtime assets into .output/server/node_modules..." SERVER_NM="$LIVE_DIR/.output/server/node_modules" SRC_NM="$LIVE_DIR/node_modules" # tesseract.js-core: the *.wasm core files (+ JS wrappers) for OCR (scanned PDFs) if [ -d "$SRC_NM/tesseract.js-core" ]; then mkdir -p "$SERVER_NM/tesseract.js-core" cp -rf "$SRC_NM/tesseract.js-core/." "$SERVER_NM/tesseract.js-core/" echo " ✓ tesseract.js-core (wasm)" else echo " ⚠ tesseract.js-core not found in node_modules" fi # pdfjs-dist: the Node 'fake worker' (pdf.worker.mjs) must sit beside pdf.mjs, # else Tier-1 text extraction throws and every PDF falls through to OCR. if [ -d "$SRC_NM/pdfjs-dist/legacy/build" ]; then mkdir -p "$SERVER_NM/pdfjs-dist/legacy/build" cp -f "$SRC_NM/pdfjs-dist/legacy/build/"pdf*.mjs "$SERVER_NM/pdfjs-dist/legacy/build/" 2>/dev/null || true echo " ✓ pdfjs-dist worker" else echo " ⚠ pdfjs-dist/legacy/build not found in node_modules" fi # canvas: native; ONLY the Tier-2 rasterize fallback used when pdftoppm (poppler) # is absent on the host. Preferred path is poppler-utils — install that instead # of relying on this copy. Copied best-effort only if present and not already there. if [ -d "$SRC_NM/canvas" ] && [ ! -e "$SERVER_NM/canvas/build/Release/canvas.node" ]; then mkdir -p "$SERVER_NM/canvas" cp -rf "$SRC_NM/canvas/." "$SERVER_NM/canvas/" 2>/dev/null || true echo " ✓ canvas (rasterize fallback)" fi # Sanity check (non-fatal): confirm the critical assets landed ls "$SERVER_NM/tesseract.js-core/"*.wasm >/dev/null 2>&1 && echo " verified: tesseract wasm present" || echo " ⚠ tesseract wasm still missing" [ -f "$SERVER_NM/pdfjs-dist/legacy/build/pdf.worker.mjs" ] && echo " verified: pdfjs worker present" || echo " ⚠ pdfjs worker still missing" # ----------------------------- # START WITH PM2 (force Node 20) # ----------------------------- cd "$LIVE_DIR" # Stop old ERP frontend process if exists $PM2_BIN delete "$APP_NAME" || true # Start ERP frontend using Node 22 interpreter $PM2_BIN start ecosystem.config.cjs \ --only "$APP_NAME" \ --update-env \ --interpreter "$NODE_BIN" # Start or restart mediaserver (ensure it's running with correct config) cd "$MEDIASERVER_DIR" if $PM2_BIN describe "$MEDIASERVER_NAME" > /dev/null 2>&1; then echo "Restarting existing mediaserver..." $PM2_BIN restart "$MEDIASERVER_NAME" --update-env --interpreter "$NODE_BIN" else echo "Starting mediaserver for the first time..." $PM2_BIN start ecosystem.config.js --update-env --interpreter "$NODE_BIN" fi # Save PM2 state (both apps) $PM2_BIN save --force # Ensure PM2 resurrects at boot WITH the right PATH/NVM $PM2_BIN startup systemd -u root --hp /root # Create a systemd drop-in so PM2 finds Node on boot AND starts only after the # iDempiere REST API (8443) answers — without the wait, every reboot produces a # ~2 minute 502 storm in the frontend until the API is up. # Keep this block in sync with deploy/pm2-root.service.d/override.conf. mkdir -p /etc/systemd/system/pm2-root.service.d cat >/etc/systemd/system/pm2-root.service.d/override.conf <