[object Object]

← back to Sanderson Onboard

Sanderson: harvest product width from public retail API ($0, no auth) into sanderson_catalog.width

fa05ca1b86680f6fa12015bcf2ed5438c5ee74a0 · 2026-08-10 10:08:51 -0700 · Steve Abrams

The trade portal carries price but not width (original TK-00058 Tier-1 blocker).
Width lives on www.sanderson.design product records; one call per SKU via
/api/n/find?type=product&filter={sku} → sdb_usable_width_inches/sdb_useable_width,
stored in the DW sibling convention '20.47 in  (52 cm)'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit fa05ca1b86680f6fa12015bcf2ed5438c5ee74a0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 10:08:51 2026 -0700

    Sanderson: harvest product width from public retail API ($0, no auth) into sanderson_catalog.width
    
    The trade portal carries price but not width (original TK-00058 Tier-1 blocker).
    Width lives on www.sanderson.design product records; one call per SKU via
    /api/n/find?type=product&filter={sku} → sdb_usable_width_inches/sdb_useable_width,
    stored in the DW sibling convention '20.47 in  (52 cm)'.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/harvest_width.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/scripts/harvest_width.js b/scripts/harvest_width.js
new file mode 100644
index 0000000..85f523a
--- /dev/null
+++ b/scripts/harvest_width.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+// Harvest Sanderson product WIDTH from the PUBLIC retail API ($0, no auth) into
+// sanderson_catalog.width. The trade portal carries price but NOT width; width
+// lives on www.sanderson.design's product record (sdb_usable_width_inches / sdb_useable_width).
+//
+// Endpoint (proven via network capture): one call per SKU, keyed by sku —
+//   GET /api/n/find?type=product&verbosity=3&filter={"sku":"<SKU>"}&limit=1
+//   → .catalog[] entry where .sku===SKU has sdb_usable_width_inches ("20.47 in")
+//     and sdb_useable_width ("52 cm").
+// Stored in the DW sibling convention: "20.47 in  (52 cm)".
+//
+// Usage: node scripts/harvest_width.js            (all null-width rows)
+//        node scripts/harvest_width.js --all      (re-harvest every row)
+const https = require('https');
+const { execFileSync } = require('child_process');
+
+const ALL = process.argv.includes('--all');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
+
+function get(url) {
+  return new Promise((resolve) => {
+    https.get(url, { headers: { 'User-Agent': UA, 'Accept': 'application/json' } }, res => {
+      let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
+    }).on('error', () => resolve(null));
+  });
+}
+const psql = sql => execFileSync('psql', ['-h', '/tmp', '-d', 'dw_unified', '-tAc', sql], { encoding: 'utf8' }).trim();
+const psqlFile = f => execFileSync('psql', ['-h', '/tmp', '-d', 'dw_unified', '-q', '-v', 'ON_ERROR_STOP=1', '-f', f], { encoding: 'utf8' });
+const q = s => "'" + String(s).replace(/'/g, "''") + "'";
+const numFrom = s => { const m = String(s == null ? '' : s).match(/-?\d+(\.\d+)?/); return m ? m[0] : null; };
+
+(async () => {
+  const where = ALL ? "mfr_sku is not null" : "mfr_sku is not null and width is null";
+  const skus = psql(`select mfr_sku from sanderson_catalog where ${where} order by mfr_sku`).split('\n').filter(Boolean);
+  console.log(`[width] ${skus.length} SKUs to harvest (${ALL ? 'all' : 'null-width only'})`);
+  const updates = []; let hit = 0, miss = 0;
+  for (let i = 0; i < skus.length; i++) {
+    const sku = skus[i];
+    const filter = encodeURIComponent(JSON.stringify({ sku }));
+    const url = `https://www.sanderson.design/api/n/find?type=product&verbosity=3&filter=${filter}&limit=1`;
+    const d = await get(url);
+    const cat = (d && Array.isArray(d.catalog)) ? d.catalog.find(c => c.sku === sku) || d.catalog[0] : null;
+    const inch = cat && numFrom(cat.sdb_usable_width_inches);
+    const cm = cat && numFrom(cat.sdb_useable_width);
+    if (inch) { const w = cm ? `${inch} in  (${cm} cm)` : `${inch} in`; updates.push(`update sanderson_catalog set width=${q(w)} where mfr_sku=${q(sku)};`); hit++; }
+    else miss++;
+    if ((i + 1) % 25 === 0 || i === skus.length - 1) console.log(`[width] ${i + 1}/${skus.length}  hit=${hit} miss=${miss}`);
+    await sleep(150);
+  }
+  if (updates.length) {
+    const fs = require('fs'), tmp = '/tmp/sanderson_width_updates.sql';
+    fs.writeFileSync(tmp, 'begin;\n' + updates.join('\n') + '\ncommit;\n');
+    psqlFile(tmp);
+  }
+  console.log(`[width] DONE: wrote ${hit} widths, ${miss} misses. Fill now: ${psql("select count(width)||'/'||count(*) from sanderson_catalog")}`);
+})();

← c895b56 TK-10400 Cody-gate fixes: reauth->openclaw, tab-race, freshn  ·  back to Sanderson Onboard  ·  chore: harvest_width.js refactor (hoist fs require) + v1.0.1 e9c105a →