← 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
A shopify/scripts/cadence/sdg-trade-login.mjsM shopify/tres-tintas-desc-backups/_done.ledger
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 →