[object Object]

← back to Designerwallcoverings

TK-10372: turnkey fix+rollback for final 3 dup-image cross-links (Beechnut/Blonde Pearl/Golden Age); sources verified, gated write

9f550a99b41f52bf183340920bfbdd45d0d69e1f · 2026-09-03 12:01:17 -0700 · Steve Abrams

Files touched

Diff

commit 9f550a99b41f52bf183340920bfbdd45d0d69e1f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 12:01:17 2026 -0700

    TK-10372: turnkey fix+rollback for final 3 dup-image cross-links (Beechnut/Blonde Pearl/Golden Age); sources verified, gated write
---
 scripts/tk10372-fix/fix-tk10372.mjs      | 68 ++++++++++++++++++++++++++++++++
 scripts/tk10372-fix/rollback-tk10372.mjs | 29 ++++++++++++++
 2 files changed, 97 insertions(+)

diff --git a/scripts/tk10372-fix/fix-tk10372.mjs b/scripts/tk10372-fix/fix-tk10372.mjs
new file mode 100644
index 0000000..1676744
--- /dev/null
+++ b/scripts/tk10372-fix/fix-tk10372.mjs
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+// TK-10372 — fix the final 3 duplicate-image cross-links (correct colorway sources verified 2026-09-03).
+// GATED: customer-facing Shopify media write. Run only with Steve's approval.
+// Reversible: writes a rollback map to ./rollback-<ts>.json; run rollback-tk10372.mjs <file> to undo.
+import fs from 'fs';
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/^["']|["']$/g, '').trim();
+const SHOP = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const api = (p, opt = {}) => fetch(`https://${SHOP}/admin/api/${API}${p}`, { ...opt, headers: { ...H, ...(opt.headers || {}) } });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// product id -> { name, correctSrc (verified 200 image/jpeg), keepOriginals? }
+const JOBS = [
+  { id: '7430015057971', name: 'MindTheGap Countryside Plaid Beechnut WP30010',
+    correctSrc: 'https://eu.mindtheg.com/media/catalog/product//c/_/c_o_countryside_plaid_beechnut.jpg' },
+  { id: '7387533934643', name: 'Maya Romanoff Bouquet Roses Blonde Pearl (RV-11X14)',
+    correctSrc: 'https://assets.mayaromanoff.com/web/skus/RV-11X14_Blonde-Pearl_RGB_Zoom-In.jpg' },
+  { id: '7387533279283', name: 'Maya Romanoff Bouquet Roses Golden Age (RV-11X13)',
+    correctSrc: 'https://assets.mayaromanoff.com/web/skus/RV-11X13_Golden-Age_RGB_Zoom_In.jpg' },
+];
+
+const rollback = { ts: new Date().toISOString(), products: [] };
+
+for (const j of JOBS) {
+  console.log(`\n=== ${j.name} (${j.id}) ===`);
+  // 0) verify source reachable + is a JPEG
+  const src = await fetch(j.correctSrc, { headers: { 'User-Agent': 'Mozilla/5.0' } });
+  if (!src.ok || !/image\/(jpe?g|png|webp)/.test(src.headers.get('content-type') || '')) {
+    console.log(`  ABORT: source not fetchable (${src.status} ${src.headers.get('content-type')})`); continue;
+  }
+  const bytes = Buffer.from(await src.arrayBuffer());
+  console.log(`  source OK: ${src.status} ${src.headers.get('content-type')} ${bytes.length} bytes`);
+
+  // 1) record current state (rollback)
+  const before = (await (await api(`/products/${j.id}.json?fields=id,image,images`)).json()).product;
+  const prevFeaturedId = before.image?.id;
+  rollback.products.push({ id: j.id, name: j.name, prevFeaturedId,
+    prevImages: before.images.map(i => ({ id: i.id, position: i.position, src: i.src })) });
+  console.log(`  before: featured=${prevFeaturedId} images=${before.images.length}`);
+
+  // 2) add correct image WITHOUT position first (verify import succeeds), via base64 attachment (CDN-independent)
+  const filename = j.correctSrc.split('/').pop().split('?')[0];
+  let res = await api(`/products/${j.id}/images.json`, { method: 'POST',
+    body: JSON.stringify({ image: { attachment: bytes.toString('base64'), filename } }) });
+  let body = await res.json();
+  if (!res.ok || !body.image?.id) { console.log('  ADD FAILED:', res.status, JSON.stringify(body)); continue; }
+  const newId = body.image.id;
+  console.log(`  added image id=${newId} (${filename})`);
+  rollback.products[rollback.products.length - 1].addedImageId = newId;
+
+  // 3) feature it (position 1) atomically
+  await sleep(1500);
+  res = await api(`/products/${j.id}/images/${newId}.json`, { method: 'PUT',
+    body: JSON.stringify({ image: { id: newId, position: 1 } }) });
+  await res.json();
+
+  // 4) verify featured flipped
+  await sleep(1500);
+  const after = (await (await api(`/products/${j.id}.json?fields=image`)).json()).product;
+  const okFeat = String(after.image?.id) === String(newId);
+  console.log(`  featured now: ${after.image?.id} -> ${(after.image?.src || '').split('/').pop().split('?')[0]}  ${okFeat ? 'OK ✅' : 'MISMATCH ⚠️'}`);
+}
+
+const rbPath = new URL('./rollback-' + Date.now() + '.json', import.meta.url).pathname;
+fs.writeFileSync(rbPath, JSON.stringify(rollback, null, 2));
+console.log('\nRollback map written:', rbPath);
+console.log('To undo: node ' + new URL('./rollback-tk10372.mjs', import.meta.url).pathname + ' ' + rbPath);
diff --git a/scripts/tk10372-fix/rollback-tk10372.mjs b/scripts/tk10372-fix/rollback-tk10372.mjs
new file mode 100644
index 0000000..d762a4c
--- /dev/null
+++ b/scripts/tk10372-fix/rollback-tk10372.mjs
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+// TK-10372 rollback — delete each added image and re-feature the prior original.
+// Usage: node rollback-tk10372.mjs rollback-<ts>.json
+import fs from 'fs';
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/^["']|["']$/g, '').trim();
+const SHOP = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const api = (p, opt = {}) => fetch(`https://${SHOP}/admin/api/${API}${p}`, { ...opt, headers: { ...H, ...(opt.headers || {}) } });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const rb = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
+for (const p of rb.products) {
+  console.log(`\n=== rollback ${p.name} (${p.id}) ===`);
+  if (p.addedImageId) {
+    await api(`/products/${p.id}/images/${p.addedImageId}.json`, { method: 'DELETE' });
+    console.log(`  deleted added image ${p.addedImageId}`);
+    await sleep(1200);
+  }
+  if (p.prevFeaturedId) {
+    await api(`/products/${p.id}/images/${p.prevFeaturedId}.json`, { method: 'PUT',
+      body: JSON.stringify({ image: { id: p.prevFeaturedId, position: 1 } }) });
+    console.log(`  re-featured original ${p.prevFeaturedId}`);
+    await sleep(1200);
+  }
+  const after = (await (await api(`/products/${p.id}.json?fields=image`)).json()).product;
+  console.log(`  featured now: ${after.image?.id}`);
+}
+console.log('\nRollback complete.');

← d455f5d TK-11186: showroom-vendor suppression enforcement — tagger g  ·  back to Designerwallcoverings  ·  TK-11186: A2 GMC-channel unpublisher (PJ-scoped, publication e45ffc0 →