[object Object]

← 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

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 →