[object Object]

← back to Eur Recrawl

auto-save: 2026-07-30T18:20:08 (5 files) — login.mjs probe-dg.mjs inspect-dg.mjs inspect.mjs reset-dg.mjs

9834b9e3ec0478228502d1adf9479b3c2798b3d8 · 2026-07-30 18:20:16 -0700 · Steve Abrams

Files touched

Diff

commit 9834b9e3ec0478228502d1adf9479b3c2798b3d8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 30 18:20:16 2026 -0700

    auto-save: 2026-07-30T18:20:08 (5 files) — login.mjs probe-dg.mjs inspect-dg.mjs inspect.mjs reset-dg.mjs
---
 inspect-dg.mjs |  26 +++++++++++++
 inspect.mjs    |  25 +++++++++++++
 login.mjs      |  45 +++++++++++++++--------
 probe-dg.mjs   | 113 +++++++++++++++++++++++++++++++++++++++------------------
 reset-dg.mjs   |  46 +++++++++++++++++++++++
 5 files changed, 204 insertions(+), 51 deletions(-)

diff --git a/inspect-dg.mjs b/inspect-dg.mjs
new file mode 100644
index 0000000..7c91c1e
--- /dev/null
+++ b/inspect-dg.mjs
@@ -0,0 +1,26 @@
+// inspect-dg.mjs — reuse the CF-cleared .auth/dg session to dump the login form's
+// real structure (inputs/buttons across all frames) so login.mjs can target it.
+import { chromium } from 'playwright';
+import path from 'node:path';
+
+const ctx = await chromium.launchPersistentContext(path.join(process.cwd(), '.auth', 'dg'), {
+  headless: false, channel: 'chrome', viewport: { width: 1440, height: 900 },
+});
+const page = ctx.pages()[0] || await ctx.newPage();
+await page.goto('https://www.designersguild.com/en-us/login/l102?t=1', { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e) => console.log('nav', e.message));
+await page.waitForTimeout(7000);
+console.log('title:', await page.title().catch(() => ''), '| url:', page.url());
+console.log('frames:', page.frames().length);
+for (const fr of page.frames()) {
+  const els = await fr.evaluate(() =>
+    [...document.querySelectorAll('input, button, [role=button]')].map((el) => ({
+      tag: el.tagName, type: el.type || '', name: el.name || '', id: el.id || '',
+      ph: el.placeholder || '', ac: el.autocomplete || '', txt: (el.innerText || el.value || '').slice(0, 25),
+    })),
+  ).catch(() => []);
+  if (els.length) console.log(`\n[frame ${fr.url().slice(0, 70)}]\n` + JSON.stringify(els));
+}
+await page.screenshot({ path: 'probe-dg-login.png', fullPage: false }).catch(() => {});
+console.log('\nscreenshot -> probe-dg-login.png');
+await page.waitForTimeout(2000);
+await ctx.close();
diff --git a/inspect.mjs b/inspect.mjs
new file mode 100644
index 0000000..63c3342
--- /dev/null
+++ b/inspect.mjs
@@ -0,0 +1,25 @@
+// inspect.mjs <url> [authProfile] — dump inputs/buttons across all frames for a page.
+import { chromium } from 'playwright';
+import path from 'node:path';
+const url = process.argv[2];
+const profile = process.argv[3] || 'dgtrade';
+if (!url) { console.error('usage: node inspect.mjs <url> [authProfile]'); process.exit(1); }
+const ctx = await chromium.launchPersistentContext(path.join(process.cwd(), '.auth', profile), {
+  headless: false, channel: 'chrome', viewport: { width: 1440, height: 900 },
+});
+const page = ctx.pages()[0] || await ctx.newPage();
+await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e) => console.log('nav', e.message));
+await page.waitForTimeout(5000);
+console.log('title:', await page.title().catch(() => ''), '| url:', page.url());
+for (const fr of page.frames()) {
+  const els = await fr.evaluate(() =>
+    [...document.querySelectorAll('input, button, a[href*=login i], [type=submit]')].map((el) => ({
+      tag: el.tagName, type: el.type || '', name: el.name || '', id: el.id || '', ph: el.placeholder || '',
+      txt: (el.innerText || el.value || '').slice(0, 25),
+    })).filter((e) => e.type !== 'hidden'),
+  ).catch(() => []);
+  if (els.length) console.log(`\n[frame ${fr.url().slice(0, 70)}]\n` + JSON.stringify(els));
+}
+await page.screenshot({ path: 'probe-inspect.png' }).catch(() => {});
+await page.waitForTimeout(1500);
+await ctx.close();
diff --git a/login.mjs b/login.mjs
index ccf67ee..2120480 100644
--- a/login.mjs
+++ b/login.mjs
@@ -67,28 +67,43 @@ catch (e) { log(`nav warning: ${e.message}`); }
 await page.waitForTimeout(1500);
 log(`loaded: "${(await page.title().catch(() => '')).slice(0, 80)}" @ ${page.url()}`);
 
