← 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
A scripts/tk10372-fix/fix-tk10372.mjsA scripts/tk10372-fix/rollback-tk10372.mjs
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 →