← 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