[object Object]

← back to Ga Allsites

Add gate2_inject.sh + gate2_rollback.sh for GA4 fleet injection

6630fbffe29698c1c24bb35a60ce076f3ea8d7a1 · 2026-08-22 10:54:44 -0700 · Steve Abrams

Idempotent injection of G-J2SL8P5KDL into 17 target files (30 vendor
shared template + 16 standalones). Skips any file already containing
the ID. Timestamps .bak before every write; rollback restores all .bak
files and reloads affected pm2 processes.

Dry-run: all 17 files already contain ID (SKIP on first run).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 6630fbffe29698c1c24bb35a60ce076f3ea8d7a1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 22 10:54:44 2026 -0700

    Add gate2_inject.sh + gate2_rollback.sh for GA4 fleet injection
    
    Idempotent injection of G-J2SL8P5KDL into 17 target files (30 vendor
    shared template + 16 standalones). Skips any file already containing
    the ID. Timestamps .bak before every write; rollback restores all .bak
    files and reloads affected pm2 processes.
    
    Dry-run: all 17 files already contain ID (SKIP on first run).
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 gate2_inject.sh   | 368 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 gate2_rollback.sh | 146 ++++++++++++++++++++++
 2 files changed, 514 insertions(+)

diff --git a/gate2_inject.sh b/gate2_inject.sh
new file mode 100755
index 0000000..39b7c6e
--- /dev/null
+++ b/gate2_inject.sh
@@ -0,0 +1,368 @@
+#!/usr/bin/env bash
+# gate2_inject.sh — GATE-2 GA4 injection for the 34-site DW fleet microsite fleet
+# Measurement ID: G-J2SL8P5KDL  (property "DW Fleet Microsites" / 551081041)
+#
+# IDEMPOTENT: any file already containing G-J2SL8P5KDL is skipped unchanged.
+# REVERSIBLE: timestamped .bak files written before every edit; gate2_rollback.sh restores them.
+# RUNS ON KAMATERA: ship this entire heredoc over SSH.
+#
+# Deploy command (Steve pastes):
+#   ! ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_inject.sh
+#
+# Rollback command:
+#   ! ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_rollback.sh
+# ---------------------------------------------------------------------------
+set -euo pipefail
+
+ID="G-J2SL8P5KDL"
+
+ssh root@45.61.58.125 "ID=$ID" 'bash -s' <<'REMOTE'
+set -uo pipefail
+ID="${ID:-G-J2SL8P5KDL}"
+TS=$(date +%Y%m%d-%H%M%S)
+
+# ────────────────────────────────────────────────────────────────────────────
+# Counters
+N_INJECTED=0; N_SKIPPED=0; N_FAILED=0; N_MANUAL=0
+declare -A RESULT                 # file -> status string
+declare -A BAK_MAP                # file -> .bak path (written to /tmp/gate2-bak-manifest.txt at end)
+
+# ────────────────────────────────────────────────────────────────────────────
+# STANDALONE TARGETS  (file path | vendor slug | pm2 process name)
+# Each gets:  async gtag loader + gtag('js') + gtag('config', ID, {vendor:'<slug>'})
+# inserted as the very first child of <head>.
+STANDALONE_RAW=(
+  "/root/Projects/artmura-site/public/index.html|artmura|artmura-site"
+  "/root/Projects/carnegie-site/public/index.html|carnegie|carnegie-site"
+  "/root/Projects/thibaut-site/public/index.html|thibaut|thibaut-site"
+  "/root/Projects/elitis-site/public/index.html|elitis|elitis-site"
+  "/root/Projects/romo-site/public/index.html|romo|romo-site"
+  "/root/Projects/la-walls-site/public/index.html|la-walls|la-walls-site"
+  "/root/Projects/british-walls-site/public/index.html|british-walls|british-walls-site"
+  "/root/Projects/leed-walls-site/public/index.html|leed-walls|leed-walls-site"
+  "/root/Projects/surface-stick-site/public/index.html|surface-stick|surface-stick-site"
+  "/root/Projects/coleandson-site/public/index.html|cole-and-son|coleandson-site"
+  "/root/Projects/fallingstar-site/public/index.html|fallingstar|fallingstar-site"
+  "/root/Projects/philipjeffries-site/public/index.html|phillip-jeffries|philipjeffries-site"
+  "/root/Projects/primo-leathers-site/public/index.html|primo-leathers|primo-leathers-site"
+  "/root/Projects/quadrille-house-site/public/index.html|quadrille|quadrille-house-site"
+  "/var/www/fabricut.designerwallcoverings.com/public/index.html|fabricut|fabricut-landing"
+  "/root/Projects/fentucci-site/public/index.html|fentucci|fentucci-site"
+)
+
+# SHARED TEMPLATE (30-vendor wolfgordon binary)
+# Gets:  async loader + gtag('js') in <head>,
+#        gtag('config', ID, {vendor:cfg.vendor}) inserted INSIDE the async IIFE
+#        on the line immediately after applyConfig(cfg);
+SHARED_FILE="/root/Projects/wolfgordon-site/public/index.html"
+# All 30 pm2 processes that run wolfgordon-site/server.js — reload all of them.
+WOLF_PM2_NAMES=(
+  anna-french-site brunschwig-fils-site coordonne-site designers-guild-site
+  designtex-site gp-j-baker-site groundworks-site harlequin-site knoll-site
+  koroseal-site kravet-site lee-jofa-site maharam-site maya-romanoff-site
+  mulberry-site nina-campbell-site nobilis-site osborne-little-site
+  phillip-jeffries-site pierre-frey-site ralph-lauren-site rebel-walls-site
+  sancar-site sandberg-site scalamandre-site threads-site william-morris-site
+  wolf-gordon-site wolfgordon-site zoffany-site
+)
+
+# FENTUCCI-WEBSITE (old binary with commented-out placeholder)
+# Gets:  commented placeholder uncommented + ID set to G-J2SL8P5KDL.
+# The real loader + config are inserted the same way as standalones (head injection),
+# but we ALSO remove the now-redundant commented placeholder if both coexist.
+FENTUCCI_OLD_FILE="/root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html"
+FENTUCCI_OLD_PM2="fentucci-website"
+
+# ────────────────────────────────────────────────────────────────────────────
+# Helper: build the head snippet for a static vendor slug
+head_snippet_static() {
+  local vendor="$1"
+  printf '<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());gtag('\''config'\'','\''%s'\'',{vendor:'\''%s'\''});</script>' \
+    "$ID" "$ID" "$vendor"
+}
+
+# Helper: build the head snippet for the shared template (no config here; fires in IIFE)
+head_snippet_shared() {
+  printf '<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());</script>' \
+    "$ID"
+}
+
+# ────────────────────────────────────────────────────────────────────────────
+# inject_standalone FILE VENDOR PM2_NAME
+inject_standalone() {
+  local file="$1" vendor="$2" pm2_name="$3"
+  local label="$vendor"
+
+  if [ ! -f "$file" ]; then
+    RESULT["$label"]="MISSING — file not found: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="SKIP — already contains $ID"
+    (( N_SKIPPED++ )) || true; return
+  fi
+
+  if ! grep -qiE '</head>' "$file"; then
+    RESULT["$label"]="NEEDS-MANUAL — no </head> anchor found: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  local bak="${file}.bak-ga-${TS}"
+  cp -a "$file" "$bak"
+  BAK_MAP["$file"]="$bak"
+
+  local snippet
+  snippet="$(head_snippet_static "$vendor")"
+
+  # Insert the snippet immediately before </head>
+  perl -0777 -i -pe "s{(</head>)}{${snippet}\n\$1}i" "$file"
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="INJECTED — $file [pm2: $pm2_name]"
+    (( N_INJECTED++ )) || true
+    # Reload pm2 process
+    if pm2 reload "$pm2_name" >/dev/null 2>&1; then
+      RESULT["$label"]+=" | pm2 reload: OK"
+    else
+      RESULT["$label"]+=" | pm2 reload: WARN (try restart manually)"
+    fi
+  else
+    # Revert
+    cp -a "$bak" "$file"
+    RESULT["$label"]="FAIL — perl injection failed, reverted from bak: $file"
+    (( N_FAILED++ )) || true
+  fi
+}
+
+# ────────────────────────────────────────────────────────────────────────────
+# inject_shared — wolfgordon-site/public/index.html
+inject_shared() {
+  local file="$SHARED_FILE"
+  local label="wolfgordon-shared-template"
+
+  if [ ! -f "$file" ]; then
+    RESULT["$label"]="MISSING — file not found: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="SKIP — already contains $ID"
+    (( N_SKIPPED++ )) || true; return
+  fi
+
+  # Both anchors must be present
+  if ! grep -qiE '</head>' "$file"; then
+    RESULT["$label"]="NEEDS-MANUAL — no </head> anchor: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+  if ! grep -qF 'applyConfig(cfg);' "$file"; then
+    RESULT["$label"]="NEEDS-MANUAL — no 'applyConfig(cfg);' anchor in IIFE: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  local bak="${file}.bak-ga-${TS}"
+  cp -a "$file" "$bak"
+  BAK_MAP["$file"]="$bak"
+
+  local head_snip
+  head_snip="$(head_snippet_shared)"
+
+  # 1. Insert loader + gtag('js') before </head>
+  perl -0777 -i -pe "s{(</head>)}{${head_snip}\n\$1}i" "$file"
+
+  # 2. Insert gtag('config', ID, {vendor:cfg.vendor}) immediately after applyConfig(cfg);
+  #    (single-line match; the line has 6 leading spaces in the current file)
+  perl -0777 -i -pe "s{(applyConfig\(cfg\);)(\s*\n)}{\$1\$2      gtag('config','${ID}',{vendor:cfg.vendor});\n}" "$file"
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="INJECTED — $file [30 pm2 processes]"
+    (( N_INJECTED++ )) || true
+    # Reload all 30 processes on this binary
+    local reload_ok=0 reload_fail=0
+    for pname in "${WOLF_PM2_NAMES[@]}"; do
+      if pm2 reload "$pname" >/dev/null 2>&1; then
+        (( reload_ok++ )) || true
+      else
+        (( reload_fail++ )) || true
+      fi
+    done
+    RESULT["$label"]+=" | pm2 reload: ${reload_ok}/${#WOLF_PM2_NAMES[@]} OK"
+    [ "$reload_fail" -gt 0 ] && RESULT["$label"]+=" (${reload_fail} failed — check pm2 log)"
+  else
+    cp -a "$bak" "$file"
+    RESULT["$label"]="FAIL — injection failed, reverted: $file"
+    (( N_FAILED++ )) || true
+  fi
+}
+
+# ────────────────────────────────────────────────────────────────────────────
+# inject_fentucci_old — /root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html
+# Strategy: the file has a commented-out placeholder:
+#   <!-- <script async src="...G-XXXXXXXXXX"></script> -->
+# Uncomment it and set the ID. Then also ensure the config call is present.
+# If the file already has G-J2SL8P5KDL anywhere, skip.
+inject_fentucci_old() {
+  local file="$FENTUCCI_OLD_FILE"
+  local label="fentucci-website-OLD-binary"
+
+  if [ ! -f "$file" ]; then
+    RESULT["$label"]="MISSING — file not found: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="SKIP — already contains $ID"
+    (( N_SKIPPED++ )) || true; return
+  fi
+
+  if ! grep -qiE '</head>' "$file"; then
+    RESULT["$label"]="NEEDS-MANUAL — no </head> anchor: $file"
+    (( N_MANUAL++ )) || true; return
+  fi
+
+  local bak="${file}.bak-ga-${TS}"
+  cp -a "$file" "$bak"
+  BAK_MAP["$file"]="$bak"
+
+  # Try to uncomment the placeholder line and set ID
+  # Pattern: <!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script> -->
+  local placeholder_found=0
+  if grep -q 'GA4 placeholder\|gtag/js.*G-' "$file"; then
+    # Uncomment the script tag and set the correct ID
+    perl -i -pe 's{<!--\s*(<script async src="https://www\.googletagmanager\.com/gtag/js\?id=)[^"]*(">[^<]*</script>)\s*-->}{\1'"$ID"'\2}' "$file"
+    placeholder_found=1
+  fi
+
+  # Whether or not we uncommented a placeholder, we need the full bootstrap in <head>.
+  # Insert before </head> if the ID isn't present yet (the uncommented placeholder is only the loader tag).
+  if ! grep -qF "$ID" "$file"; then
+    # placeholder uncomment didn't land the ID — inject from scratch
+    local snippet
+    snippet="$(head_snippet_static "fentucci")"
+    perl -0777 -i -pe "s{(</head>)}{${snippet}\n\$1}i" "$file"
+  else
+    # We got the loader via placeholder uncomment but still need the config call + init script.
+    # Insert a config+init block right after the uncommented loader line.
+    perl -i -pe 's{(<script async src="https://www\.googletagmanager\.com/gtag/js\?id='"$ID"'"></script>)}{$1\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());gtag('\''config'\'','\'''"$ID"''\'',{vendor:'\''fentucci'\''});</script>}' "$file" || true
+  fi
+
+  if grep -qF "$ID" "$file"; then
+    RESULT["$label"]="INJECTED — $file [pm2: $FENTUCCI_OLD_PM2]"
+    (( N_INJECTED++ )) || true
+    if pm2 reload "$FENTUCCI_OLD_PM2" >/dev/null 2>&1; then
+      RESULT["$label"]+=" | pm2 reload: OK"
+    else
+      RESULT["$label"]+=" | pm2 reload: WARN"
+    fi
+  else
+    cp -a "$bak" "$file"
+    RESULT["$label"]="FAIL — injection failed, reverted: $file"
+    (( N_FAILED++ )) || true
+  fi
+}
+
+# ────────────────────────────────────────────────────────────────────────────
+# EXECUTE
+
+echo ""
+echo "========================================================"
+echo " GATE-2 GA4 INJECT  |  ID: $ID"
+echo " Timestamp: $TS"
+echo "========================================================"
+echo ""
+
+echo "[1/3] Shared wolfgordon template (30 vendors)..."
+inject_shared
+
+echo "[2/3] Standalone sites (16 files)..."
+for entry in "${STANDALONE_RAW[@]}"; do
+  IFS='|' read -r fpath vendor pm2name <<< "$entry"
+  inject_standalone "$fpath" "$vendor" "$pm2name"
+done
+
+echo "[3/3] Fentucci-website old binary (placeholder uncomment)..."
+inject_fentucci_old
+
+# ────────────────────────────────────────────────────────────────────────────
+# Write bak manifest so rollback can find all .bak files for this run
+echo "" > /tmp/gate2-bak-manifest-${TS}.txt
+for orig in "${!BAK_MAP[@]}"; do
+  echo "${orig}|${BAK_MAP[$orig]}" >> /tmp/gate2-bak-manifest-${TS}.txt
+done
+echo "(bak manifest: /tmp/gate2-bak-manifest-${TS}.txt)"
+
+# ────────────────────────────────────────────────────────────────────────────
+# VERIFY — curl each site, grep for the ID in served HTML
+echo ""
+echo "========================================================"
+echo " VERIFY"
+echo "========================================================"
+
+# Map vendor slug -> live URL for curl verification
+declare -A VERIFY_URL
+VERIFY_URL["artmura"]="http://127.0.0.1:$(pm2 jlist 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);[print(p['pm2_env']['env'].get('PORT',3000)) for p in d if p['name']=='artmura-site']" 2>/dev/null | head -1 || echo 3000)"
+# Simpler: pull port from pm2 env for each site
+get_port() {
+  local pm2name="$1"
+  pm2 jlist 2>/dev/null | python3 -c "
+import json,sys
+try:
+  d=json.load(sys.stdin)
+  for p in d:
+    if p.get('name')=='$pm2name':
+      env=p.get('pm2_env',{}).get('env',{})
+      port=env.get('PORT') or env.get('port') or 3000
+      print(port)
+      break
+except:
+  print(3000)
+" 2>/dev/null | head -1
+}
+
+# Build verify list: "label|url"
+VERIFY_LIST=()
+for entry in "${STANDALONE_RAW[@]}"; do
+  IFS='|' read -r fpath vendor pm2name <<< "$entry"
+  port=$(get_port "$pm2name")
+  VERIFY_LIST+=("$vendor|http://127.0.0.1:${port}/")
+done
+# wolfgordon shared: pick wolf-gordon-site process
+wolf_port=$(get_port "wolf-gordon-site")
+VERIFY_LIST+=("wolfgordon-shared|http://127.0.0.1:${wolf_port}/")
+# fentucci-website old binary
+fwo_port=$(get_port "$FENTUCCI_OLD_PM2")
+VERIFY_LIST+=("fentucci-website-OLD|http://127.0.0.1:${fwo_port}/")
+
+N_VERIFY_PASS=0; N_VERIFY_FAIL=0
+for entry in "${VERIFY_LIST[@]}"; do
+  IFS='|' read -r label url <<< "$entry"
+  html=$(curl -sf --max-time 6 --retry 2 "$url" 2>/dev/null || true)
+  if echo "$html" | grep -qF "$ID"; then
+    printf "  VERIFY PASS   %-30s  %s\n" "$label" "$url"
+    (( N_VERIFY_PASS++ )) || true
+  else
+    printf "  VERIFY FAIL   %-30s  %s\n" "$label" "$url"
+    (( N_VERIFY_FAIL++ )) || true
+  fi
+done
+
+# ────────────────────────────────────────────────────────────────────────────
+echo ""
+echo "========================================================"
+echo " SUMMARY"
+echo "========================================================"
+for label in $(echo "${!RESULT[@]}" | tr ' ' '\n' | sort); do
+  printf "  %-40s  %s\n" "$label" "${RESULT[$label]}"
+done
+echo ""
+echo "  Injected : $N_INJECTED"
+echo "  Skipped  : $N_SKIPPED"
+echo "  Failed   : $N_FAILED"
+echo "  Manual   : $N_MANUAL"
+echo "  Verify   : ${N_VERIFY_PASS} PASS / ${N_VERIFY_FAIL} FAIL"
+echo ""
+echo "Rollback: ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_rollback.sh"
+echo "  (restores all .bak files matching *.bak-ga-${TS})"
+echo "========================================================"
+REMOTE
diff --git a/gate2_rollback.sh b/gate2_rollback.sh
new file mode 100755
index 0000000..1e8dec6
--- /dev/null
+++ b/gate2_rollback.sh
@@ -0,0 +1,146 @@
+#!/usr/bin/env bash
+# gate2_rollback.sh — Restore all files modified by gate2_inject.sh
+# Finds every .bak-ga-* file written by the inject run and restores it.
+# Then reloads affected pm2 processes.
+#
+# Run from Mac2:
+#   ! ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_rollback.sh
+# ---------------------------------------------------------------------------
+set -uo pipefail
+
+ssh root@45.61.58.125 'bash -s' <<'REMOTE'
+set -uo pipefail
+ID="G-J2SL8P5KDL"
+
+echo ""
+echo "========================================================"
+echo " GATE-2 ROLLBACK — restoring .bak-ga-* files"
+echo "========================================================"
+echo ""
+
+# Find every .bak file this inject run created (pattern: <original>.bak-ga-YYYYMMDD-HHMMSS)
+BAK_FILES=()
+for f in \
+  /root/Projects/wolfgordon-site/public/index.html.bak-ga-* \
+  /root/Projects/artmura-site/public/index.html.bak-ga-* \
+  /root/Projects/carnegie-site/public/index.html.bak-ga-* \
+  /root/Projects/thibaut-site/public/index.html.bak-ga-* \
+  /root/Projects/elitis-site/public/index.html.bak-ga-* \
+  /root/Projects/romo-site/public/index.html.bak-ga-* \
+  /root/Projects/la-walls-site/public/index.html.bak-ga-* \
+  /root/Projects/british-walls-site/public/index.html.bak-ga-* \
+  /root/Projects/leed-walls-site/public/index.html.bak-ga-* \
+  /root/Projects/surface-stick-site/public/index.html.bak-ga-* \
+  /root/Projects/coleandson-site/public/index.html.bak-ga-* \
+  /root/Projects/fallingstar-site/public/index.html.bak-ga-* \
+  /root/Projects/philipjeffries-site/public/index.html.bak-ga-* \
+  /root/Projects/primo-leathers-site/public/index.html.bak-ga-* \
+  /root/Projects/quadrille-house-site/public/index.html.bak-ga-* \
+  "/var/www/fabricut.designerwallcoverings.com/public/index.html.bak-ga-*" \
+  /root/Projects/fentucci-site/public/index.html.bak-ga-* \
+  "/root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html.bak-ga-*"
+do
+  [ -f "$f" ] && BAK_FILES+=("$f")
+done
+
+if [ ${#BAK_FILES[@]} -eq 0 ]; then
+  echo "  Nothing to roll back — no .bak-ga-* files found for any of the 17 target paths."
+  echo "  (Either gate2_inject.sh was never run, or all files were already skipped as no-op.)"
+  exit 0
+fi
+
+N_RESTORED=0
+N_FAILED=0
+TOUCHED_PM2=()
+
+restore_file() {
+  local bak="$1"
+  # Original file is everything before .bak-ga-
+  local orig="${bak%.bak-ga-*}"
+
+  echo "  Restoring: $orig"
+  echo "       from: $bak"
+
+  if cp -a "$bak" "$orig"; then
+    echo "       status: RESTORED"
+    (( N_RESTORED++ )) || true
+    # Track for pm2 reload
+    TOUCHED_PM2+=("$orig")
+  else
+    echo "       status: FAIL — could not copy bak"
+    (( N_FAILED++ )) || true
+  fi
+}
+
+for bak in "${BAK_FILES[@]}"; do
+  restore_file "$bak"
+  echo ""
+done
+
+# ─── Reload pm2 processes for restored files ────────────────────────────────
+echo "Reloading pm2 processes..."
+
+# If wolfgordon shared template was restored, reload all 30 wolf processes
+wolf_restored=0
+for orig in "${TOUCHED_PM2[@]}"; do
+  [[ "$orig" == *wolfgordon-site/public/index.html ]] && wolf_restored=1
+done
+
+if [ "$wolf_restored" -eq 1 ]; then
+  echo "  wolfgordon-site shared template — reloading all 30 processes..."
+  WOLF_PROCS=(
+    anna-french-site brunschwig-fils-site coordonne-site designers-guild-site
+    designtex-site gp-j-baker-site groundworks-site harlequin-site knoll-site
+    koroseal-site kravet-site lee-jofa-site maharam-site maya-romanoff-site
+    mulberry-site nina-campbell-site nobilis-site osborne-little-site
+    phillip-jeffries-site pierre-frey-site ralph-lauren-site rebel-walls-site
+    sancar-site sandberg-site scalamandre-site threads-site william-morris-site
+    wolf-gordon-site wolfgordon-site zoffany-site
+  )
+  ok=0; fail=0
+  for p in "${WOLF_PROCS[@]}"; do
+    pm2 reload "$p" >/dev/null 2>&1 && (( ok++ )) || (( fail++ )) || true
+  done
+  echo "    reloaded ${ok}/30 OK (${fail} failed)"
+fi
+
+# Map standalone paths to pm2 names
+declare -A PATH_PM2
+PATH_PM2["/root/Projects/artmura-site/public/index.html"]="artmura-site"
+PATH_PM2["/root/Projects/carnegie-site/public/index.html"]="carnegie-site"
+PATH_PM2["/root/Projects/thibaut-site/public/index.html"]="thibaut-site"
+PATH_PM2["/root/Projects/elitis-site/public/index.html"]="elitis-site"
+PATH_PM2["/root/Projects/romo-site/public/index.html"]="romo-site"
+PATH_PM2["/root/Projects/la-walls-site/public/index.html"]="la-walls-site"
+PATH_PM2["/root/Projects/british-walls-site/public/index.html"]="british-walls-site"
+PATH_PM2["/root/Projects/leed-walls-site/public/index.html"]="leed-walls-site"
+PATH_PM2["/root/Projects/surface-stick-site/public/index.html"]="surface-stick-site"
+PATH_PM2["/root/Projects/coleandson-site/public/index.html"]="coleandson-site"
+PATH_PM2["/root/Projects/fallingstar-site/public/index.html"]="fallingstar-site"
+PATH_PM2["/root/Projects/philipjeffries-site/public/index.html"]="philipjeffries-site"
+PATH_PM2["/root/Projects/primo-leathers-site/public/index.html"]="primo-leathers-site"
+PATH_PM2["/root/Projects/quadrille-house-site/public/index.html"]="quadrille-house-site"
+PATH_PM2["/var/www/fabricut.designerwallcoverings.com/public/index.html"]="fabricut-landing"
+PATH_PM2["/root/Projects/fentucci-site/public/index.html"]="fentucci-site"
+PATH_PM2["/root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html"]="fentucci-website"
+
+for orig in "${TOUCHED_PM2[@]}"; do
+  [[ "$orig" == *wolfgordon-site/public/index.html ]] && continue  # already handled above
+  pname="${PATH_PM2[$orig]:-}"
+  if [ -n "$pname" ]; then
+    pm2 reload "$pname" >/dev/null 2>&1 && echo "  reloaded $pname" || echo "  WARN: reload $pname failed"
+  fi
+done
+
+# ────────────────────────────────────────────────────────────────────────────
+echo ""
+echo "========================================================"
+echo " ROLLBACK SUMMARY"
+echo "========================================================"
+echo "  Restored : $N_RESTORED files"
+echo "  Failed   : $N_FAILED files"
+echo ""
+echo "NOTE: .bak files are preserved at their original .bak-ga-* paths."
+echo "      Verify with: grep -l 'G-J2SL8P5KDL' <any restored file> (should return nothing)"
+echo "========================================================"
+REMOTE

← bccd4f3 auto-data-snapshot: 2026-08-21T17:47:31 (1 data files) — etl  ·  back to Ga Allsites  ·  chore: GA GATE-2 fleet injector (route-by-nginx, idempotent, f399dae →