[object Object]

← 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

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 →