← 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
A scripts/sanderson-onboard/tk11070-hero-fix.mjs
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 →