← 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
A scripts/maya-romanoff-logohero-fix/README.mdA scripts/maya-romanoff-logohero-fix/apply.mjsA scripts/maya-romanoff-logohero-fix/resolved.tsvA scripts/maya-romanoff-logohero-fix/undo.mjs
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 →