[object Object]

← back to Designerwallcoverings

TK-11750: Maya Romanoff logo-hero fix — gated attach/reorder script + resolved images

950de982302f6b9b3324d29e79cc909bc2430022 · 2026-09-14 16:34:01 -0700 · Steve Abrams

3 ACTIVE products showing mayalogohold placeholder as hero. Real images
resolved feed-first from mayaromanoff.com (name-matched, 2500px). GATED:
default dry-run, EXECUTE=1 required. Reversible (local logo backup + undo.mjs).
Folds in Kimi review: poll READY before any delete; never leave zero-image window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145uBwAMQGsYkhx1rrDUfbW

Files touched

Diff

commit 950de982302f6b9b3324d29e79cc909bc2430022
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Sep 14 16:34:01 2026 -0700

    TK-11750: Maya Romanoff logo-hero fix — gated attach/reorder script + resolved images
    
    3 ACTIVE products showing mayalogohold placeholder as hero. Real images
    resolved feed-first from mayaromanoff.com (name-matched, 2500px). GATED:
    default dry-run, EXECUTE=1 required. Reversible (local logo backup + undo.mjs).
    Folds in Kimi review: poll READY before any delete; never leave zero-image window.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0145uBwAMQGsYkhx1rrDUfbW
---
 scripts/maya-romanoff-logohero-fix/README.md    |  50 ++++++++
 scripts/maya-romanoff-logohero-fix/apply.mjs    | 154 ++++++++++++++++++++++++
 scripts/maya-romanoff-logohero-fix/resolved.tsv |   3 +
 scripts/maya-romanoff-logohero-fix/undo.mjs     |  36 ++++++
 4 files changed, 243 insertions(+)