-// Let Cloudflare clear + the form render (real Chrome passes the challenge naturally).
-let hasPass = false;
-try { await page.waitForSelector('input[type=password]', { timeout: 90000, state: 'visible' }); hasPass = true; }
-catch { log('no password field after 90s — a Cloudflare/captcha challenge may be showing; solve it in the window if so.'); }
+// Let Cloudflare clear + the form render. Fields may be hidden behind tabs (DG), so wait
+// for the form to be ATTACHED (present in DOM), not visible.
+try {
+  await page.waitForSelector('#trade-login-email, #login-email, input[type=email], input[type=password]', { timeout: 60000, state: 'attached' });
+  log('login form present');
+} catch { log('login form not detected in 60s — a Cloudflare/captcha challenge may be showing; solve it in the window if so.'); }
+await page.waitForTimeout(1500);
 
 async function fillCreds() {
-  const userSel = 'input[type=email], input[autocomplete="username"], input[name*=user i], input[name*=email i], input[id*=user i], input[id*=email i]';
-  const u = await page.$(userSel);
-  if (u) { await u.fill('').catch(() => {}); await u.type(cfg.user, { delay: 25 }); log('username filled'); }
-  else log('username field not found');
-
+  if (which === 'dg') {
+    // Designers Guild: use the TRADE LOGIN tab (gives net/trade pricing).
+    const tab = await page.$('#signin-trade-tab-anchor');
+    if (tab) { await tab.click().catch(() => {}); await page.waitForTimeout(1200); log('opened Trade Login tab'); }
+    const radio = await page.$('#trade-already-authenticated');
+    if (radio) { await radio.check().catch(() => radio.click().catch(() => {})); }
+    const em = await page.$('#trade-login-email');
+    const pw = await page.$('#trade-login-password');
+    if (em) { await em.fill('').catch(() => {}); await em.type(cfg.user, { delay: 25 }); log('trade email filled'); } else log('trade email field not found');
+    if (pw) { await pw.fill('').catch(() => {}); await pw.type(cfg.pass, { delay: 25 }); log('trade password filled'); } else log('trade password field not found');
+    // reCAPTCHA may sit between fill and submit — pass invisibly or wait for Steve to click it.
+    const rc = page.frames().some((f) => /recaptcha/.test(f.url()));
+    if (rc) log('reCAPTCHA present — if it shows a challenge, click it in the window; then I detect login.');
+    const signin = await page.$('#trade-login-password ~ button[type=submit], button:has-text("Sign in")');
+    if (signin) { await signin.click().catch(() => {}); log('trade Sign in clicked'); } else log('trade Sign in button not found');
+    return;
+  }
+  // generic (osborne + others)
+  const u = await page.$('input[type=email], input[autocomplete="username"], input[name*=user i], input[name*=email i], input[id*=user i], input[id*=email i]');
+  if (u) { await u.fill('').catch(() => {}); await u.type(cfg.user, { delay: 25 }); log('username filled'); } else log('username field not found');
   if (!(await page.$('input[type=password]'))) {
     const nxt = await page.$('button:has-text("Continue"), button:has-text("Next")');
     if (nxt) { await nxt.click().catch(() => {}); await page.waitForTimeout(1500); }
   }
   const p = await page.$('input[type=password]');
-  if (p) { await p.fill('').catch(() => {}); await p.type(cfg.pass, { delay: 25 }); log('password filled'); }
-  else log('password field not found');
-
-  const btn = await page.$('button[type=submit], input[type=submit], button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign in"), button:has-text("Sign In")');
-  if (btn) { await btn.click().catch(() => {}); log('submit clicked'); }
-  else { await page.keyboard.press('Enter').catch(() => {}); log('submitted via Enter'); }
+  if (p) { await p.fill('').catch(() => {}); await p.type(cfg.pass, { delay: 25 }); log('password filled'); } else log('password field not found');
+  const btn = await page.$('button[type=submit], input[type=submit], button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign in")');
+  if (btn) { await btn.click().catch(() => {}); log('submit clicked'); } else { await page.keyboard.press('Enter').catch(() => {}); log('submitted via Enter'); }
 }
 await fillCreds().catch((e) => log('fill error: ' + e.message));
 
