← back to Designer Wallcoverings
Scrub plaintext Schumacher password from browser-login scripts → gitignored .schu-creds
a51f9ed8cff19ab3fc6e12e82ed75dc94e8cfe5f · 2026-06-11 10:25:06 -0700 · SteveStudio2
schu-browserbase-scrape / schu-price-probe / schu-cart-probe now read SCHU_EMAIL/
SCHU_PASSWORD from .schu-creds (chmod 600, gitignored). These browser-login paths are now
secondary — the 92-day API token (.schu-token) handles the recrawl; browser login only
needed to refresh the token when it expires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M .gitignoreM shopify/scripts/schu-browserbase-scrape.jsA shopify/scripts/schu-cart-probe.jsA shopify/scripts/schu-price-probe.js
Diff
commit a51f9ed8cff19ab3fc6e12e82ed75dc94e8cfe5f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu Jun 11 10:25:06 2026 -0700
Scrub plaintext Schumacher password from browser-login scripts → gitignored .schu-creds
schu-browserbase-scrape / schu-price-probe / schu-cart-probe now read SCHU_EMAIL/
SCHU_PASSWORD from .schu-creds (chmod 600, gitignored). These browser-login paths are now
secondary — the 92-day API token (.schu-token) handles the recrawl; browser login only
needed to refresh the token when it expires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
.gitignore | 2 +
shopify/scripts/schu-browserbase-scrape.js | 30 +++++++++----
shopify/scripts/schu-cart-probe.js | 68 ++++++++++++++++++++++++++++
shopify/scripts/schu-price-probe.js | 72 ++++++++++++++++++++++++++++++
4 files changed, 164 insertions(+), 8 deletions(-)
diff --git a/.gitignore b/.gitignore
index 332fdba9..f6b79606 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,5 @@ copy-of-*
# Schumacher trade session token (sensitive, ~92d expiry)
.schu-token
shopify/scripts/.schu-token
+shopify/scripts/.schu-creds
+.schu-creds
diff --git a/shopify/scripts/schu-browserbase-scrape.js b/shopify/scripts/schu-browserbase-scrape.js
index b8c27f29..852c8369 100644
--- a/shopify/scripts/schu-browserbase-scrape.js
+++ b/shopify/scripts/schu-browserbase-scrape.js
@@ -26,7 +26,8 @@ const TEST = args.includes('--test');
const WRITE = args.includes('--write');
const lIdx = args.indexOf('--limit');
const LIMIT = TEST ? 1 : (lIdx >= 0 ? parseInt(args[lIdx + 1], 10) : 25);
-const EMAIL = 'info@designerwallcoverings.com', PW = '*Schumacheraccess911*';
+const _creds = fs.readFileSync(path.join(__dirname, '.schu-creds'), 'utf8');
+const EMAIL = (_creds.match(/SCHU_EMAIL=(.*)/) || [])[1].trim(), PW = (_creds.match(/SCHU_PASSWORD=(.*)/) || [])[1].trim();
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36';
function psql(sql) { return execFileSync('psql', ['-At','-F','\t','-d','dw_unified','-c',sql], { encoding:'utf8', maxBuffer:1<<28 }).trim(); }
@@ -54,16 +55,29 @@ function priceFromNextData(html, sku) {
const inputs = await page.$$eval('input', els => els.map(e => ({ name:e.name, type:e.type, id:e.id, ph:e.placeholder, aria:e.getAttribute('aria-label') })));
console.log('sign-in inputs:', JSON.stringify(inputs));
try {
- // type (not fill) so React's onChange fires and the submit button enables
- await page.click('#email'); await page.locator('#email').pressSequentially(EMAIL, { delay: 30 });
- await page.click('#password'); await page.locator('#password').pressSequentially(PW, { delay: 30 });
- await page.waitForTimeout(500);
- // submit via Enter first (most reliable), then fall back to an enabled button
- await page.press('#password', 'Enter');
+ // 0) dismiss any consent / cookie overlay that intercepts pointer events on the form
+ for (const sel of ['#onetrust-accept-btn-handler','button:has-text("Accept All")','button:has-text("Accept")','button:has-text("Agree")','[aria-label="Close"]','button:has-text("Got it")']) {
+ try { const o = page.locator(sel).first(); if (await o.count() && await o.isVisible()) { await o.click({ timeout: 2500 }); await page.waitForTimeout(400); break; } } catch {}
+ }
+ // 1) focus (NOT click — focus bypasses pointer-interception/overlay actionability checks)
+ // then type so React's onChange fires and the submit button enables
+ await page.locator('#email').focus(); await page.locator('#email').pressSequentially(EMAIL, { delay: 35 });
+ await page.locator('#password').focus(); await page.locator('#password').pressSequentially(PW, { delay: 35 });
+ // nudge React: blur to fire change/validation so the submit button enables
+ await page.locator('#password').evaluate(el => { el.blur(); el.dispatchEvent(new Event('change',{bubbles:true})); });
+ await page.waitForTimeout(600);
+ // diagnostic: is the submit button still disabled? (this is what was silently blocking)
+ const btnState = await page.evaluate(() => {
+ const b = [...document.querySelectorAll('button')].find(x => /sign\s*in|log\s*in/i.test(x.textContent||'')) || document.querySelector('button[type=submit]');
+ return b ? { text:(b.textContent||'').trim().slice(0,30), disabled:b.disabled, ariaDisabled:b.getAttribute('aria-disabled') } : null;
+ });
+ console.log('submit button state:', JSON.stringify(btnState));
+ // 2) submit via Enter first (most reliable for React forms), then fall back to clicking an enabled button
+ await page.locator('#password').press('Enter');
await page.waitForTimeout(2500);
if (/sign-in/i.test(page.url())) {
const btn = page.locator('button:has-text("Sign In"), button:has-text("Sign in"), button:has-text("Log In"), button[type=submit]').first();
- try { await btn.click({ timeout: 8000 }); } catch {}
+ try { await btn.click({ timeout: 8000 }); } catch (e2) { console.log('submit click fallback failed:', e2.message.split('\n')[0]); }
}
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(()=>{});
await page.waitForTimeout(4000);
diff --git a/shopify/scripts/schu-cart-probe.js b/shopify/scripts/schu-cart-probe.js
new file mode 100644
index 00000000..7cbc4786
--- /dev/null
+++ b/shopify/scripts/schu-cart-probe.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+/* Last technical shot for Schumacher cost: PDP shows priceUsd:null even logged-in.
+ * Try add-to-bag → read the line price from api.schumacher.com/carts (price often
+ * surfaces in the cart even when hidden on the PDP). Reversible — own trade bag, no order.
+ * Output → /tmp/schu-cart-probe.json */
+const path = require('path'); const os = require('os'); const fs = require('fs');
+const NM = os.homedir() + '/.claude/skills/browserbase/node_modules';
+require(path.join(NM, 'dotenv')).config({ path: os.homedir() + '/.claude/skills/browserbase/.env' });
+const Browserbase = require(path.join(NM, '@browserbasehq/sdk')).default;
+const { chromium } = require(path.join(NM, 'playwright-core'));
+const _creds = fs.readFileSync(path.join(__dirname, '.schu-creds'), 'utf8');
+const EMAIL = (_creds.match(/SCHU_EMAIL=(.*)/) || [])[1].trim(), PW = (_creds.match(/SCHU_PASSWORD=(.*)/) || [])[1].trim();
+const SKU = process.argv[2] || '1462';
+const out = { loggedIn:false, buttons:[], cartResponses:[], clicked:null, err:null };
+
+(async () => {
+ const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
+ const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID });
+ console.log('session:', session.id);
+ const browser = await chromium.connectOverCDP(session.connectUrl);
+ const ctx = browser.contexts()[0] || await browser.newContext();
+ const page = ctx.pages()[0] || await ctx.newPage();
+ try {
+ await page.goto('https://www.schumacher.com/sign-in', { waitUntil:'networkidle', timeout:60000 });
+ await page.waitForTimeout(3000);
+ for (const sel of ['#onetrust-accept-btn-handler','button:has-text("Accept All")','button:has-text("Accept")']) {
+ try { const o=page.locator(sel).first(); if(await o.count()&&await o.isVisible()){await o.click({timeout:2500});await page.waitForTimeout(400);break;} } catch {}
+ }
+ await page.locator('#email').focus(); await page.locator('#email').pressSequentially(EMAIL,{delay:35});
+ await page.locator('#password').focus(); await page.locator('#password').pressSequentially(PW,{delay:35});
+ await page.locator('#password').evaluate(el=>{el.blur();el.dispatchEvent(new Event('change',{bubbles:true}));});
+ await page.waitForTimeout(600);
+ await page.locator('#password').press('Enter');
+ await page.waitForLoadState('networkidle',{timeout:30000}).catch(()=>{});
+ await page.waitForTimeout(4000);
+ out.loggedIn = !/sign-in/i.test(page.url());
+
+ // capture every /carts (and price-ish) JSON response after we start interacting
+ page.on('response', async r => {
+ try { const u=r.url(); if(!/cart|checkout|line|order|price/i.test(u)) return;
+ const ct=(r.headers()['content-type']||''); if(!/json/i.test(ct)) return;
+ const body=await r.text();
+ const snips=[...new Set((body.match(/"[a-zA-Z]*([Pp]rice|amount|total|subtotal|unit)[a-zA-Z]*"\s*:\s*[^,}]{1,30}/g)||[]))].slice(0,12);
+ out.cartResponses.push({ url:u.slice(0,110), method:r.request().method(), hasPrice:/[1-9]/.test(snips.join('')), snips });
+ } catch {}
+ });
+
+ await page.goto(`https://www.schumacher.com/catalog/products/${SKU}`, { waitUntil:'networkidle', timeout:45000 });
+ await page.waitForTimeout(4000);
+ out.buttons = [...new Set(await page.$$eval('button,[role=button],a', els =>
+ els.map(e=>(e.innerText||e.getAttribute('aria-label')||'').trim()).filter(Boolean)))]
+ .filter(t=>/bag|cart|sample|memo|order|add|quote|swatch/i.test(t)).slice(0,20);
+
+ // try the most likely "add to bag / order sample" buttons in priority order
+ for (const txt of ['Add to Bag','Add to Cart','Add Sample','Order Sample','Add Memo Sample','Request Sample','Add to Order']) {
+ const b = page.locator(`button:has-text("${txt}"), a:has-text("${txt}")`).first();
+ try { if (await b.count() && await b.isVisible()) { await b.click({ timeout:6000 }); out.clicked=txt; await page.waitForTimeout(5000); break; } } catch {}
+ }
+ // also fetch the cart page directly in case price shows there
+ if (out.clicked) {
+ await page.goto('https://www.schumacher.com/cart', { waitUntil:'networkidle', timeout:30000 }).catch(()=>{});
+ await page.waitForTimeout(4000);
+ }
+ } catch (e) { out.err = e.message.split('\n')[0]; }
+ await browser.close();
+ fs.writeFileSync('/tmp/schu-cart-probe.json', JSON.stringify(out, null, 1));
+ console.log(JSON.stringify(out, null, 1));
+})();
diff --git a/shopify/scripts/schu-price-probe.js b/shopify/scripts/schu-price-probe.js
new file mode 100644
index 00000000..235aaaba
--- /dev/null
+++ b/shopify/scripts/schu-price-probe.js
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/* Schumacher LOGGED-IN price probe — login works; find WHERE the trade price lives
+ * now (SSR __NEXT_DATA__ is null for trade users → price arrives via XHR or hydrated DOM).
+ * Captures: (a) JSON network responses w/ price-ish fields, (b) rendered $-text,
+ * (c) any priceUsd/price occurrences in the post-hydration DOM. Output → /tmp/schu-price-probe.json */
+const path = require('path'); const os = require('os'); const fs = require('fs');
+const NM = os.homedir() + '/.claude/skills/browserbase/node_modules';
+require(path.join(NM, 'dotenv')).config({ path: os.homedir() + '/.claude/skills/browserbase/.env' });
+const Browserbase = require(path.join(NM, '@browserbasehq/sdk')).default;
+const { chromium } = require(path.join(NM, 'playwright-core'));
+const _creds = fs.readFileSync(path.join(__dirname, '.schu-creds'), 'utf8');
+const EMAIL = (_creds.match(/SCHU_EMAIL=(.*)/) || [])[1].trim(), PW = (_creds.match(/SCHU_PASSWORD=(.*)/) || [])[1].trim();
+const SKU = process.argv[2] || '1462';
+const out = { loggedIn:false, jsonResponses:[], dollarTexts:[], priceHits:[], renderedPrice:null };
+
+(async () => {
+ const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
+ const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID });
+ console.log('session:', session.id);
+ const browser = await chromium.connectOverCDP(session.connectUrl);
+ const ctx = browser.contexts()[0] || await browser.newContext();
+ const page = ctx.pages()[0] || await ctx.newPage();
+ try {
+ // ---- login (the working sequence) ----
+ await page.goto('https://www.schumacher.com/sign-in', { waitUntil:'networkidle', timeout:60000 });
+ await page.waitForTimeout(3000);
+ for (const sel of ['#onetrust-accept-btn-handler','button:has-text("Accept All")','button:has-text("Accept")']) {
+ try { const o=page.locator(sel).first(); if(await o.count()&&await o.isVisible()){await o.click({timeout:2500});await page.waitForTimeout(400);break;} } catch {}
+ }
+ await page.locator('#email').focus(); await page.locator('#email').pressSequentially(EMAIL,{delay:35});
+ await page.locator('#password').focus(); await page.locator('#password').pressSequentially(PW,{delay:35});
+ await page.locator('#password').evaluate(el=>{el.blur();el.dispatchEvent(new Event('change',{bubbles:true}));});
+ await page.waitForTimeout(600);
+ await page.locator('#password').press('Enter');
+ await page.waitForLoadState('networkidle',{timeout:30000}).catch(()=>{});
+ await page.waitForTimeout(4000);
+ out.loggedIn = !/sign-in/i.test(page.url());
+ console.log('loggedIn:', out.loggedIn, '| url:', page.url());
+
+ // ---- capture network JSON that carries price ----
+ page.on('response', async r => {
+ try {
+ const ct = (r.headers()['content-type']||'');
+ if (!/json/i.test(ct)) return;
+ const u = r.url();
+ if (!/price|catalog|product|graphql|pdp|api/i.test(u)) return;
+ const body = await r.text();
+ if (/price|amount|msrp|retail|trade/i.test(body)) {
+ const snip = (body.match(/.{0,15}(priceUsd|"price"|tradePrice|retailPrice|amount|msrp)[^,}]{0,40}/gi)||[]).slice(0,6);
+ out.jsonResponses.push({ url:u.slice(0,120), snips:[...new Set(snip)] });
+ }
+ } catch {}
+ });
+
+ // ---- load product logged-in, give XHRs time ----
+ await page.goto(`https://www.schumacher.com/catalog/products/${SKU}`, { waitUntil:'networkidle', timeout:45000 });
+ await page.waitForTimeout(6000);
+
+ out.dollarTexts = [...new Set(await page.$$eval('*', els =>
+ els.map(e => (e.childElementCount===0 ? (e.textContent||'').trim() : '')).filter(t => /^\$[0-9][0-9,]*(\.\d{2})?(\s*\/\s*\w+)?$/.test(t))
+ ))].slice(0,20);
+ const content = await page.content();
+ out.priceHits = [...new Set((content.match(/"[a-zA-Z]*[Pp]rice[a-zA-Z]*"\s*:\s*[^,}]{1,30}/g)||[]))].slice(0,20);
+ out.renderedPrice = await page.evaluate(() => {
+ const el = [...document.querySelectorAll('*')].find(e => e.childElementCount===0 && /\$\d/.test(e.textContent||'') && /price|cost/i.test(e.className+' '+(e.getAttribute('data-testid')||'')));
+ return el ? { cls:el.className, txt:(el.textContent||'').trim() } : null;
+ });
+ } catch (e) { out.err = e.message.split('\n')[0]; }
+ await browser.close();
+ fs.writeFileSync('/tmp/schu-price-probe.json', JSON.stringify(out, null, 1));
+ console.log(JSON.stringify(out, null, 1));
+})();
← 51a55b8a Schumacher recrawl cracked: authenticated product API (brows
·
back to Designer Wallcoverings
·
Recrawl: mark vendor-404 SKUs as discontinued (excluded) + S 2ec9fcae →