← back to Dw Domain Fleet

scripts/fleet-heroes.sh

140 lines

#!/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) ════════"

# Open the DW Fleet registry viewer when launched (macOS interactive only).
if command -v open >/dev/null 2>&1 && [ -z "${FLEET_HEROES_NO_OPEN:-}" ]; then
  curl -s -o /dev/null --max-time 3 http://localhost:9774/ 2>/dev/null \
    || ( cd "$HOME/Projects/dw-fleet-registry" && nohup node viewer/server.mjs >/tmp/dw-fleet-registry.log 2>&1 & disown )
  open "http://localhost:9774/" 2>/dev/null && echo "==> opened fleet registry → http://localhost:9774/"
fi

# ---------- 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");
    const rows=fs.readFileSync("data/standalone-hero-paths.tsv","utf8").trim().split("\n").map(l=>l.split("\t"));
    // Collapse to ONE entry per logical site (slug), preferring the /var/www
    // served copy. Same-site www+repo copies are intentionally identical and must
    // NOT be treated as cross-site duplicates (that caused needless re-churn).
    const bySlug={};
    for (const [d,p,h] of rows) {
      const slug=d.replace(/\.(com|org|net)$/,"");
      const isWww=p.startsWith("/var/www");
      if (!bySlug[slug] || (isWww && !bySlug[slug].p.startsWith("/var/www"))) bySlug[slug]={d,p,h};
    }
    const served=Object.values(bySlug);
    const byHash={}; served.forEach(({h,d})=>{(byHash[h]=byHash[h]||[]).push(d)});
    const dup=served.filter(({h})=>byHash[h].length>1).map(({d})=>d);   // TRUE cross-site dups only
    fs.writeFileSync("/tmp/fh_served.txt",dup.join("\n"));
    console.log("    distinct sites:",served.length,"| duplicate served sites needing unique heroes:",dup.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) ════════"
if [ "$DEPLOY" = 0 ]; then
  echo "Re-run with --deploy to push changes live. Then backsync prod→Mac2 + commit (see skill)."
fi
exit 0