← back to Designerwallcoverings
TK-10168: RW image re-attach — 5 DRAFT products get correct colorway image (Steve-approved, canary-verified)
651d16319a77e5efb5c929bc6d3880253257bb4d · 2026-08-04 09:51:27 -0700 · Steve Abrams
Files touched
A scripts/google-feed/rw-image-reattach.mjs
Diff
commit 651d16319a77e5efb5c929bc6d3880253257bb4d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 4 09:51:27 2026 -0700
TK-10168: RW image re-attach — 5 DRAFT products get correct colorway image (Steve-approved, canary-verified)
---
scripts/google-feed/rw-image-reattach.mjs | 59 +++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/scripts/google-feed/rw-image-reattach.mjs b/scripts/google-feed/rw-image-reattach.mjs
new file mode 100644
index 0000000..bc12888
--- /dev/null
+++ b/scripts/google-feed/rw-image-reattach.mjs
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+/**
+ * TK-10168 — Rebel Walls image re-attach (Steve-approved 2026-08-04, "approve all 6").
+ * Attaches each DRAFT no-image RW product's VERIFIED catalog image (colorway-matched,
+ * confirmed in Cycles 2-3) as its featured media. Products stay DRAFT.
+ *
+ * GATED: default DRY-RUN. Live write requires EXECUTE=1. LIMIT=N caps rows (canary=1).
+ * Rollback log: ~/.claude/yolo-queue/restore-maps/rw-image-reattach.jsonl
+ */
+import { readFileSync, appendFileSync, mkdirSync } from 'fs';
+import { join } from 'path';
+import { homedir } from 'os';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = readFileSync(join(homedir(), 'Projects/secrets-manager/.env'), 'utf8')
+ .split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='))?.split('=').slice(1).join('=').trim();
+const EXECUTE = process.env.EXECUTE === '1';
+const LIMIT = parseInt(process.env.LIMIT || '0', 10) || Infinity;
+
+const LOGDIR = join(homedir(), '.claude/yolo-queue/restore-maps');
+mkdirSync(LOGDIR, { recursive: true });
+const LOG = join(LOGDIR, 'rw-image-reattach.jsonl');
+
+async function gql(query, variables) {
+ const r = await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ return r.json();
+}
+const Q_FIND = `query($q:String!){ products(first:1, query:$q){ nodes{ id handle status media(first:5){ nodes{ id } } } } }`;
+const M_CREATE = `mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ ... on MediaImage { id status } } mediaUserErrors{ field message } } }`;
+
+const rows = readFileSync('/tmp/rw_reattach.tsv', 'utf8').trim().split('\n')
+ .map(l => l.split('\t')).filter(a => a.length >= 3)
+ .map(([handle, dw_sku, image_url]) => ({ handle, dw_sku, image_url }));
+
+console.log(`\n=== RW image re-attach — ${EXECUTE ? 'EXECUTE (LIVE)' : 'DRY-RUN'} — ${Math.min(LIMIT, rows.length)}/${rows.length} products ===\n`);
+let done = 0, skipped = 0, failed = 0;
+for (const row of rows) {
+ if (done + skipped + failed >= LIMIT) break;
+ const f = await gql(Q_FIND, { q: `handle:${row.handle}` });
+ const p = f?.data?.products?.nodes?.[0];
+ if (!p) { console.log(` ✗ ${row.dw_sku}: product not found (handle:${row.handle})`); failed++; continue; }
+ const mediaCount = p.media?.nodes?.length || 0;
+ if (mediaCount > 0) { console.log(` ⏭ ${row.dw_sku}: already has ${mediaCount} media — SKIP (guard)`); skipped++; continue; }
+ console.log(` ${EXECUTE ? '▶' : '·'} ${row.dw_sku} [${p.status}] ${p.id}\n attach: ${row.image_url.slice(0, 80)}...`);
+ if (!EXECUTE) { console.log(' [dry-run: no change]\n'); done++; continue; }
+ const cr = await gql(M_CREATE, { id: p.id, media: [{ mediaContentType: 'IMAGE', originalSource: row.image_url, alt: row.dw_sku }] });
+ const ue = cr?.data?.productCreateMedia?.mediaUserErrors || [];
+ const newId = cr?.data?.productCreateMedia?.media?.[0]?.id;
+ if (ue.length || !newId) { console.log(` ✗ FAILED: ${JSON.stringify(ue) || JSON.stringify(cr)}\n`); failed++; continue; }
+ appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), productId: p.id, dw_sku: row.dw_sku, mediaId: newId, image_url: row.image_url }) + '\n');
+ console.log(` ✓ attached media ${newId}\n`);
+ done++;
+}
+console.log(`\n=== ${EXECUTE ? 'EXECUTED' : 'DRY-RUN'}: ${done} attached/planned, ${skipped} skipped(had media), ${failed} failed ===`);
← e9b0570 auto-save: 2026-08-04T08:01:22 (3 files) — scripts/price-she
·
back to Designerwallcoverings
·
build-romo-rolls: guard empty baseSku — skip products with n 0aa4464 →