[object Object]

← back to Designerwallcoverings

TK-11070: hero-fix tool — re-upload correct per-colorway hero + reorder to pos1 on the 36 stale-image live Sanderson products (demote-only, reversible, rollback-mapped)

0d054c4da9d32bef6dec68147c96b62743aa0675 · 2026-09-01 17:26:15 -0700 · Steve Abrams

Files touched

Diff

commit 0d054c4da9d32bef6dec68147c96b62743aa0675
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 17:26:15 2026 -0700

    TK-11070: hero-fix tool — re-upload correct per-colorway hero + reorder to pos1 on the 36 stale-image live Sanderson products (demote-only, reversible, rollback-mapped)
---
 scripts/sanderson-onboard/tk11070-hero-fix.mjs | 109 +++++++++++++++++++++++++
 1 file changed, 109 insertions(+)

diff --git a/scripts/sanderson-onboard/tk11070-hero-fix.mjs b/scripts/sanderson-onboard/tk11070-hero-fix.mjs
new file mode 100644
index 0000000..2f3fe0e
--- /dev/null
+++ b/scripts/sanderson-onboard/tk11070-hero-fix.mjs
@@ -0,0 +1,109 @@
+#!/usr/bin/env node
+// TK-11070 hero-fix: for each Sanderson live product carrying the WRONG per-colorway
+// position-1 hero, upload its CORRECT hero (from payloads-imgfixed.jsonl) and reorder it
+// to position 1. Does NOT delete the wrong image (demote-only) — fully reversible.
+// Rollback map (pre-change image list per product) saved to /tmp/tk11070-herofix-rollback.json.
+//
+// Usage:
+//   node tk11070-hero-fix.mjs                 # dry-run (default) — shows plan, no writes
+//   node tk11070-hero-fix.mjs --apply --limit=5   # canary
+//   node tk11070-hero-fix.mjs --apply             # full remaining
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dir = path.dirname(fileURLToPath(import.meta.url));
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+const TOKEN = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
+  .split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN=')) || '')
+  .split('=').slice(1).join('=').replace(/^["' ]+|["' ]+$/g, '');
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const APPLY = process.argv.includes('--apply');
+const LIMITm = process.argv.find(a => a.startsWith('--limit='));
+const LIMIT = LIMITm ? parseInt(LIMITm.split('=')[1], 10) : Infinity;
+
+const ROLLBACK = '/tmp/tk11070-herofix-rollback.json';
+const MISMATCH = '/tmp/tk11070-live-mismatches.json';
+
+// expected heroes from imgfixed payloads
+const rows = fs.readFileSync(path.join(__dir, 'out/payloads-imgfixed.jsonl'), 'utf8')
+  .split('\n').filter(Boolean).map(l => JSON.parse(l));
+const bySku = Object.fromEntries(rows.map(r => [r.sku, r]));
+const expHeroSrc = (sku) => {
+  const imgs = bySku[sku]?.product?.images || [];
+  return imgs[0]?.src || null;
+};
+const codeOf = (fnOrSrc) => (fnOrSrc || '').split('/').pop().split('?')[0].split('_')[0];
+
+const mismatches = JSON.parse(fs.readFileSync(MISMATCH, 'utf8'));
+
+async function gql(query, variables) {
+  const res = await fetch(`https://${STORE}/admin/api/${VER}/graphql.json`, {
+    method: 'POST',
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await res.json();
+  if (j.errors) throw new Error(JSON.stringify(j.errors));
+  return j.data;
+}
+async function rest(pmethod, url, body) {
+  const res = await fetch(`https://${STORE}/admin/api/${VER}${url}`, {
+    method: pmethod,
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+  const j = await res.json().catch(() => ({}));
+  if (!res.ok) throw new Error(`${res.status} ${JSON.stringify(j)}`);
+  return j;
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function run() {
+  const rollback = fs.existsSync(ROLLBACK) ? JSON.parse(fs.readFileSync(ROLLBACK, 'utf8')) : {};
+  let done = 0, ok = 0, skip = 0, err = 0;
+  for (const m of mismatches) {
+    if (done >= LIMIT) break;
+    const sku = m.sku, pid = m.pid;
+    const src = expHeroSrc(sku);
+    if (!src) { console.log(`SKIP ${sku}: no expected hero in payloads`); skip++; continue; }
+    const expCode = codeOf(src);
+    // pull current images (rollback snapshot)
+    const cur = await rest('GET', `/products/${pid}.json?fields=id,status,images`);
+    const imgs = (cur.product.images || []).sort((a, b) => a.position - b.position);
+    const curPos1 = imgs[0];
+    const curCode = codeOf(curPos1?.src);
+    if (expCode && curCode === expCode) { console.log(`ALREADY-OK ${sku}: pos1=${curCode}`); ok++; done++; continue; }
+    // is the correct image already present but not pos1? if so just reorder; else upload
+    let target = imgs.find(i => codeOf(i.src) === expCode);
+    console.log(`${APPLY ? 'FIX ' : 'PLAN'} ${sku} [${cur.product.status}] pid=${pid}  pos1=${curCode} -> want ${expCode}  ${target ? '(reorder existing)' : '(upload+reorder)'}  src=${src.split('/').pop()}`);
+    if (!APPLY) { done++; continue; }
+    // save rollback (pre-change ordered image ids + src)
+    rollback[pid] = { sku, before: imgs.map(i => ({ id: i.id, src: i.src, position: i.position })) };
+    fs.writeFileSync(ROLLBACK, JSON.stringify(rollback, null, 1));
+    try {
+      if (!target) {
+        const up = await rest('POST', `/products/${pid}/images.json`, { image: { src } });
+        target = up.image;
+      }
+      // reorder: put target first, keep the rest in existing order
+      const order = [target.id, ...imgs.filter(i => i.id !== target.id).map(i => i.id)];
+      await rest('PUT', `/products/${pid}.json`, { product: { id: Number(pid), images: order.map((id, idx) => ({ id, position: idx + 1 })) } });
+      // verify
+      await sleep(600);
+      const v = await rest('GET', `/products/${pid}.json?fields=images`);
+      const vimgs = (v.product.images || []).sort((a, b) => a.position - b.position);
+      const vpos1 = codeOf(vimgs[0]?.src);
+      if (vpos1 === expCode) { console.log(`   OK pos1 now ${vpos1}`); ok++; }
+      else { console.log(`   *** POST-CHECK FAILED pos1=${vpos1} exp=${expCode}`); err++; }
+    } catch (e) {
+      console.log(`   ERR ${sku}: ${e.message}`); err++;
+    }
+    done++;
+    await sleep(900); // inter-item pacing
+  }
+  console.log(`\nDONE. processed=${done} ok=${ok} already-ok=${skip === 0 ? '(see ok)' : skip} err=${err}  rollback=${ROLLBACK}`);
+}
+run();

← f4eb073 sanderson daily-batch: add post-golive anomaly guard (DTD ve  ·  back to Designerwallcoverings  ·  TK-11076: sanitize commas in color: tag (Shopify splits tags 98f971f →