diff --git a/probe-dg.mjs b/probe-dg.mjs
index 0dbf36a..0ef913d 100644
--- a/probe-dg.mjs
+++ b/probe-dg.mjs
@@ -1,47 +1,88 @@
-// probe-dg.mjs <MFR_CODE> — reuse the authed DG session (.auth/dg) to find where a
-// pattern's LOGGED-IN price renders. Read-only recon for the eur-recrawl pricing crawl.
+// probe-dg.mjs <MFR_CODE> — reuse the authed DG session (.auth/dg) to (1) confirm we're
+// logged in and (2) find where a pattern's LOGGED-IN price renders. Read-only recon.
 //   node probe-dg.mjs PDG674
-// Prints: the product URL it lands on + every price-looking string on the page, so we
-// can confirm whether a trade/net price is exposed to DW's logged-in account.
-
 import { chromium } from 'playwright';
 import path from 'node:path';
 
-const mfr = (process.argv[2] || '').trim();
-if (!mfr) { console.error('usage: node probe-dg.mjs <MFR_CODE>'); process.exit(1); }
+const arg = (process.argv[2] || '').trim();
+if (!arg) { console.error('usage: node probe-dg.mjs <MFR_CODE | product-URL>'); process.exit(1); }
+const isUrl = /^https?:\/\//i.test(arg);
+const base = isUrl ? '' : arg.split(/[\/\-]/)[0];
 
-const base = mfr.split(/[\/\-]/)[0]; // PDG674/07 -> PDG674 (search on the base pattern)
-const authDir = path.join(process.cwd(), '.auth', 'dg');
-const ctx = await chromium.launchPersistentContext(authDir, { headless: false, channel: 'chrome', viewport: { width: 1440, height: 900 } });
+const ctx = await chromium.launchPersistentContext(path.join(process.cwd(), '.auth', 'dg'), {
+  headless: false, channel: 'chrome', viewport: { width: 1440, height: 900 },
+});
 const page = ctx.pages()[0] || await ctx.newPage();
 
