← back to Designer Wallcoverings
auto-save: 2026-07-09T23:42:48 (4 files) — pending-approval/boost-filter-consolidation-2026-06-25 shopify/scripts/set-doubleroll-lengths.py vendor-scrapers/china-seas-refresh DW-Programming/ImportNewSkufromURL/yw-costcheck.js
a4a39262d79cb99f2417229b12bbaebd01f552b3 · 2026-07-09 23:42:52 -0700 · Steve Abrams
Files touched
A DW-Programming/ImportNewSkufromURL/yw-costcheck.jsM shopify/scripts/set-doubleroll-lengths.py
Diff
commit a4a39262d79cb99f2417229b12bbaebd01f552b3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 9 23:42:52 2026 -0700
auto-save: 2026-07-09T23:42:48 (4 files) — pending-approval/boost-filter-consolidation-2026-06-25 shopify/scripts/set-doubleroll-lengths.py vendor-scrapers/china-seas-refresh DW-Programming/ImportNewSkufromURL/yw-costcheck.js
---
DW-Programming/ImportNewSkufromURL/yw-costcheck.js | 92 ++++++++++++++++++++++
shopify/scripts/set-doubleroll-lengths.py | 21 ++++-
2 files changed, 111 insertions(+), 2 deletions(-)
diff --git a/DW-Programming/ImportNewSkufromURL/yw-costcheck.js b/DW-Programming/ImportNewSkufromURL/yw-costcheck.js
new file mode 100644
index 00000000..43a9b747
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/yw-costcheck.js
@@ -0,0 +1,92 @@
+// Full cost/stock pull for one Brewster/York MFR SKU via the yorkwall trade portal.
+// One login, then internal navigation (does NOT re-lock). Aborts hard if the
+// account still reads locked. Password supplied via YW_PASS env (never written).
+const fs = require('fs'), os = require('os'), path = require('path');
+try { require('dotenv').config(); } catch { /* dotenv optional */ }
+const { chromium } = require('playwright');
+const md = fs.readFileSync(path.join(os.homedir(), '.claude/skills/brewster-scraper-manager/SKILL.md'), 'utf8');
+// Prefer the secrets-routed YORKWALL_* (loaded from .env); fall back to inline YW_* / skill username.
+const user = (process.env.YORKWALL_USER || process.env.YW_USER || md.match(/\*\*Username\*\*\s*\|\s*`([^`]+)`/)[1]).trim();
+const pass = (process.env.YORKWALL_PASS || process.env.YW_PASS || '').trim();
+const MFR = process.argv[2] || '437-RD336';
+
+const priceStockDump = () => {
+ const seen = new Set(), hits = [];
+ for (const el of document.querySelectorAll('*')) {
+ if (el.children.length) continue;
+ const t = (el.textContent || '').trim();
+ if (!t || t.length > 90) continue;
+ if (/\$\s?\d|\bprice\b|\bstock\b|\bavailab|\bin[-\s]?stock|\bout of stock|\bqty\b|\bbackorder|\bunit\b|\bper roll\b|\byd\b|discontinu/i.test(t)) {
+ if (!seen.has(t)) { seen.add(t); hits.push(t); }
+ }
+ }
+ return hits.slice(0, 50);
+};
+
+(async () => {
+ const out = { mfr: MFR };
+ const b = await chromium.launch({ headless: true, channel: 'chrome' });
+ const ctx = await b.newContext();
+ const p = await ctx.newPage();
+ try {
+ await p.goto('https://www.yorkwall.com/Login/', { waitUntil: 'networkidle' });
+ await p.fill('#CustomerNumber', user);
+ await p.fill('#Password', pass);
+ await Promise.all([
+ p.waitForNavigation({ timeout: 25000 }).catch(() => {}),
+ p.click('input[type="submit"],button[type="submit"]')
+ ]);
+ await p.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
+
+ const bodyText = await p.evaluate(() => document.body.innerText);
+ out.locked = /account has been locked/i.test(bodyText);
+ out.authError = /user name or password provided is incorrect/i.test(bodyText);
+ out.url = p.url();
+ out.loggedIn = !out.locked && !out.authError && !/\/Login\/?$|SignIn/i.test(p.url());
+
+ // HARD ABORT if still locked or bad auth — do not hammer the portal.
+ if (out.locked || out.authError || !out.loggedIn) {
+ out.abort = out.locked ? 'STILL_LOCKED' : (out.authError ? 'AUTH_REJECTED' : 'NOT_LOGGED_IN');
+ console.log(JSON.stringify(out, null, 2));
+ await b.close();
+ return;
+ }
+
+ // Logged in. Discover the landing page's search + nav.
+ out.landing = { title: await p.title(), url: p.url() };
+ out.searchInputs = await p.$$eval('input', is => is
+ .filter(i => /search|find|sku|term|keyword|\bq\b/i.test((i.name || '') + ' ' + (i.id || '') + ' ' + (i.placeholder || '')))
+ .map(i => ({ name: i.name, id: i.id, ph: i.placeholder }))).catch(() => []);
+ out.navLinks = await p.$$eval('a[href]', as => [...new Set(as.map(a => a.getAttribute('href'))
+ .filter(h => h && /search|product|catalog|browse|find|book/i.test(h)))].slice(0, 25)).catch(() => []);
+
+ // Attempt to reach the product. Try a search box first, then URL shapes.
+ let onProduct = false;
+ if (out.searchInputs.length) {
+ const sel = out.searchInputs[0].id ? `#${out.searchInputs[0].id}` : `input[name="${out.searchInputs[0].name}"]`;
+ await p.fill(sel, MFR).catch(() => {});
+ await Promise.all([p.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}), p.press(sel, 'Enter').catch(() => {})]);
+ out.afterSearch = p.url();
+ }
+ for (const u of [
+ `https://www.yorkwall.com/${MFR}`,
+ `https://www.yorkwall.com/product/${MFR}`,
+ `https://www.yorkwall.com/Products/${MFR}`,
+ `https://www.brewsterwallcovering.com/${MFR.toLowerCase()}-brooke-anaglypta`
+ ]) {
+ const r = await p.goto(u, { waitUntil: 'networkidle', timeout: 20000 }).catch(() => null);
+ const st = r ? r.status() : 'ERR';
+ out.steps = out.steps || []; out.steps.push([u, st, p.url()]);
+ if (st === 200 && !/\/Login/i.test(p.url())) { onProduct = true; break; }
+ }
+
+ out.priceStock = await p.evaluate(priceStockDump).catch(() => []);
+ out.pageTitle = await p.title().catch(() => null);
+ await p.screenshot({ path: '/tmp/yw-product.png' }).catch(() => {});
+ } catch (e) {
+ out.fatal = e.message;
+ } finally {
+ await b.close();
+ }
+ console.log(JSON.stringify(out, null, 2));
+})();
diff --git a/shopify/scripts/set-doubleroll-lengths.py b/shopify/scripts/set-doubleroll-lengths.py
index 4f6f8add..35f4bf7c 100644
--- a/shopify/scripts/set-doubleroll-lengths.py
+++ b/shopify/scripts/set-doubleroll-lengths.py
@@ -267,7 +267,20 @@ def main():
continue
sqft = wi/12 * lf
- if SINGLE_LO <= sqft <= SINGLE_HI:
+ # PRIMARY: length-based single/double discriminator (width-INDEPENDENT), gated to
+ # plausible roll widths (<=60"; wider = mural/panoramic, not a roll) and only the
+ # UNAMBIGUOUS length zones. The sqft windows conflate width, so a narrow 20.5"x9yd
+ # bolt (46 sqft) or a wide 36"x11yd bolt (99 sqft) fell in the sqft dead-zone though
+ # the LENGTH clearly marks them double — that stranded real WallQuest bolts as ODD.
+ # A single roll runs <=5.5yd (<=16.5 ft); a double/bolt >=8yd (>=24 ft). The 6-7.5yd
+ # gray zone (e.g. Thibaut 36"x6yd, genuinely single-OR-double) and wide/panoramic
+ # items fall through to the sqft classifier UNCHANGED, so we never flip an ambiguous
+ # product (the S/R-D/R mismatch class). This adds coverage; it never re-guesses.
+ if wi <= 60 and lf <= 16.5:
+ n_single += 1; single = L; double = scale_tokens(L, 2.0); kind = "single"
+ elif wi <= 60 and lf >= 24:
+ n_double += 1; single = scale_tokens(L, 0.5); double = L; kind = "bolt"
+ elif SINGLE_LO <= sqft <= SINGLE_HI:
n_single += 1; single = L; double = scale_tokens(L, 2.0); kind = "single"
elif DOUBLE_LO <= sqft <= DOUBLE_HI:
n_double += 1; single = scale_tokens(L, 0.5); double = L; kind = "bolt"
@@ -279,7 +292,11 @@ def main():
if len(cls_samples) < 6:
cls_samples.append(f"{dwsku:16} {W:16} L={L!r:20} {sqft:.0f}sqft {kind}: S/R={single!r} D/R={double!r}")
# a "correction" = we're changing an existing double_roll_length to a different value
- if cur_dr and cur_dr != double: corrections += 1
+ if cur_dr and cur_dr != double:
+ corrections += 1
+ if os.environ.get("DR_DEBUG"):
+ sys.stderr.write(f"CORRECTION {dwsku:14} W={W!r:18} L={L!r:16} {kind:6} "
+ f"cur[S/R={cur_sr!r} D/R={cur_dr!r}] -> new[S/R={single!r} D/R={double!r}]\n")
Wdisp = (p["w"] or {}).get("value") or W
if cur_sr != single: inputs.append(mf(pid,"single_roll_length",single)); lenw+=1
← d4b939ce WallQuest onboard: gated Phillipe Romano draft-push (per-yar
·
back to Designer Wallcoverings
·
stage yorkwall trade-portal cost/stock scraper + overnight o 9d2b6d26 →