[object Object]

← back to Designer Wallcoverings

auto-save: 2026-06-23T11:51:26 (5 files) — shopify/scripts/cadence/data/cadence-cursor.json shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json shopify/scripts/tres-tintas-room-settings.js shopify/tres-tintas-spec-backups/_done.ledger shopify/scripts/tt-room-dbg2.js

7467dd91b1b1acf4407ef39a39c6ccf19ba313a6 · 2026-06-23 11:51:31 -0700 · Steve Abrams

Files touched

Diff

commit 7467dd91b1b1acf4407ef39a39c6ccf19ba313a6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 23 11:51:31 2026 -0700

    auto-save: 2026-06-23T11:51:26 (5 files) — shopify/scripts/cadence/data/cadence-cursor.json shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json shopify/scripts/tres-tintas-room-settings.js shopify/tres-tintas-spec-backups/_done.ledger shopify/scripts/tt-room-dbg2.js
---
 shopify/scripts/cadence/data/cadence-cursor.json   |   2 +-
 .../cadence/data/cadence-plan-am-2026-06-23.json   | 273 +--------------------
 shopify/scripts/tres-tintas-room-settings.js       |  10 +-
 shopify/scripts/tt-room-dbg2.js                    | 175 +++++++++++++
 shopify/tres-tintas-spec-backups/_done.ledger      | 245 ++++++++++++++++++
 5 files changed, 435 insertions(+), 270 deletions(-)

diff --git a/shopify/scripts/cadence/data/cadence-cursor.json b/shopify/scripts/cadence/data/cadence-cursor.json
index c9522b84..d826dd45 100644
--- a/shopify/scripts/cadence/data/cadence-cursor.json
+++ b/shopify/scripts/cadence/data/cadence-cursor.json
@@ -1,5 +1,5 @@
 {
- "idx": 0,
+ "idx": 6,
  "lastSlot": "am",
  "lastRun": "2026-06-23"
 }
\ No newline at end of file
diff --git a/shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json
index e02a935d..95b93dd8 100644
--- a/shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json
+++ b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-23.json
@@ -1,272 +1,11 @@
 [
  {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005011",
-  "cost": 156,
-  "retail": 282.35,
+  "vendor": "Romo",
+  "dw_sku": "DWRM-240001",
+  "cost": 467.502,
+  "retail": 846.16,
   "sample": "4.25",
-  "title": "Padmasalis Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005012",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Kente Green, Green Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005013",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Kente Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005014",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Kente Wine, Wine Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005015",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Kente Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005016",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Blue, Blue Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005017",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005018",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Clay, Clay Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005019",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Green, Green Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005020",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Lilac, Lilac Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005021",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Uzbek Cream, Cream Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005022",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Huipil Blue, Blue Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005023",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Huipil Red, Red Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005024",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Huipil Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005025",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Huipil Ink, Ink Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005026",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Huipil Wine, Wine Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005027",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Sumba Cream, Cream Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005028",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Sumba White, White Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005029",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Sumba Green, Green Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005030",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Sumba Navy, Navy Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005031",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Llengües Green, Green Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005032",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Llengües Blue, Blue Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005033",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Llengües Cream, Cream Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005034",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Llengües Black, Black Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005035",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Llengües Wine, Wine Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005036",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Abr Blue, Blue Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005037",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Abr Cream, Cream Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005038",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Abr Green, Green Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005039",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Abr Wine, Wine Wallcoverings | Newwall",
-  "willActivate": false
- },
- {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005040",
-  "cost": 156,
-  "retail": 282.35,
-  "sample": "4.25",
-  "title": "Abr Black, Black Wallcoverings | Newwall",
-  "willActivate": false
+  "title": "Abaca, Alabaster Wallcoverings | Romo",
+  "willActivate": true
  }
 ]