-const tries = [
-  `https://www.designersguild.com/en-us/search?q=${encodeURIComponent(base)}`,
-  `https://www.designersguild.com/en-us/search?q=${encodeURIComponent(mfr)}`,
-];
-for (const url of tries) {
-  try {
-    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
-    await page.waitForTimeout(2500);
-    console.log(`\n[search] ${url}\n  title: ${(await page.title().catch(() => '')).slice(0, 80)} @ ${page.url()}`);
-    // click first product result if present
-    const link = await page.$(`a[href*="/product"], a[href*="/p/"], .product a, [class*=product] a`);
+if (isUrl) {
+  await page.goto(arg, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e) => console.log('nav', e.message));
+  await page.waitForTimeout(4000);
+  const info = await page.evaluate(() => {
+    const t = document.body ? document.body.innerText : '';
+    const authed = /log ?out|sign ?out|my account/i.test(t);
+    // any element whose class/id/data mentions price -> its trimmed text
+    const priceEls = [...document.querySelectorAll('[class*=price i],[id*=price i],[data-price],[class*=cost i]')]
+      .map((el) => (el.innerText || el.getAttribute('data-price') || '').trim())
+      .filter((x) => x && x.length < 60);
+    // any number that looks like money even without a currency symbol
+    const nums = [...t.matchAll(/\b\d{2,4}\.\d{2}\b/g)].map((m) => m[0]).slice(0, 20);
+    // links mentioning trade (find the separate Trade Site)
+    const tradeLinks = [...document.querySelectorAll('a[href]')]
+      .filter((a) => /trade/i.test(a.href) || /trade/i.test(a.innerText || ''))
+      .map((a) => `${(a.innerText || '').trim().slice(0, 20)} -> ${a.href}`).slice(0, 12);
+    return { url: location.href, title: document.title, authed, priceEls: [...new Set(priceEls)].slice(0, 20), nums, tradeLinks: [...new Set(tradeLinks)] };
+  });
+  console.log(`authed(hasLogout)=${info.authed}`);
+  console.log(`landed: ${info.title.slice(0, 80)} @ ${info.url}`);
+  console.log(`price ELEMENTS: ${info.priceEls.length ? JSON.stringify(info.priceEls) : '(none)'}`);
+  console.log(`money-like nums: ${info.nums.length ? info.nums.join(' | ') : '(none)'}`);
+  console.log(`trade links:\n  ${info.tradeLinks.length ? info.tradeLinks.join('\n  ') : '(none)'}`);
+  await page.screenshot({ path: 'probe-dg-result.png', fullPage: true }).catch(() => {});
+  console.log('screenshot -> probe-dg-result.png');
+  await page.waitForTimeout(1200);
+  await ctx.close();
+  process.exit(0);
+}
+
+// 1) auth check on the homepage
+await page.goto('https://www.designersguild.com/en-us', { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e) => console.log('nav', e.message));
+await page.waitForTimeout(3000);
+const auth = await page.evaluate(() => {
+  const t = document.body ? document.body.innerText : '';
+  return { loggedOut: /trade login|sign in|log in\b/i.test(t) && !/log ?out|sign ?out|my account/i.test(t), hasLogout: /log ?out|sign ?out|my account/i.test(t) };
+});
+console.log(`auth: hasLogout=${auth.hasLogout}  looksLoggedOut=${auth.loggedOut}  @ ${page.url()}`);
+
+// 2) search via the real search box
+try {
+  const box = await page.$('#product-search-input, input[name="search-term"]');
+  if (box) {
+    await box.click().catch(() => {});
+    await box.fill(base).catch(() => {});
+    const submit = await page.$('#search-box-submit');
+    if (submit) await submit.click().catch(() => {});
+    else await page.keyboard.press('Enter').catch(() => {});
+    await page.waitForTimeout(4000);
+    console.log(`[search "${base}"] -> ${(await page.title().catch(() => '')).slice(0, 60)} @ ${page.url()}`);
+    // click first product result
+    const link = await page.$('a[href*="/wallpaper"], a[href*="/product"], a[href*="/fabric"], .product-tile a, [class*=product] a[href]');
     if (link) {
       const href = await link.getAttribute('href').catch(() => null);
-      if (href) { await page.goto(new URL(href, page.url()).href, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {}); await page.waitForTimeout(2500); }
+      if (href) { await page.goto(new URL(href, page.url()).href, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {}); await page.waitForTimeout(3500); }
     }
-    const info = await page.evaluate(() => {
-      const txt = document.body ? document.body.innerText : '';
-      const prices = [...txt.matchAll(/(?:£|\$|€|USD|GBP)\s?\d[\d.,]*/g)].map((m) => m[0]).slice(0, 25);
-      const codeSeen = /PDG|P\d{3}/i.test(txt);
-      return { url: location.href, title: document.title, prices, codeSeen };
-    });
-    console.log(`  landed: ${info.title.slice(0, 80)} @ ${info.url}`);
-    console.log(`  pattern-code visible on page: ${info.codeSeen}`);
-    console.log(`  price strings: ${info.prices.length ? info.prices.join(' | ') : '(none found)'}`);
-    if (info.prices.length) break;
-  } catch (e) { console.log(`  probe error: ${e.message}`); }
-}
-await page.waitForTimeout(3000);
+  } else { console.log('search box not found'); }
+} catch (e) { console.log('search error:', e.message); }
+
+const info = await page.evaluate(() => {
+  const t = document.body ? document.body.innerText : '';
+  const prices = [...t.matchAll(/(?:£|\$|€|USD|GBP)\s?\d[\d.,]*/g)].map((m) => m[0]).slice(0, 30);
+  const labels = [...t.matchAll(/(trade|net|retail|rrp|your price)[^\n]{0,25}/gi)].map((m) => m[0].trim()).slice(0, 12);
+  return { url: location.href, title: document.title, prices, labels };
+});
+console.log(`landed: ${info.title.slice(0, 70)} @ ${info.url}`);
+console.log(`price strings: ${info.prices.length ? info.prices.join(' | ') : '(none)'}`);
+console.log(`price labels : ${info.labels.length ? info.labels.join(' | ') : '(none)'}`);
+await page.screenshot({ path: 'probe-dg-result.png', fullPage: false }).catch(() => {});
+console.log('screenshot -> probe-dg-result.png');
+await page.waitForTimeout(1500);
 await ctx.close();
