[object Object]

← back to Rebel Walls Push

Relabel 2 mural-typed Rebel Walls products Single Roll -> Mural (per m²) (TK-10029, Steve-approved); rollback map committed

56889417288327056b2dabc64b4a5b498052fcfa · 2026-09-03 14:01:00 -0700 · Steve Abrams

Files touched

Diff

commit 56889417288327056b2dabc64b4a5b498052fcfa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 14:01:00 2026 -0700

    Relabel 2 mural-typed Rebel Walls products Single Roll -> Mural (per m²) (TK-10029, Steve-approved); rollback map committed
---
 ...2flagged-rollback-2026-09-03T21-00-30-365Z.json | 29 ++++++++
 scripts/relabel-2-flagged.js                       | 79 ++++++++++++++++++++++
 2 files changed, 108 insertions(+)

diff --git a/data/relabel-2flagged-rollback-2026-09-03T21-00-30-365Z.json b/data/relabel-2flagged-rollback-2026-09-03T21-00-30-365Z.json
new file mode 100644
index 0000000..8f0dce4
--- /dev/null
+++ b/data/relabel-2flagged-rollback-2026-09-03T21-00-30-365Z.json
@@ -0,0 +1,29 @@
+{
+  "ticket": "TK-10029",
+  "when": "2026-09-03T21:00:30.366Z",
+  "dry": false,
+  "from": "Single Roll",
+  "to": "Mural (per m²)",
+  "rows": [
+    {
+      "sku": "DWRW-451820",
+      "product_id": "gid://shopify/Product/7814966280243",
+      "title": "Waves Magenta | Rebel Walls",
+      "option_id": "gid://shopify/ProductOption/9960398454835",
+      "option_name": "Title",
+      "ov_id": "gid://shopify/ProductOptionValue/4445018685491",
+      "old_value": "Single Roll",
+      "new_value": "Mural (per m²)"
+    },
+    {
+      "sku": "DWRW-450057",
+      "product_id": "gid://shopify/Product/7815342555187",
+      "title": "Street Art Brick Wall | Rebel Walls",
+      "option_id": "gid://shopify/ProductOption/9961055813683",
+      "option_name": "Title",
+      "ov_id": "gid://shopify/ProductOptionValue/4445018619955",
+      "old_value": "Single Roll",
+      "new_value": "Mural (per m²)"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/scripts/relabel-2-flagged.js b/scripts/relabel-2-flagged.js
new file mode 100644
index 0000000..bdca2c4
--- /dev/null
+++ b/scripts/relabel-2-flagged.js
@@ -0,0 +1,79 @@
+#!/usr/bin/env node
+/**
+ * TK-10029 (Steve GO): relabel EXACTLY the 2 flagged mural-typed products that were
+ * hiding under a "Single Roll" label — rename that one option value to the canonical
+ * "Mural (per m²)". Nothing else in the catalog is touched. Writes a rollback map
+ * BEFORE mutating so the change is one-command reversible, then verifies.
+ *
+ * Dry-run by default. Pass --apply to execute.  Usage:
+ *   node scripts/relabel-2-flagged.js            # dry-run
+ *   node scripts/relabel-2-flagged.js --apply    # live (Steve-approved)
+ */
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const SECRETS_ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-10';
+const TOKEN = (() => { const e = fs.readFileSync(SECRETS_ENV, 'utf8'); const m = e.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m); if (!m) throw new Error('token'); return m[1].trim(); })();
+const DRY = !process.argv.includes('--apply');
+const CANONICAL = 'Mural (per m²)';
+const OLD = 'Single Roll';
+const TARGET_SKUS = ['DWRW-451820', 'DWRW-450057']; // Waves Magenta, Street Art Brick Wall
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function gqlOnce(q, v) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query: q, variables: v || {} });
+    const req = https.request({ hostname: DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
+      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(`non-JSON ${res.statusCode}`)); } }); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+async function gql(q, v) { let last; for (let a = 1; a <= 8; a++) { try { const r = await gqlOnce(q, v); if (r && r.data) return r; last = new Error('no data: ' + JSON.stringify(r.errors || r).slice(0, 120)); } catch (e) { last = e; } if (a < 8) await sleep(Math.min(10000, 500 * 2 ** (a - 1))); } throw last; }
+
+(async () => {
+  console.log(`[relabel-2] mode=${DRY ? 'DRY' : 'LIVE'}  "${OLD}" -> "${CANONICAL}"  targets=${TARGET_SKUS.join(', ')}\n`);
+  const rollbackRows = [];
+  let done = 0;
+  for (const sku of TARGET_SKUS) {
+    const r = await gql(`query{ products(first:1, query:"vendor:Rebel Walls sku:${sku}"){ edges{ node{ id title productType
+      options{ id name optionValues{ id name } } } } } }`);
+    const n = r.data.products.edges[0]?.node;
+    if (!n) { console.log(`  ✗ ${sku}: NOT FOUND — skipping`); continue; }
+    const opt = n.options.find(o => o.optionValues.some(v => v.name === OLD));
+    if (!opt) { console.log(`  ✗ ${sku} (${n.title}): no "${OLD}" value present (already relabeled?) — skipping`); continue; }
+    const ov = opt.optionValues.find(v => v.name === OLD);
+    console.log(`  → ${sku}  ${n.title}  [${n.productType}]  option "${opt.name}": "${OLD}" → "${CANONICAL}"`);
+    rollbackRows.push({ sku, product_id: n.id, title: n.title, option_id: opt.id, option_name: opt.name, ov_id: ov.id, old_value: OLD, new_value: CANONICAL });
+    if (DRY) { done++; continue; }
+    const res = await gql(`mutation($productId:ID!,$option:OptionUpdateInput!,$ovs:[OptionValueUpdateInput!]!){
+      productOptionUpdate(productId:$productId, option:$option, optionValuesToUpdate:$ovs){ userErrors{ field message } } }`,
+      { productId: n.id, option: { id: opt.id, name: opt.name }, ovs: [{ id: ov.id, name: CANONICAL }] });
+    const errs = res.data?.productOptionUpdate?.userErrors;
+    if (errs && errs.length) { console.log(`    ✗ ERR: ${errs.map(e => e.message).join('; ')}`); continue; }
+    console.log(`    ✓ relabeled`);
+    done++; await sleep(300);
+  }
+
+  // durable rollback map (written in both DRY + LIVE so the plan is auditable)
+  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+  const mapPath = path.join(__dirname, '..', 'data', `relabel-2flagged-rollback-${stamp}.json`);
+  fs.writeFileSync(mapPath, JSON.stringify({ ticket: 'TK-10029', when: new Date().toISOString(), dry: DRY, from: OLD, to: CANONICAL, rows: rollbackRows }, null, 2));
+  console.log(`\n[relabel-2] ${DRY ? 'would relabel' : 'relabeled'} ${done}/${TARGET_SKUS.length}. rollback map: ${mapPath}`);
+
+  if (!DRY) {
+    // verify
+    let ok = 0;
+    for (const row of rollbackRows) {
+      const r = await gql(`query{ products(first:1, query:"vendor:Rebel Walls sku:${row.sku}"){ edges{ node{ options{ optionValues{ name } } } } } }`);
+      const vals = r.data.products.edges[0]?.node.options.flatMap(o => o.optionValues.map(v => v.name)) || [];
+      const good = vals.includes(CANONICAL) && !vals.includes(OLD);
+      console.log(`  verify ${row.sku}: ${good ? '✓ Mural (per m²) present, Single Roll gone' : '✗ ' + JSON.stringify(vals)}`);
+      if (good) ok++;
+    }
+    console.log(`[relabel-2] verified ${ok}/${rollbackRows.length}`);
+  }
+})().catch(e => { console.error('[relabel-2] FATAL', e.message); process.exit(1); });

← 3fda4bb Add read-only confirm for soft roll/bolt cohort (TK-10029 DT  ·  back to Rebel Walls Push  ·  auto-data-snapshot: 2026-09-03T14:29:24 (1 data files) — dat 5b92c13 →