[object Object]

← 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

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 →