← 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
A inspect-dg.mjsA inspect.mjsM login.mjsM probe-dg.mjsA reset-dg.mjs
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 →