diff --git a/scripts/maya-romanoff-logohero-fix/README.md b/scripts/maya-romanoff-logohero-fix/README.md
new file mode 100644
index 0000000..c3b49fe
--- /dev/null
+++ b/scripts/maya-romanoff-logohero-fix/README.md
@@ -0,0 +1,50 @@
+# TK-11750 — Maya Romanoff logo-hero image fix
+
+**Real customer-facing defect** (NOT showroom-only): 3 ACTIVE Maya Romanoff
+products on the live DW store (`designer-laboratory-sandbox`) show a
+`mayalogohold` **logo placeholder** as their sole/featured hero image.
+
+Verified 2026-09-14 against the fresh dw_unified mirror (synced 14:21) AND the
+live storefront `/products/<handle>.json`. The ticket title said "13" — the
+authoritative current count is **3 ACTIVE** (+3 ARCHIVED, not customer-facing).
+
+## Resolved images (feed-first, $0, mayaromanoff.com — name-matched)
+
+| handle | pattern | real image |
+|---|---|---|
+| maya-50039 | Grand Scale Ajiro Chevron | `MR-AC2-4604_Grand-Scale-Ajiro-Chevron_Maple_Install...jpg` (2500×3125, 2.1M) |
+| maya-50179 | Beadazzled Geode | `BG-1554_Beadazzled-Geode_Metallic-Taupe_Install...jpg` (2500×3387, 6.1M) |
+| maya-50543 | Weathered Metals II | `MR-FV-16034-S_Weathered-Metals_Gilded-Pewter_Install...jpg` (2500×3781, 3.1M) |
+
+Full URLs in `resolved.tsv`. All HTTP-200, within Shopify limits, filename
+literally contains the pattern name.
+
+## GATED — this is a customer-facing Shopify write. Draft only; Steve approves.
+
+```
+# preview (read-only + local logo backup, no writes):
+node scripts/maya-romanoff-logohero-fix/apply.mjs
+
+# fix the hero (attach real image, poll READY, reorder to featured; leaves logo at pos 2):
+EXECUTE=1 node scripts/maya-romanoff-logohero-fix/apply.mjs
+
+# also remove the logo placeholder media (backed up locally first):
+EXECUTE=1 DELETE_LOGO=1 node scripts/maya-romanoff-logohero-fix/apply.mjs
+
+# canary one product first:
+EXECUTE=1 LIMIT=1 node scripts/maya-romanoff-logohero-fix/apply.mjs
+
+# undo:
+EXECUTE=1 node scripts/maya-romanoff-logohero-fix/undo.mjs
+```
+
+## Design (folds in Kimi second-model review, 2026-09-14)
+- attach → **poll status=READY** → reorder to featured. Never delete the logo
+  before the real image is READY (avoids a zero-usable-image window; aborts +
+  keeps logo on FAILED/timeout, e.g. a manufacturer hotlink block).
+- logo PNG binary + alt backed up locally to
+  `~/.claude/yolo-queue/restore-maps/tk11750-logo-backups/` (durable undo — a
+  deleted Shopify CDN url is not a durable restore source).
+- restore map: `~/.claude/yolo-queue/restore-maps/tk11750-maya-logohero.jsonl`.
+- guards: skips a product that already has a mayaromanoff.com image, or whose
+  logo media is gone (state changed).
diff --git a/scripts/maya-romanoff-logohero-fix/apply.mjs b/scripts/maya-romanoff-logohero-fix/apply.mjs
new file mode 100644
index 0000000..f72d82a
--- /dev/null
+++ b/scripts/maya-romanoff-logohero-fix/apply.mjs
@@ -0,0 +1,154 @@
+#!/usr/bin/env node
+/**
+ * TK-11750 — Maya Romanoff logo-hero image fix (GATED).
+ *
+ * 3 ACTIVE products on the live DW store (designer-laboratory-sandbox) carry a
+ * "mayalogohold" LOGO PLACEHOLDER as their sole/featured hero image — a real
+ * customer-facing defect. Real product install-shots were resolved feed-first
+ * from mayaromanoff.com, name-matched by pattern (see resolved.tsv), all 2500px
+ * HTTP-200 JPEGs.
+ *
+ * FLOW per product (folds in the Kimi second-model review, 2026-09-14):
+ *   1. Back up the logo PNG binary + alt LOCALLY (durable undo — a deleted
+ *      Shopify CDN url is NOT a durable restore source).
+ *   2. productCreateMedia -> attach the real image.
+ *   3. POLL media status until READY (abort + KEEP logo on FAILED/timeout — this
+ *      is how a manufacturer hotlink-block or fetch error can't leave a product
+ *      with zero usable image).
+ *   4. productReorderMedia -> real image to position 1 (becomes featured). The
+ *      logo drops to position 2 (harmless, still present — durable undo).
+ *   5. Only if DELETE_LOGO=1: productDeleteMedia(logo) AFTER the real image is
+ *      READY + featured (backup already saved in step 1).
+ *
+ * GATED: default DRY-RUN. Live write requires EXECUTE=1.
+ *   EXECUTE=1                 attach + reorder (fixes hero, leaves logo at pos 2)
+ *   EXECUTE=1 DELETE_LOGO=1   also delete the logo media (backed up first)
+ *   LIMIT=N                   cap rows (canary=1)
+ * Restore map: ~/.claude/yolo-queue/restore-maps/tk11750-maya-logohero.jsonl
+ * Undo: see undo.mjs (deletes the added media; re-attaches logo from local backup
+ *       via staged upload if it was deleted).
+ */
+import { gql } from '/Users/macstudio3/Projects/designerwallcoverings/scripts/lib/shopify.mjs';
+import { readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
+import { join, dirname } from 'path';
+import { homedir } from 'os';
+import { fileURLToPath } from 'url';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const EXECUTE = process.env.EXECUTE === '1';
+const DELETE_LOGO = process.env.DELETE_LOGO === '1';
+const LIMIT = parseInt(process.env.LIMIT || '0', 10) || Infinity;
+
+const MAPDIR = join(homedir(), '.claude/yolo-queue/restore-maps');
+const BAKDIR = join(MAPDIR, 'tk11750-logo-backups');
+mkdirSync(BAKDIR, { recursive: true });
+const MAP = join(MAPDIR, 'tk11750-maya-logohero.jsonl');
+
+const M_CREATE = `mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ ... on MediaImage { id status } } mediaUserErrors{ field message } } }`;
+const M_REORDER = `mutation($id:ID!,$moves:[MoveInput!]!){ productReorderMedia(id:$id, moves:$moves){ job{ id done } userErrors{ field message } } }`;
+const M_DELETE = `mutation($id:ID!,$ids:[ID!]!){ productDeleteMedia(productId:$id, mediaIds:$ids){ deletedMediaIds mediaUserErrors{ field message } } }`;
+const Q_STATUS = `query($id:ID!){ node(id:$id){ ... on MediaImage { id status image{ url } } } }`;
+const Q_PROD = `query($id:ID!){ product(id:$id){ id handle status featuredMedia{ ... on MediaImage { id } } media(first:20){ nodes{ ... on MediaImage { id status alt image{ url } } } } } }`;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function pollReady(mediaId, { timeoutMs = 120000, everyMs = 3000 } = {}) {
+  const t0 = Date.now();
+  while (Date.now() - t0 < timeoutMs) {
+    const r = await gql(Q_STATUS, { id: mediaId });
+    const st = r?.node?.status;
+    if (st === 'READY') return { ok: true, url: r.node.image?.url };
+    if (st === 'FAILED') return { ok: false, reason: 'FAILED' };
+    await sleep(everyMs);
+  }
+  return { ok: false, reason: 'TIMEOUT' };
+}
+
+const rows = readFileSync(join(HERE, 'resolved.tsv'), 'utf8').trim().split('\n')
+  .map(l => l.split('\t'))
+  .map(([handle, dw_sku, product_id, pattern, logo_media_id, logo_url, real_url]) =>
+    ({ handle, dw_sku, product_id, pattern, logo_media_id, logo_url, real_url }));
+
+console.log(`\n=== TK-11750 Maya Romanoff logo-hero fix — ${EXECUTE ? 'EXECUTE (LIVE)' : 'DRY-RUN'}${DELETE_LOGO ? ' +DELETE_LOGO' : ''} — ${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;
+
+  // Read current live state (guard: only act if the logo is still the media)
+  const pr = await gql(Q_PROD, { id: row.product_id });
+  const p = pr?.product;
+  if (!p) { console.log(`  ✗ ${row.dw_sku}: product not found`); failed++; continue; }
+  const media = p.media?.nodes || [];
+  const hasLogo = media.some(m => m.id === row.logo_media_id);
+  const hasReal = media.some(m => (m.image?.url || '').includes('assets.mayaromanoff.com'));
+  if (hasReal) { console.log(`  ⏭  ${row.dw_sku}: already has a mayaromanoff.com image — SKIP (guard)`); skipped++; continue; }
+  if (!hasLogo) { console.log(`  ⏭  ${row.dw_sku}: logo media ${row.logo_media_id} no longer present — SKIP (state changed)`); skipped++; continue; }
+
+  const logoNode = media.find(m => m.id === row.logo_media_id);
+  console.log(`  ${EXECUTE ? '▶' : '·'} ${row.dw_sku} "${row.pattern}" [${p.status}] ${p.id}`);
+  console.log(`       real:  ${row.real_url.split('/').pop()}`);
+  console.log(`       logo:  ${row.logo_media_id} (${(logoNode?.alt || '').slice(0,40)})`);
+
+  if (!EXECUTE) {
+    // still back up the logo binary in dry-run so the artifact exists for review
+    try {
+      const bin = await (await fetch(row.logo_url)).arrayBuffer();
+      const bak = join(BAKDIR, `${row.dw_sku}_logo.png`);
+      writeFileSync(bak, Buffer.from(bin));
+      console.log(`       [dry-run] logo backed up -> ${bak} (${bin.byteLength}B)`);
+    } catch (e) { console.log(`       [dry-run] logo backup failed: ${e.message}`); }
+    console.log('');
+    done++; continue;
+  }
+
+  // 1. backup logo binary + alt (durable undo)
+  let bakPath = null;
+  try {
+    const bin = await (await fetch(row.logo_url)).arrayBuffer();
+    bakPath = join(BAKDIR, `${row.dw_sku}_logo.png`);
+    writeFileSync(bakPath, Buffer.from(bin));
+  } catch (e) { console.log(`       ✗ logo backup failed, ABORT this row: ${e.message}`); failed++; continue; }
+
+  // 2. attach real image
+  const cr = await gql(M_CREATE, { id: p.id, media: [{ mediaContentType: 'IMAGE', originalSource: row.real_url, alt: `${row.pattern} — Maya Romanoff` }] });
+  const ue = cr?.productCreateMedia?.mediaUserErrors || [];
+  const newId = cr?.productCreateMedia?.media?.[0]?.id;
+  if (ue.length || !newId) { console.log(`       ✗ attach FAILED: ${JSON.stringify(ue) || JSON.stringify(cr)}`); failed++; continue; }
+  console.log(`       ✓ attached ${newId} — polling READY...`);
+
+  // 3. poll READY (abort + keep logo on FAILED/timeout)
+  const rdy = await pollReady(newId);
+  if (!rdy.ok) {
+    console.log(`       ✗ media ${rdy.reason} — deleting the bad media, KEEPING logo (no zero-image window)`);
+    await gql(M_DELETE, { id: p.id, ids: [newId] });
+    failed++; continue;
+  }
+  console.log(`       ✓ READY`);
+
+  // 4. reorder real image to position 1 (featured)
+  const rr = await gql(M_REORDER, { id: p.id, moves: [{ id: newId, newPosition: '0' }] });
+  const rue = rr?.productReorderMedia?.userErrors || [];
+  if (rue.length) { console.log(`       ⚠ reorder userErrors: ${JSON.stringify(rue)} (real image attached but not featured — review)`); }
+  else { console.log(`       ✓ real image -> position 1 (featured)`); }
+
+  // 5. optional: delete logo (backup already saved)
+  let logoDeleted = false;
+  if (DELETE_LOGO) {
+    const dr = await gql(M_DELETE, { id: p.id, ids: [row.logo_media_id] });
+    const due = dr?.productDeleteMedia?.mediaUserErrors || [];
+    if (due.length) console.log(`       ⚠ logo delete userErrors: ${JSON.stringify(due)}`);
+    else { logoDeleted = true; console.log(`       ✓ logo media deleted (backup: ${bakPath})`); }
+  }
+
+  appendFileSync(MAP, JSON.stringify({
+    ts: new Date().toISOString(), ticket: 'TK-11750', product_id: p.id, handle: row.handle,
+    dw_sku: row.dw_sku, pattern: row.pattern, new_media_id: newId, real_url: row.real_url,
+    logo_media_id: row.logo_media_id, logo_url: row.logo_url, logo_alt: logoNode?.alt || null,
+    logo_backup: bakPath, logo_deleted: logoDeleted,
+  }) + '\n');
+  console.log('');
+  done++;
+}
+console.log(`\n=== ${EXECUTE ? 'EXECUTED' : 'DRY-RUN'}: ${done} fixed/planned, ${skipped} skipped, ${failed} failed ===`);
+if (EXECUTE && done) console.log(`Restore map: ${MAP}`);
diff --git a/scripts/maya-romanoff-logohero-fix/resolved.tsv b/scripts/maya-romanoff-logohero-fix/resolved.tsv
new file mode 100644
index 0000000..3f605fd
--- /dev/null
+++ b/scripts/maya-romanoff-logohero-fix/resolved.tsv
@@ -0,0 +1,3 @@
+maya-50039	Maya-50040	gid://shopify/Product/7384903745587	Grand Scale Ajiro Chevron	gid://shopify/MediaImage/23919139749939	https://cdn.shopify.com/s/files/1/0015/4117/7456/files/mayalogohold_c6107e69-7fef-47d7-a193-27888b161694.png	https://assets.mayaromanoff.com/web/general_images/MR-AC2-4604_Grand-Scale-Ajiro-Chevron_Maple_Install_WEB_IS_RGB_V1_1.jpg
+maya-50179	Maya-50179	gid://shopify/Product/7384909611059	Beadazzled Geode	gid://shopify/MediaImage/23919162884147	https://cdn.shopify.com/s/files/1/0015/4117/7456/files/mayalogohold_474cd581-6b18-4f77-99e3-2bb820092acf.png	https://assets.mayaromanoff.com/web/general_images/BG-1554_Beadazzled-Geode_Metallic-Taupe_Install_WEB_IS_RGB_V1.jpg
+maya-50543	Maya-50543	gid://shopify/Product/7384926453811	Weathered Metals II	gid://shopify/MediaImage/23919244673075	https://cdn.shopify.com/s/files/1/0015/4117/7456/files/mayalogohold_57450251-fe91-45ab-9296-5ce8359cbbe5.png	https://assets.mayaromanoff.com/web/general_images/MR-FV-16034-S_Weathered-Metals_Gilded-Pewter_Install_WEB_IS_RGB_V1.jpg
diff --git a/scripts/maya-romanoff-logohero-fix/undo.mjs b/scripts/maya-romanoff-logohero-fix/undo.mjs
new file mode 100644
index 0000000..6ab527e
--- /dev/null
+++ b/scripts/maya-romanoff-logohero-fix/undo.mjs
@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+/**
+ * TK-11750 — reversible UNDO for apply.mjs.
+ * Reads the restore map and, per row: deletes the real media that was added, and
+ * — if the logo was deleted — re-attaches it (from its Shopify CDN url, which
+ * normally still resolves; the local backup under tk11750-logo-backups/ is the
+ * durable fallback if the CDN asset was purged, re-uploadable via stagedUploads).
+ * GATED: default DRY-RUN, EXECUTE=1 to run.
+ */
+import { gql } from '/Users/macstudio3/Projects/designerwallcoverings/scripts/lib/shopify.mjs';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import { homedir } from 'os';
+
+const EXECUTE = process.env.EXECUTE === '1';
+const MAP = join(homedir(), '.claude/yolo-queue/restore-maps/tk11750-maya-logohero.jsonl');
+const M_DELETE = `mutation($id:ID!,$ids:[ID!]!){ productDeleteMedia(productId:$id, mediaIds:$ids){ deletedMediaIds mediaUserErrors{ field message } } }`;
+const M_CREATE = `mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ ... on MediaImage { id status } } mediaUserErrors{ field message } } }`;
+
+let lines;
+try { lines = readFileSync(MAP, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l)); }
+catch { console.log('no restore map yet — nothing to undo'); process.exit(0); }
+
+console.log(`\n=== TK-11750 UNDO — ${EXECUTE ? 'EXECUTE (LIVE)' : 'DRY-RUN'} — ${lines.length} rows ===\n`);
+for (const r of lines) {
+  console.log(`  ${EXECUTE ? '▶' : '·'} ${r.dw_sku}: delete real ${r.new_media_id}${r.logo_deleted ? ' + re-attach logo' : ' (logo still present, auto-refeatures)'}`);
+  if (!EXECUTE) continue;
+  await gql(M_DELETE, { id: r.product_id, ids: [r.new_media_id] });
+  if (r.logo_deleted) {
+    const cr = await gql(M_CREATE, { id: r.product_id, media: [{ mediaContentType: 'IMAGE', originalSource: r.logo_url, alt: r.logo_alt || '' }] });
+    const ue = cr?.productCreateMedia?.mediaUserErrors || [];
+    if (ue.length) console.log(`     ⚠ logo re-attach failed from CDN (${JSON.stringify(ue)}) — use local backup ${r.logo_backup} via stagedUploads`);
+    else console.log('     ✓ logo re-attached');
+  }
+}
+console.log(`\n=== ${EXECUTE ? 'UNDONE' : 'DRY-RUN'} ===`);

← d681254 auto-data-snapshot: 2026-09-14T15:41:36 (1 data files) — dat  ·  back to Designerwallcoverings  ·  auto-data-snapshot: 2026-09-14T16:46:18 (1 data files) — ass ef5c6f2 →