-console.log('\nprobe done.');
diff --git a/reset-dg.mjs b/reset-dg.mjs
new file mode 100644
index 0000000..26e35ff
--- /dev/null
+++ b/reset-dg.mjs
@@ -0,0 +1,46 @@
+// reset-dg.mjs — complete the DG consumer-account password reset (Steve-initiated).
+// pw read from /tmp/dg-newpw.txt (never inline). Token from the info@ reset email.
+import { chromium } from 'playwright';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const pw = fs.readFileSync('/tmp/dg-newpw.txt', 'utf8').trim();
+const email = 'info@designerwallcoverings.com';
+const url = 'https://www.designersguild.com/en-us/password-reset/l181?token=68DA5BFD-EAFA-48D6-BC0A-EDBCD830AA5EE9A50F873C1CCCB98E74';
+
+const ctx = await chromium.launchPersistentContext(path.join(process.cwd(), '.auth', 'dg'), {
+  headless: false, channel: 'chrome', viewport: { width: 1440, height: 900 },
+});
+const page = ctx.pages()[0] || await ctx.newPage();
+await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e) => console.log('nav', e.message));
+// Wait out the Cloudflare "security verification" interstitial (real Chrome clears it).
+let ready = false;
+for (let i = 0; i < 20; i++) {
+  await page.waitForTimeout(3000);
+  if (await page.$('#new-password')) { ready = true; break; }
+  const t = await page.title().catch(() => '');
+  if (i % 3 === 0) console.log(`  waiting for reset form... (${(i + 1) * 3}s, title="${t.slice(0, 40)}")`);
+}
+console.log(ready ? '  reset form ready' : '  reset form still not present (Cloudflare may need a hand in the window)');
+
+async function tryFill(sel, val, label) {
+  const el = await page.$(sel);
+  if (!el) { console.log(`${label}: field not found`); return false; }
+  try { await el.fill(val); console.log(`${label}: filled`); return true; }
+  catch (e) { console.log(`${label}: fill error ${e.message.slice(0, 50)}`); return false; }
+}
+await tryFill('#password-reset-email', email, 'reset-email');
+await tryFill('#new-password', pw, 'new-password');
+await tryFill('#new-password-confirm', pw, 'confirm-password');
+
+const save = await page.$('button:has-text("SAVE")');
+if (save) { await save.click().catch(() => {}); console.log('SAVE clicked'); }
+else { await page.keyboard.press('Enter').catch(() => {}); console.log('submitted via Enter'); }
+await page.waitForTimeout(6000);
+
+const body = await page.evaluate(() => (document.body ? document.body.innerText : '')).catch(() => '');
+console.log('post-save url:', page.url());
+console.log('result:', body.replace(/\s+/g, ' ').slice(0, 300));
+await page.screenshot({ path: 'probe-reset.png' }).catch(() => {});
+await page.waitForTimeout(1500);
+await ctx.close();

← 1a1804f Osborne portal probe: reachable, clean login form (no CF), b  ·  back to Eur Recrawl  ·  Price basis RESOLVED (retail=trade/0.65/0.85=x1.810, verifie 54ef49f →