[object Object]

← back to Designer Wallcoverings

auto-save: 2026-06-23T08:50:33 (2 files) — shopify/tres-tintas-desc-backups/_done.ledger shopify/scripts/cadence/sdg-trade-login.mjs

7b78feaa3c23cbb64325c3a774de9cef6e82d1f8 · 2026-06-23 08:50:38 -0700 · Steve Abrams

Files touched

Diff

commit 7b78feaa3c23cbb64325c3a774de9cef6e82d1f8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 23 08:50:38 2026 -0700

    auto-save: 2026-06-23T08:50:33 (2 files) — shopify/tres-tintas-desc-backups/_done.ledger shopify/scripts/cadence/sdg-trade-login.mjs
---
 shopify/scripts/cadence/sdg-trade-login.mjs   | 125 ++++++++++++++++++++++++++
 shopify/tres-tintas-desc-backups/_done.ledger |  13 +++
 2 files changed, 138 insertions(+)

diff --git a/shopify/scripts/cadence/sdg-trade-login.mjs b/shopify/scripts/cadence/sdg-trade-login.mjs
new file mode 100644
index 00000000..ccea887f
--- /dev/null
+++ b/shopify/scripts/cadence/sdg-trade-login.mjs
@@ -0,0 +1,125 @@
+#!/usr/bin/env node
+/**
+ * SDG Trade Hub login + reconnaissance (Zoffany cost fetch, Steve 2026-06-23).
+ * Separate headless Chromium — NEVER touches Steve's Chrome.
+ * Login = email + password + 6-digit MFA emailed to info@ (read via LOCAL George).
+ *
+ * Phase 1 (this run): log in, clear MFA, confirm success, dump nav/links + screenshot
+ * so we can locate the price list / order-pad. Phase 2 adds the per-SKU scrape.
+ */
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+let chromium;
+for (const p of [
+  '/Users/stevestudio2/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/playwright',
+  'playwright',
+]) { try { ({ chromium } = require(p)); break; } catch {} }
+if (!chromium) { console.error('FATAL: playwright not resolvable'); process.exit(1); }
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import os from 'node:os';
+
+const EMAIL = 'info@designerwallcoverings.com';
+const ENV = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+const PW = (ENV.match(/^SANDERSON_PASSWORD=(.*)$/m) || [])[1];
+if (!PW) { console.error('FATAL: SANDERSON_PASSWORD not in secrets .env'); process.exit(1); }
+
+const OUT = '/tmp/sdg'; fs.mkdirSync(OUT, { recursive: true });
+const GEORGE = 'http://127.0.0.1:9850';
+const gq = (q) => JSON.parse(execSync(`curl -s -u admin: "${GEORGE}/api/search?account=info&q=${encodeURIComponent(q)}&maxResults=5"`).toString());
+const gget = (id) => JSON.parse(execSync(`curl -s -u admin: "${GEORGE}/api/messages/${id}?account=info"`).toString());
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const log = (...a) => console.log(new Date().toISOString().slice(11,19), ...a);
+
+function latestMfaId() {
+  const r = gq('from:sandersondesigngroup (Multifactor OR Authentication OR "Trade Hub")');
+  const m = r.messages || [];
+  return m.length ? m[0].id : null;
+}
+function codeFromMsg(id) {
+  const m = gget(id);
+  const hay = `${m.snippet || ''} ${m.subject || ''} ${(m.body || m.text || '')}`;
+  const mm = hay.match(/\b(\d{6})\b/);
+  return mm ? mm[1] : null;
+}
+
+const browser = await chromium.launch({ headless: true });
+const ctx = await browser.newContext({
+  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
+  viewport: { width: 1440, height: 1000 },
+});
+const page = await ctx.newPage();
+try {
+  log('goto login');
+  await page.goto('https://trade.sandersondesigngroup.com/login', { waitUntil: 'domcontentloaded', timeout: 60000 });
+  await sleep(1500);
+  // cookie banner
+  try { await page.getByRole('button', { name: /^accept$/i }).click({ timeout: 4000 }); log('cookies accepted'); } catch { log('no cookie button'); }
+
+  // currency → USD if available
+  try {
+    const cur = page.locator('select').first();
+    if (await cur.count()) { await cur.selectOption({ label: 'USD' }).catch(()=>cur.selectOption('USD').catch(()=>{})); log('currency→USD tried'); }
+  } catch { log('currency select skipped'); }
+
+  // email + password (robust selectors)
+  await page.fill('input[type="email"], input[name*="mail" i], input[id*="mail" i]', EMAIL);
+  await page.fill('input[type="password"], input[name*="pass" i], input[id*="pass" i]', PW);
+  log('filled email+password');
+
+  const beforeMfa = latestMfaId();
+  log('latest MFA id before submit:', beforeMfa);
+
+  await page.screenshot({ path: `${OUT}/01-prelogin.png` });
+  await Promise.all([
+    page.getByRole('button', { name: /log ?in/i }).click({ timeout: 8000 }).catch(()=>page.click('button[type="submit"]')),
+  ]);
+  log('submitted login');
+  await sleep(4000);
+  await page.screenshot({ path: `${OUT}/02-postsubmit.png` });
+
+  // MFA step? look for a code input
+  const mfaInput = page.locator('input[type="text"], input[type="tel"], input[autocomplete*="one-time" i], input[name*="code" i], input[name*="otp" i]');
+  let needMfa = false;
+  try { needMfa = (await mfaInput.count()) > 0 && /code|verif|authentic|mfa|otp/i.test(await page.content()); } catch {}
+  log('MFA required?', needMfa);
+
+  if (needMfa) {
+    // poll George for a NEW MFA email (id different from beforeMfa)
+    let code = null;
+    for (let i = 0; i < 24; i++) { // ~120s
+      await sleep(5000);
+      const id = latestMfaId();
+      if (id && id !== beforeMfa) { code = codeFromMsg(id); if (code) { log('got fresh MFA code via George'); break; } }
+      log(`waiting for MFA email… (${i+1})`);
+    }
+    if (!code) { log('FATAL: no fresh MFA code arrived'); await page.screenshot({ path: `${OUT}/03-nocode.png` }); throw new Error('no-mfa'); }
+    // enter the 6 digits (single field or split fields)
+    const fields = await mfaInput.elementHandles();
+    if (fields.length >= 6) { for (let i=0;i<6;i++) await fields[i].type(code[i]); }
+    else { await mfaInput.first().fill(code); }
+    log('entered MFA code');
+    await page.getByRole('button', { name: /verif|submit|continue|log ?in/i }).first().click({ timeout: 6000 }).catch(()=>{});
+    await sleep(4000);
+  }
+
+  await page.screenshot({ path: `${OUT}/04-landing.png`, fullPage: true });
+  const url = page.url();
+  const loggedIn = !/login/i.test(url);
+  log('post-login url:', url, '| loggedIn:', loggedIn);
+
+  // dump nav links to find the price list / order pad / catalogue
+  const links = await page.$$eval('a[href]', as => as.map(a => ({ t: (a.textContent||'').trim().slice(0,40), h: a.getAttribute('href') }))
+    .filter(x => x.t && !/^#/.test(x.h||'')).slice(0, 120));
+  fs.writeFileSync(`${OUT}/links.json`, JSON.stringify(links, null, 2));
+  const interesting = links.filter(l => /price|order|pad|catalog|product|wallpaper|wallcover|brand|zoffany|account|download|export/i.test(l.t + ' ' + l.h));
+  log('interesting links:'); interesting.forEach(l => console.log('   ', JSON.stringify(l)));
+  fs.writeFileSync(`${OUT}/result.json`, JSON.stringify({ loggedIn, url, interesting }, null, 2));
+  log('DONE — artifacts in', OUT);
+} catch (e) {
+  log('ERROR:', e.message);
+  try { await page.screenshot({ path: `${OUT}/99-error.png`, fullPage: true }); } catch {}
+  process.exitCode = 2;
+} finally {
+  await browser.close();
+}
diff --git a/shopify/tres-tintas-desc-backups/_done.ledger b/shopify/tres-tintas-desc-backups/_done.ledger
index d97ba91f..6d996f42 100644
--- a/shopify/tres-tintas-desc-backups/_done.ledger
+++ b/shopify/tres-tintas-desc-backups/_done.ledger
@@ -121,3 +121,16 @@
 7863637049395
 7863637082163
 7863637114931
+7863637147699
+7863637180467
+7863637213235
+7863637278771
+7863637377075
+7863637475379
+7863637540915
+7863637639219
+7863637671987
+7863637737523
+7863637770291
+7863637803059
+7863637835827

← ab61bf4f Update tres-tintas ledger  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-23T09:20:41 (16 files) — shopify/scripts/ e78be974 →