← back to Designerwallcoverings
TK-12227: local mirror step + public verify (browser/Googlebot headers)
ff13b1762b15859dba2f00c25845300aa470de74 · 2026-09-25 09:37:54 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Files touched
A scripts/palm-grove-promote/mirror.mjsM scripts/palm-grove-promote/rollback.mjsA scripts/palm-grove-promote/verify.mjs
Diff
commit ff13b1762b15859dba2f00c25845300aa470de74
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 25 09:37:54 2026 -0700
TK-12227: local mirror step + public verify (browser/Googlebot headers)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---
scripts/palm-grove-promote/mirror.mjs | 29 ++++++++++++++++++++
scripts/palm-grove-promote/rollback.mjs | 4 ++-
scripts/palm-grove-promote/verify.mjs | 48 +++++++++++++++++++++++++++++++++
3 files changed, 80 insertions(+), 1 deletion(-)
diff --git a/scripts/palm-grove-promote/mirror.mjs b/scripts/palm-grove-promote/mirror.mjs
new file mode 100644
index 0000000..6e65fb4
--- /dev/null
+++ b/scripts/palm-grove-promote/mirror.mjs
@@ -0,0 +1,29 @@
+// TK-12227 — mirror one pair's live status into LOCAL dw_unified (host=/tmp) only. No Kamatera writes.
+// Usage: node mirror.mjs <restore-map.json> (records pre values into map.mirror for rollback)
+import fs from 'node:fs';
+import { execFileSync } from 'node:child_process';
+import { snap } from './snapshot.mjs';
+const file = process.argv[2];
+const m = JSON.parse(fs.readFileSync(file, 'utf8'));
+const psql = (sql) => execFileSync('psql', ['host=/tmp dbname=dw_unified', '-At', '-v', 'ON_ERROR_STOP=1', '-F', '\t', '-c', sql], { encoding: 'utf8' }).trim();
+const COLS = ['status', 'image_url', 'variant_count', 'has_product_variant', 'online_store_published'];
+const draftId = m.pre.draft.id, twinId = m.pre.twin.id;
+const d = await snap(draftId.split('/').pop()), t = await snap(twinId.split('/').pop());
+const pre = [];
+for (const gid of [draftId, twinId]) {
+ const rows = psql(`select id, ${COLS.join(', ')} from shopify_products where shopify_id='${gid}'`).split('\n').filter(Boolean);
+ if (rows.length !== 1) { console.error(`expected 1 mirror row for ${gid}, got ${rows.length}`); process.exit(3); }
+ const [id, ...vals] = rows[0].split('\t');
+ pre.push({ id: Number(id), shopify_id: gid, ...Object.fromEntries(COLS.map((c, i) => [c, vals[i] === '' ? null : vals[i]])) });
+}
+m.mirror = { pre, at: new Date().toISOString() };
+fs.writeFileSync(file, JSON.stringify(m, null, 2));
+const q = s => s == null ? 'NULL' : `'${String(s).replace(/'/g, "''")}'`;
+const img = d.featuredImage?.url || null;
+const out = execFileSync('psql', ['host=/tmp dbname=dw_unified', '-v', 'ON_ERROR_STOP=1', '-f', '-'], { encoding: 'utf8', input: `begin;
+update shopify_products set status=${q(d.status)}, image_url=${q(img)}, variant_count=${d.variants.nodes.length}, has_product_variant=true, online_store_published=true where id=${pre[0].id};
+update shopify_products set status=${q(t.status)} where id=${pre[1].id};
+commit;` }).trim();
+if (!/UPDATE 1\s+UPDATE 1\s+COMMIT/.test(out)) { console.error('mirror row counts unexpected:', out); process.exit(4); }
+console.log('psql:', out.replace(/\n/g, ' | '));
+console.log(psql(`select id, handle, status, variant_count, (image_url is not null) img, has_product_variant, online_store_published from shopify_products where id in (${pre[0].id},${pre[1].id}) order by id`));
diff --git a/scripts/palm-grove-promote/rollback.mjs b/scripts/palm-grove-promote/rollback.mjs
index b286c7e..8fe1968 100644
--- a/scripts/palm-grove-promote/rollback.mjs
+++ b/scripts/palm-grove-promote/rollback.mjs
@@ -35,8 +35,10 @@ for (const [label, q, v] of ops) {
}
if (APPLY && m.mirror?.pre) {
for (const row of m.mirror.pre) {
+ const q = s => s == null ? 'NULL' : `'${String(s).replace(/'/g, "''")}'`;
+ const sets = Object.entries(row).filter(([k]) => !['id', 'shopify_id'].includes(k)).map(([k, v]) => `${k}=${k === 'variant_count' && v != null ? Number(v) : (k.startsWith('has_') || k === 'online_store_published') && v != null ? (v === 't' || v === true) : q(v)}`).join(', ');
execFileSync('psql', ['host=/tmp dbname=dw_unified', '-v', 'ON_ERROR_STOP=1', '-c',
- `update shopify_products set status='${row.status}' where id=${Number(row.id)}`], { stdio: 'inherit' });
+ `update shopify_products set ${sets} where id=${Number(row.id)}`], { stdio: 'inherit' });
}
}
console.log(APPLY ? 'rollback applied' : 'dry-run only (add --apply)');
diff --git a/scripts/palm-grove-promote/verify.mjs b/scripts/palm-grove-promote/verify.mjs
new file mode 100644
index 0000000..98e2416
--- /dev/null
+++ b/scripts/palm-grove-promote/verify.mjs
@@ -0,0 +1,48 @@
+// TK-12227 — public verification of one pair + "other pairs untouched" check.
+// Usage: node verify.mjs <MFR> [--done LN40201,LN40202] (done = pairs already promoted)
+import fs from 'node:fs';
+import { PAIRS, snap } from './snapshot.mjs';
+const SITE = 'https://www.designerwallcoverings.com';
+const mfr = process.argv[2];
+const doneList = (process.argv.includes('--done') ? process.argv[process.argv.indexOf('--done') + 1] : '').split(',').filter(Boolean);
+const pre = JSON.parse(fs.readFileSync(new URL('./snapshot-pre.json', import.meta.url)));
+const out = { mfr, at: new Date().toISOString(), checks: {} };
+const ok = (k, cond, detail) => { out.checks[k] = { pass: !!cond, detail }; console.log(`${cond ? 'PASS' : 'FAIL'} ${k} ${detail ?? ''}`); };
+const p = pre[mfr];
+const dh = p.draft.handle, th = p.twin.handle;
+// draft page
+const page = await fetch(`${SITE}/products/${dh}`, { redirect: 'manual' });
+const html = await page.text();
+ok('draft_page_200', page.status === 200, `${page.status} ${SITE}/products/${dh}`);
+const leaks = (html.match(/wallquest/gi) || []).length;
+ok('no_wallquest_in_html', leaks === 0, `occurrences=${leaks}`);
+ok('html_has_price_114.03', /114\.03/.test(html), '');
+const js = await (await fetch(`${SITE}/products/${dh}.js`)).json();
+const roll = js.variants.find(v => /Roll/.test(v.title)), samp = js.variants.find(v => /Sample/.test(v.title));
+ok('roll_114.03_pos1', roll && roll.price === 11403 && js.variants[0].id === roll.id && roll.available, JSON.stringify({ title: roll?.title, price: roll?.price, sku: roll?.sku, available: roll?.available }));
+ok('sample_4.25', samp && samp.price === 425 && samp.available, JSON.stringify({ price: samp?.price, sku: samp?.sku }));
+ok('featured_image', !!js.featured_image, js.featured_image?.split('?')[0].split('/').pop());
+// add to cart (fresh anonymous cart; no order created)
+const add = await fetch(`${SITE}/cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: roll?.id, quantity: 2 }] }) });
+const addj = await add.json().catch(() => ({}));
+ok('add_to_cart_roll_x2', add.status === 200 && addj.items?.[0]?.price === 11403, `${add.status} line_price=${addj.items?.[0]?.line_price}`);
+const adds = await fetch(`${SITE}/cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: samp?.id, quantity: 1 }] }) });
+const addsj = await adds.json().catch(() => ({}));
+ok('add_to_cart_sample', adds.status === 200 && addsj.items?.[0]?.price === 425, `${adds.status}`);
+// twin redirect
+// NB: Node's default 'accept-language: *' draws a 302 from the edge; real browsers + Googlebot get Shopify's 301.
+const tw = await fetch(`${SITE}/products/${th}`, { redirect: 'manual', headers: { 'accept-language': 'en-US,en;q=0.9', 'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' } });
+const loc = tw.headers.get('location') || '';
+ok('twin_301_to_draft', tw.status === 301 && loc.endsWith(`/products/${dh}`) && tw.headers.get('x-redirect-reason') === 'shop_redirect', `${tw.status} -> ${loc} (${tw.headers.get('x-redirect-reason')})`);
+// other pairs untouched (not yet processed) / still in promoted state (processed)
+const sig = x => JSON.stringify({ st: x.status, t: x.title, tags: [...x.tags].sort(), v: x.variants.nodes.map(v => [v.id, v.price, v.sku, v.position]), m: x.media.nodes.map(n => n.id), l: x.resourcePublicationsV2.nodes.map(n => n.publication.name + n.isPublished) });
+for (const pr of PAIRS) {
+ if (pr.mfr === mfr) continue;
+ const d = await snap(pr.draft), t = await snap(pr.twin);
+ if (doneList.includes(pr.mfr)) ok(`other_${pr.mfr}_promoted_state`, d.status === 'ACTIVE' && t.status === 'ARCHIVED', `${d.status}/${t.status}`);
+ else ok(`other_${pr.mfr}_untouched`, sig(d) === sig(pre[pr.mfr].draft) && sig(t) === sig(pre[pr.mfr].twin), '');
+}
+out.pass = Object.values(out.checks).every(c => c.pass);
+fs.writeFileSync(new URL(`./verify-${mfr}.json`, import.meta.url), JSON.stringify(out, null, 2));
+console.log(out.pass ? `VERIFY ${mfr}: ALL PASS` : `VERIFY ${mfr}: FAILURES`);
+process.exit(out.pass ? 0 : 1);
← dd22e2c TK-12227: restrict to Online Store (unpublish pre-listed cha
·
back to Designerwallcoverings
·
TK-12227: contrarian fixes — pin inventory to twin roll loca 48cde5e →