\ No newline at end of file
diff --git a/shopify/scripts/tres-tintas-room-settings.js b/shopify/scripts/tres-tintas-room-settings.js
index 39b12c99..96f11af0 100644
--- a/shopify/scripts/tres-tintas-room-settings.js
+++ b/shopify/scripts/tres-tintas-room-settings.js
@@ -108,7 +108,9 @@ async function uploadImage(productId, filePath, alt) {
 async function main() {
   if (!TOKEN || !RTOKEN) throw new Error('missing SHOPIFY_DRAFT_TOKEN or REPLICATE_API_TOKEN');
   console.log(`MODE: ${APPLY ? 'APPLY (live)' : 'DRY RUN'}  room=${ROOM}${LIMIT !== Infinity ? `  LIMIT=${LIMIT}` : ''}  est $${COST_PER_IMG}/img`);
-  const q = `query($c:String){products(first:60,query:"vendor:'Tres Tintas'",after:$c){pageInfo{hasNextPage endCursor}
+  // first:100 (matches the desc/spec scripts that enumerate all 592 cleanly).
+  // first:60 hit a Shopify frozen-endCursor pagination bug -> only saw 120.
+  const q = `query($c:String){products(first:100,query:"vendor:'Tres Tintas'",after:$c){pageInfo{hasNextPage endCursor}
     edges{node{id title images(first:10){edges{node{url}}}} } }}`;
 
   let cursor = null, scanned = 0, need = 0, processed = 0, ok = 0, errs = 0, spend = 0;
@@ -157,7 +159,11 @@ async function main() {
         await sleep(500);
       }
     }
-    if (conn.pageInfo.hasNextPage && processed < LIMIT) cursor = conn.pageInfo.endCursor; else break;
+    // Shopify can return hasNextPage:true with a FROZEN endCursor at the
+    // result-set boundary -> infinite same-page loop (the 2026-06-23 wedge).
+    // Break if the cursor doesn't advance.
+    const next = conn.pageInfo.endCursor;
+    if (conn.pageInfo.hasNextPage && processed < LIMIT && next && next !== cursor) cursor = next; else break;
   }
 
   console.log(`\n--- SUMMARY ---`);
diff --git a/shopify/scripts/tt-room-dbg2.js b/shopify/scripts/tt-room-dbg2.js
new file mode 100644
index 00000000..48694948
--- /dev/null
+++ b/shopify/scripts/tt-room-dbg2.js
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+/**
+ * tres-tintas-room-settings.js
+ *
+ * Generates ONE photorealistic room-setting image per Tres Tintas product that
+ * lacks one, using the EXACT catalog pattern (Replicate SDXL inpainting with the
+ * pattern wall mask-LOCKED so SDXL can never alter the pattern — it only renders
+ * the furniture/floor/light around it).
+ *
+ * Pipeline per product:
+ *   1. Pull the product's pattern image + repeat (custom.pattern_repeat) from Shopify.
+ *   2. Build a one-point-perspective room: exact pattern tiled on the back wall
+ *      (with molding frame + baseboard), neutral floor/ceiling/side walls.
+ *      Mask = black over the pattern wall (keep), white elsewhere (generate).
+ *      -> delegates to build-room-composite.py (PIL).
+ *   3. Replicate SDXL inpainting renders the room around the locked pattern.
+ *   4. Upload the result as a 2nd product image (alt "Room Setting").
+ *
+ * Idempotent (ledger), error-tolerant (one product's failure is skipped, not
+ * fatal), shows a running $ total. DRY RUN by default. APPLY=1 to write.
+ *
+ *   LIMIT=1 node tres-tintas-room-settings.js          # dry run, 1
+ *   LIMIT=1 APPLY=1 node tres-tintas-room-settings.js  # write 1 (pilot)
+ *   APPLY=1 node tres-tintas-room-settings.js          # full ~557 run
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-01';
+const APPLY = process.env.APPLY === '1';
+const LIMIT = process.env.LIMIT ? parseInt(process.env.LIMIT, 10) : Infinity;
+const ROOM = process.env.ROOM || 'living_room';
+const REPLICATE_VERSION = 'a5b13068cc81a89a4fbeefeccc774869fcb34df4dbc92c1555e0f2771d49dde7';
+const COST_PER_IMG = 0.02;            // ~$0.02 per SDXL inpaint run (observed)
+const PY = fs.existsSync('/opt/homebrew/bin/python3') ? '/opt/homebrew/bin/python3' : 'python3';
+const COMPOSITE_PY = path.join(__dirname, 'build-room-composite.py');
+
+function envval(key) {
+  const env = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
+  const m = env.match(new RegExp(`^${key}=["']?([^"'\\n]+)`, 'm'));
+  return m ? m[1].trim() : null;
+}
+const TOKEN = envval('SHOPIFY_DRAFT_TOKEN');
+const RTOKEN = envval('REPLICATE_API_TOKEN');
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+
+const TMP = '/tmp/tt-rooms';
+fs.mkdirSync(TMP, { recursive: true });
+const BACKUP_DIR = path.join(__dirname, '..', 'tres-tintas-room-backups');
+const LEDGER = path.join(BACKUP_DIR, '_done.ledger');
+fs.mkdirSync(BACKUP_DIR, { recursive: true });
+const done = new Set(fs.existsSync(LEDGER) ? fs.readFileSync(LEDGER, 'utf8').split('\n').filter(Boolean) : []);
+
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+// Per-request timeout so a half-open socket can never wedge the batch (the
+// 2026-06-23 25-min hang). Default 60s; uploads/large posts get more headroom.
+async function fetchRetry(url, opts, tries = 3, timeoutMs = 60000) {
+  let e;
+  for (let i = 0; i < tries; i++) {
+    try {
+      return await fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
+    } catch (err) { e = err; await sleep(800 * (i + 1)); }
+  }
+  throw e;
+}
+async function gql(query, variables) {
+  // Route through fetchRetry so the paginated scan can't wedge on a half-open
+  // socket (the bare-fetch hole that hung the 2026-06-23 relaunch with 0 output).
+  const r = await fetchRetry(`https://${STORE}/admin/api/${API}/graphql.json`, { method: 'POST', headers: H, body: JSON.stringify({ query, variables }) });
+  const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
+}
+
+// inches from "23.6 in / 60 cm" -> 23.6 ; default 19.7
+function repeatInches(v) { const m = /([\d.]+)\s*in/i.exec(v || ''); return m ? parseFloat(m[1]) : 19.7; }
+
+async function replicateInpaint(initPath, maskPath, outPath) {
+  const dataUri = (p) => 'data:image/png;base64,' + fs.readFileSync(p).toString('base64');
+  const body = { version: REPLICATE_VERSION, input: {
+    image: dataUri(initPath), mask: dataUri(maskPath),
+    prompt: 'photorealistic modern living room interior, comfortable fabric sofa, wooden coffee table, area rug, floor lamps, large window with soft natural daylight, the back feature wall has geometric patterned wallcovering framed by white crown molding and baseboard, warm professional architectural interior photography, high detail, realistic shadows and lighting',
+    negative_prompt: 'blurry, low quality, distorted, cartoon, painting, sketch, oversaturated, warped walls, extra walls, text, watermark',
+    strength: 0.99, steps: 30, guidance_scale: 8, num_outputs: 1 } };
+  let j = await (await fetchRetry('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { Authorization: `Bearer ${RTOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).json();
+  if (j.error) throw new Error('replicate create: ' + JSON.stringify(j.error));
+  const url = j.urls.get;
+  const deadline = Date.now() + 5 * 60 * 1000; // hard 5-min wall-clock cap per prediction
+  for (let i = 0; i < 90; i++) {
+    if (Date.now() > deadline) throw new Error('replicate wall-clock timeout');
+    await sleep(2000);
+    const pr = await (await fetchRetry(url, { headers: { Authorization: `Bearer ${RTOKEN}` } })).json();
+    if (pr.status === 'succeeded') { const out = Array.isArray(pr.output) ? pr.output[0] : pr.output; const img = await (await fetchRetry(out, {}, 3, 120000)).arrayBuffer(); fs.writeFileSync(outPath, Buffer.from(img)); return; }
+    if (pr.status === 'failed' || pr.status === 'canceled') throw new Error('replicate ' + pr.status + ': ' + JSON.stringify(pr.error));
+  }
+  throw new Error('replicate poll timeout');
+}
+
+async function uploadImage(productId, filePath, alt) {
+  const b64 = fs.readFileSync(filePath).toString('base64');
+  const r = await fetchRetry(`https://${STORE}/admin/api/${API}/products/${productId}/images.json`, {
+    method: 'POST', headers: H, body: JSON.stringify({ image: { attachment: b64, filename: `${productId}-room-${ROOM}.png`, alt } }) }, 3, 120000);
+  const j = await r.json();
+  if (!j.image) throw new Error('shopify upload: ' + JSON.stringify(j.errors || j));
+  return j.image.id;
+}
+
+async function main() {
+  if (!TOKEN || !RTOKEN) throw new Error('missing SHOPIFY_DRAFT_TOKEN or REPLICATE_API_TOKEN');
+  console.log(`MODE: ${APPLY ? 'APPLY (live)' : 'DRY RUN'}  room=${ROOM}${LIMIT !== Infinity ? `  LIMIT=${LIMIT}` : ''}  est $${COST_PER_IMG}/img`);
+  // first:100 (matches the desc/spec scripts that enumerate all 592 cleanly).
+  // first:60 hit a Shopify frozen-endCursor pagination bug -> only saw 120.
+  const q = `query($c:String){products(first:100,query:"vendor:'Tres Tintas'",after:$c){pageInfo{hasNextPage endCursor}
+    edges{node{id title images(first:10){edges{node{url}}}} } }}`;
+
+  let cursor = null, scanned = 0, need = 0, processed = 0, ok = 0, errs = 0, spend = 0;
+  while (true) {
+    const conn = (await gql(q, { cursor })).products;
+    for (const e of conn.edges) {
+      const n = e.node; scanned++;
+      const id = n.id.split('/').pop();
+      const imgs = n.images.edges.map(x => x.node.url);
+      const hasRoom = imgs.some(u => /interior|_int|room|ambiente|setting/i.test(u));
+      if (imgs.length > 1 || hasRoom) continue;   // already has a 2nd/room image
+      need++;
+      if (done.has(id)) continue;
+      if (processed >= LIMIT) continue;
+
+      const patternUrl = imgs[0];
+      if (!patternUrl) { console.log(`  !! ${id} no pattern image — SKIP`); continue; }
+
+      try {
+        // fetch repeat for tile scale
+        const mfRes = await fetchRetry(`https://${STORE}/admin/api/${API}/products/${id}/metafields.json`, { headers: H });
+        const mfs = (await mfRes.json()).metafields || [];
+        const rep = repeatInches((mfs.find(m => m.namespace === 'custom' && m.key === 'pattern_repeat') || {}).value);
+
+        // download pattern
+        const patPath = `${TMP}/${id}-pat.jpg`;
+        const pbuf = await (await fetchRetry(patternUrl, {})).arrayBuffer();
+        fs.writeFileSync(patPath, Buffer.from(pbuf));
+
+        const initPath = `${TMP}/${id}-init.png`, maskPath = `${TMP}/${id}-mask.png`, outPath = `${TMP}/${id}-room.png`;
+        execFileSync(PY, [COMPOSITE_PY, patPath, initPath, maskPath, String(rep)], { stdio: 'pipe' });
+
+        processed++;
+        if (!APPLY) { console.log(`  [dry] ${id} ${n.title}  repeat=${rep}in  -> would inpaint + upload`); continue; }
+
+        await replicateInpaint(initPath, maskPath, outPath);
+        spend += COST_PER_IMG;
+        const imgId = await uploadImage(id, outPath, `Room Setting - ${n.title.replace(/\s*\|.*$/, '')}`);
+        ok++;
+        fs.appendFileSync(LEDGER, id + '\n'); done.add(id);
+        console.log(`  ✓ ${id} ${n.title}  img=${imgId}  [${ok} done · ~$${spend.toFixed(2)}]`);
+        await sleep(300);
+      } catch (err) {
+        errs++;
+        console.log(`  ✗ ${id} ${n.title}: ${err.message} — SKIP (retry next run)`);
+        await sleep(500);
+      }
+    }
+    // Shopify can return hasNextPage:true with a FROZEN endCursor at the
+    // result-set boundary -> infinite same-page loop (the 2026-06-23 wedge).
+    // Break if the cursor doesn't advance.
+    const next = conn.pageInfo.endCursor;
+    console.error("DBG page: edges="+conn.edges.length+" hasNext="+conn.pageInfo.hasNextPage+" scanned="+scanned+" frozen="+(next===cursor));
+    if (conn.pageInfo.hasNextPage && processed < LIMIT && next && next !== cursor) cursor = next; else break;
+  }
+
+  console.log(`\n--- SUMMARY ---`);
+  console.log(`scanned: ${scanned} | need-room: ${need} | processed: ${processed} | uploaded: ${ok} | errors: ${errs}`);
+  console.log(`Replicate spend: ~$${spend.toFixed(2)} (${ok} imgs × $${COST_PER_IMG})`);
+  if (APPLY) console.log(`ledger -> ${LEDGER}`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/shopify/tres-tintas-spec-backups/_done.ledger b/shopify/tres-tintas-spec-backups/_done.ledger
index a9bbf4f4..c4569b01 100644
--- a/shopify/tres-tintas-spec-backups/_done.ledger
+++ b/shopify/tres-tintas-spec-backups/_done.ledger
@@ -307,3 +307,248 @@
 7866348142643
 7866348175411
 7866348208179
+7866348240947
+7866348339251
+7866348437555
+7866348470323
+7866348503091
+7866348535859
+7866348568627
+7866348601395
+7866348634163
+7866348666931
+7866348699699
+7866348732467
+7866348765235
+7866348798003
+7866348830771
+7866348863539
+7866348929075
+7866348961843
+7866348994611
+7866349027379
+7866349060147
+7866349092915
+7866349125683
+7866349158451
+7866349191219
+7866349223987
+7866349256755
+7866349289523
+7866349322291
+7866349355059
+7866349387827
+7866349420595
+7866349453363
+7866349486131
+7866349518899
+7866349551667
+7866349584435
+7866349617203
+7866349649971
+7866349682739
+7866349715507
+7866349781043
+7866349813811
+7866349846579
+7866349879347
+7866349912115
+7866349944883
+7866349977651
+7866350010419
+7866350043187
+7866350075955
+7866350108723
+7866350141491
+7866350174259
+7866350207027
+7866350239795
+7866350272563
+7866350305331
+7866350338099
+7866350370867
+7866350403635
+7866350436403
+7866350469171
+7866350501939
+7866350534707
+7866350567475
+7866350600243
+7866350633011
+7866350665779
+7866350698547
+7866350731315
+7866350764083
+7866350796851
+7866350829619
+7866350862387
+7866350895155
+7866350927923
+7866350960691
+7866350993459
+7866351026227
+7866351058995
+7866351124531
+7866351157299
+7866351190067
+7866351222835
+7866351255603
+7866351616051
+7866352074803
+7866352566323
+7866352664627
+7866352730163
+7866352762931
+7866352795699
+7866352828467
+7866352861235
+7866352894003
+7866352926771
+7866352959539
+7866352992307
+7866353057843
+7866353090611
+7866353123379
+7866353156147
+7866353188915
+7866353221683
+7866353254451
+7866353287219
+7866353319987
+7866353352755
+7866353385523
+7866353418291
+7866353451059
+7866353483827
+7866353516595
+7866353549363
+7866353582131
+7866353614899
+7866353647667
+7866353680435
+7866353713203
+7866353745971
+7866353778739
+7866353811507
+7866353844275
+7866353909811
+7866353942579
+7866353975347
+7866354008115
+7866354040883
+7866354073651
+7866354106419
+7866354139187
+7866354171955
+7866354237491
+7866354270259
+7866354303027
+7866354335795
+7866354368563
+7866354401331
+7866354434099
+7866354466867
+7866354499635
+7866354532403
+7866354565171
+7866354597939
+7866354630707
+7866354663475
+7866354696243
+7866354729011
+7866354761779
+7866354794547
+7866354827315
+7866354860083
+7866354892851
+7866354925619
+7866354958387
+7866354991155
+7866355023923
+7866355056691
+7866355089459
+7866355122227
+7866355154995
+7866355220531
+7866355286067
+7866355318835
+7866355351603
+7866355384371
+7866355417139
+7866355449907
+7866355482675
+7866355515443
+7866355548211
+7866355580979
+7866355613747
+7866355646515
+7866355679283
+7866355712051
+7866355744819
+7866355777587
+7866355810355
+7866355843123
+7866355875891
+7866355908659
+7866355941427
+7866355974195
+7866356006963
+7866356039731
+7866356072499
+7866356105267
+7866356138035
+7866356170803
+7866356203571
+7866356236339
+7866356269107
+7866356301875
+7866356334643
+7866356367411
+7866356400179
+7866356432947
+7866356465715
+7866356498483
+7866356531251
+7866356564019
+7866356596787
+7866356629555
+7866356662323
+7866356695091
+7866356727859
+7866356760627
+7866356793395
+7866356826163
+7866356858931
+7866356891699
+7866356924467
+7866356957235
+7866356990003
+7866357022771
+7866357055539
+7866357088307
+7866357121075
+7866357153843
+7866357186611
+7866357252147
+7866357284915
+7866357317683
+7866357350451
+7866357383219
+7866357415987
+7866357448755
+7866357481523
+7866357514291
+7866357547059
+7866357579827
+7866357612595
+7866357645363
+7866357678131
+7866357710899
+7866357743667
+7866357776435
+7866357809203
+7866357841971
+7866357874739
+7866357907507
+7866357940275
+7866357973043

← 90af3941 Route room generator GraphQL scan through timeout-guarded fe  ·  back to Designer Wallcoverings  ·  Fix Tres Tintas room generator infinite-loop: GraphQL var na b2a25a5b →