[object Object]

← back to Dw Domain Fleet

Add fleet-heroes.sh driver: repeatable fleet-wide hero refresh + global dedupe (report-only default, --deploy gated, batched reload)

addb71d1f903b9160fe0f303798eaf402de3df55 · 2026-06-01 14:19:32 -0700 · Steve Abrams

Files touched

Diff

commit addb71d1f903b9160fe0f303798eaf402de3df55
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 14:19:32 2026 -0700

    Add fleet-heroes.sh driver: repeatable fleet-wide hero refresh + global dedupe (report-only default, --deploy gated, batched reload)
---
 scripts/fleet-heroes.sh | 121 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 121 insertions(+)

diff --git a/scripts/fleet-heroes.sh b/scripts/fleet-heroes.sh
new file mode 100755
index 0000000..095eed2
--- /dev/null
+++ b/scripts/fleet-heroes.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+# fleet-heroes.sh — refresh + dedupe hero images across the whole DW web fleet.
+#
+# Two layers:
+#   1. GENERIC fleet (dw-domain-fleet, 43 rotating-hero sites) — driven by the
+#      regenerable manifest data/hero-allocation.json.
+#   2. STANDALONE sister sites — static public/hero-bg.jpg files; duplicates get
+#      replaced with unique, clean, niche-relevant catalog images.
+#
+# Usage:
+#   scripts/fleet-heroes.sh                 # REPORT-ONLY (default): regen manifests,
+#                                           #   audit live fleet, print what WOULD change.
+#                                           #   Zero prod writes.
+#   scripts/fleet-heroes.sh --deploy        # push generic (BATCHED reload) + apply
+#                                           #   unique standalone heroes + verify + backsync
+#   scripts/fleet-heroes.sh --pull-catalog  # refresh data/catalog.json from live store first
+#                                           #   (combine with --deploy)
+#
+# Safety: NEVER mass-reloads pm2 (that OOM-killed the daemon once). dwf apps are
+# reloaded in batches of 6. All standalone hero replacements back up to
+# <file>.pre-uniquehero and validate JPEG/PNG >= 30KB before swapping.
+set -uo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+HOST="root@45.61.58.125"
+REMOTE="/root/public-projects/dw-domain-fleet"
+DEPLOY=0; PULL=0
+for a in "$@"; do
+  case "$a" in
+    --deploy) DEPLOY=1 ;;
+    --pull-catalog) PULL=1 ;;
+    -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
+    *) echo "unknown arg: $a"; exit 2 ;;
+  esac
+done
+cd "$ROOT"
+mode=$([ "$DEPLOY" = 1 ] && echo "DEPLOY" || echo "REPORT-ONLY")
+echo "════════ fleet-heroes ($mode) ════════"
+
+# ---------- 0. optional catalog refresh ----------
+if [ "$PULL" = 1 ]; then
+  echo "==> pulling fresh catalog"; node scripts/pull-catalog.js || { echo "catalog pull failed"; exit 1; }
+fi
+
+# ---------- 1. GENERIC fleet manifest ----------
+echo "==> [generic] regenerating hero-allocation.json"
+node scripts/gen-hero-allocation.js || { echo "generic manifest FAILED (dup or error)"; exit 1; }
+
+if [ "$DEPLOY" = 1 ]; then
+  echo "==> [generic] pushing manifest + server.js to prod"
+  rsync -az server.js "$HOST:$REMOTE/server.js"
+  rsync -az data/hero-allocation.json "$HOST:$REMOTE/data/hero-allocation.json"
+  ssh "$HOST" "node -c $REMOTE/server.js" || { echo "remote syntax check failed"; exit 1; }
+  echo "==> [generic] BATCHED reload of dwf apps (6 at a time)"
+  ssh "$HOST" 'bash -s' <<'RB'
+names=$(pm2 jlist 2>/dev/null | grep -oE '"name":"dwf-[^"]+"' | sed 's/"name":"//;s/"//' | sort -u)
+arr=($names); batch=()
+for n in "${arr[@]}"; do batch+=("$n"); if [ ${#batch[@]} -ge 6 ]; then pm2 restart "${batch[@]}" >/dev/null 2>&1; sleep 3; batch=(); fi; done
+[ ${#batch[@]} -gt 0 ] && { pm2 restart "${batch[@]}" >/dev/null 2>&1; sleep 3; }
+echo "  dwf online: $(pm2 list 2>/dev/null | grep -E 'dwf-' | grep -c online)"
+RB
+fi
+
+# ---------- 2. STANDALONE static heroes ----------
+if [ "$DEPLOY" = 1 ]; then
+  echo "==> [standalone] capturing current served hero-bg.jpg paths+hashes from prod"
+  ssh "$HOST" 'for f in $(find /var/www -maxdepth 3 -regextype posix-extended -regex "/var/www/[a-z0-9]+\.(com|org)/public/hero-bg.jpg" 2>/dev/null; find /root/Projects -maxdepth 3 -path "*/public/hero-bg.jpg" 2>/dev/null); do
+      d=$(basename "$(dirname "$(dirname "$f")")"); echo -e "$d\t$f\t$(md5sum "$f"|cut -d" " -f1)\t$(stat -c%s "$f")"; done | sort -u' > data/standalone-hero-paths.tsv
+  echo "    captured $(wc -l < data/standalone-hero-paths.tsv) hero files"
+  echo "==> [standalone] regenerating allocation (disjoint from generic + each other)"
+  node scripts/gen-standalone-heroes.js || { echo "standalone alloc FAILED"; exit 1; }
+  # dup-cluster served domains only (skip already-unique + non-served repo twins)
+  node -e '
+    const fs=require("fs"),a=require("./data/standalone-hero-allocation.json");
+    const rows=fs.readFileSync("data/standalone-hero-paths.tsv","utf8").trim().split("\n").map(l=>l.split("\t"));
+    const byHash={}; rows.forEach(([d,p,h])=>{(byHash[h]=byHash[h]||[]).push(d)});
+    const dup=rows.filter(([d,p,h])=>byHash[h].length>1).map(([d])=>d);
+    const wwwSlugs=new Set(dup.filter(d=>a[d]&&a[d].path.startsWith("/var/www")).map(d=>d.replace(/\.(com|org|net)$/,"")));
+    const served=dup.filter(d=>{const w=a[d]&&a[d].path.startsWith("/var/www");const s=d.replace(/\.(com|org|net)$/,"");return w||!wwwSlugs.has(s);});
+    fs.writeFileSync("/tmp/fh_served.txt",served.join("\n"));
+    console.log("    duplicate served sites needing unique heroes:",served.length);
+  '
+  if [ -s /tmp/fh_served.txt ]; then
+    ssh "$HOST" "mkdir -p /root/_hero-fix"
+    scp -q data/standalone-hero-allocation.json "$HOST:/root/_hero-fix/alloc.json"
+    scp -q scripts/apply-standalone-heroes.js scripts/apply-retry.js "$HOST:/root/_hero-fix/"
+    scp -q /tmp/fh_served.txt "$HOST:/root/_hero-fix/served.txt"
+    echo "==> [standalone] applying unique heroes on prod (backup + validate >=30KB)"
+    ssh "$HOST" "cd /root/_hero-fix && node apply-standalone-heroes.js alloc.json served.txt 2>&1 | tail -4"
+    ssh "$HOST" "rm -rf /root/_hero-fix"
+    echo "    NOTE: any tiny-thumbnail rejects need scripts/gen-retry-candidates.js + apply-retry.js (see skill)."
+  else
+    echo "    no new standalone duplicates — nothing to apply"
+  fi
+else
+  echo "==> [standalone] (report-only) live duplicate audit"
+  ssh "$HOST" 'tmp=$(mktemp); served=$(find /var/www -maxdepth 3 -regextype posix-extended -regex "/var/www/[a-z0-9]+\.(com|org)/public/hero-bg.jpg" 2>/dev/null)
+    for f in /root/Projects/ffepurchasing/public/hero-bg.jpg /root/Projects/hospitalitywallcoverings/public/hero-bg.jpg /root/Projects/wallpapercanada/public/hero-bg.jpg; do [ -f "$f" ] && served="$served
+$f"; done
+    for f in $served; do [ -f "$f" ] && md5sum "$f"|cut -d" " -f1 >> "$tmp"; done
+    echo "    standalone served: $(wc -l < "$tmp") files / $(sort -u "$tmp"|wc -l) unique / $(sort "$tmp"|uniq -d|wc -l) DUPLICATE clusters"; rm -f "$tmp"'
+fi
+
+# ---------- 3. VERIFY (always) ----------
+echo "==> verifying live fleet uniqueness"
+ssh "$HOST" 'bash -s' <<'VR'
+g=$(mktemp)
+for d in $(ls /etc/nginx/sites-enabled/ | grep "\.conf$" | sed "s/.conf//"); do
+  curl -skL --max-time 7 --resolve "$d:443:127.0.0.1" "https://$d/" | grep -oE "cinema-slide[^>]*background-image:url\('[^']+'\)" | grep -oE "https://[^']+" >> "$g" 2>/dev/null
+done
+echo "  GENERIC live: $(wc -l < "$g") slides / $(sort -u "$g"|wc -l) unique / $(sort "$g"|uniq -d|wc -l) dup"
+s=$(mktemp); served=$(find /var/www -maxdepth 3 -regextype posix-extended -regex "/var/www/[a-z0-9]+\.(com|org)/public/hero-bg.jpg" 2>/dev/null)
+for f in /root/Projects/ffepurchasing/public/hero-bg.jpg /root/Projects/hospitalitywallcoverings/public/hero-bg.jpg /root/Projects/wallpapercanada/public/hero-bg.jpg; do [ -f "$f" ] && served="$served
+$f"; done
+for f in $served; do [ -f "$f" ] && md5sum "$f"|cut -d" " -f1 >> "$s"; done
+echo "  STANDALONE live: $(wc -l < "$s") files / $(sort -u "$s"|wc -l) unique / $(sort "$s"|uniq -d|wc -l) dup"
+rm -f "$g" "$s"
+VR
+
+echo "════════ done ($mode) ════════"
+[ "$DEPLOY" = 0 ] && echo "Re-run with --deploy to push changes live. Then backsync prod→Mac2 + commit (see skill)."

← c069168 fix: app.set('trust proxy', 1) — resolves ERR_ERL_UNEXPECTED  ·  back to Dw Domain Fleet  ·  fleet-heroes: open DW fleet registry viewer (:9774) on launc 6c986ea →