[object Object]

← back to Schumacher Internal

chore: lint, refactor, v0.3.0 (session close) — RFC7617 auth split, fresh-deploy load guard, build-script cleanup, rename astek→schumacher

eec73e2e92ce0f9a1e25d7fc28cb6ed9450753f6 · 2026-07-16 17:20:57 -0700 · Steve Abrams

Files touched

Diff

commit eec73e2e92ce0f9a1e25d7fc28cb6ed9450753f6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 17:20:57 2026 -0700

    chore: lint, refactor, v0.3.0 (session close) — RFC7617 auth split, fresh-deploy load guard, build-script cleanup, rename astek→schumacher
---
 package-lock.json              |  4 +--
 package.json                   |  6 ++--
 scripts/build-products-json.js | 68 ++++++++++++++++++++++++++----------------
 scripts/verify-e2e.mjs         |  4 ++-
 server.js                      |  5 ++--
 5 files changed, 54 insertions(+), 33 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index a962cc4..1511e8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "astek-landing",
-  "version": "0.2.1",
+  "version": "0.3.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "astek-landing",
-      "version": "0.2.1",
+      "version": "0.3.0",
       "dependencies": {
         "compression": "^1.8.1",
         "express": "^4.19.2",
diff --git a/package.json b/package.json
index c7325d6..53798b3 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
-  "name": "astek-landing",
-  "version": "0.2.1",
+  "name": "schumacher-internal",
+  "version": "0.3.0",
   "private": true,
-  "description": "Editorial vendor landing for Astek (INTERNAL dw_unified data — not on Shopify)",
+  "description": "Internal-only Schumacher line viewer (INTERNAL dw_unified data — never customer-facing)",
   "scripts": {
     "scrape": "node scripts/scrape-astek.js",
     "build-products": "node scripts/build-products-json.js",
diff --git a/scripts/build-products-json.js b/scripts/build-products-json.js
index 8bc3cf6..d260a5f 100644
--- a/scripts/build-products-json.js
+++ b/scripts/build-products-json.js
@@ -12,6 +12,11 @@ const path = require('path');
 const { Pool } = require('pg');
 
 const OUT = path.join(__dirname, '..', 'data', 'products.json');
+
+// Facet slice limits
+const MAX_SERIES = 300;
+const MAX_STYLES =  60;
+
 const PW = (() => {
   const env = fs.readFileSync(require('os').homedir() + '/Projects/secrets-manager/.env', 'utf8');
   const m = env.match(/^DW_ADMIN_DB_PASSWORD=(.*)$/m);
@@ -50,22 +55,25 @@ const cap = s => String(s || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.t
 function bookFor(pt) {
   const t = (pt || '').toLowerCase();
   if (t.includes('memo') || t.includes('sample')) return 'Memo Sample';
-  if (t.includes('trim')) return 'Trim';
-  if (t.includes('fabric')) return 'Fabric';
-  if (t.includes('pillow')) return 'Pillow';
-  if (t.includes('wall')) return 'Wallcovering';
+  if (t.includes('trim'))    return 'Trim';
+  if (t.includes('fabric'))  return 'Fabric';
+  if (t.includes('pillow'))  return 'Pillow';
   return 'Wallcovering';
 }
 
 function parseTags(raw) {
   if (!raw) return [];
-  let s = String(raw).trim();
+  const s = String(raw).trim();
   if (s.startsWith('{') && s.endsWith('}')) {           // pg array text form {"a","b"}
     return s.slice(1, -1).split(',').map(t => t.replace(/^"|"$/g, '').trim()).filter(Boolean);
   }
   return s.split(',').map(t => t.trim()).filter(Boolean); // csv form
 }
 
+function parseMetafields(raw) {
+  try { return typeof raw === 'string' ? JSON.parse(raw) : (raw || {}); } catch { return {}; }
+}
+
 function colorFromTags(tags) {
   // prefer an explicit "color:Xyz" structured tag, else first bucket-matching tag
   let name = null, bucket = null;
@@ -79,12 +87,21 @@ function colorFromTags(tags) {
   }
   return { name, bucket };
 }
+
 function styleFromTags(tags) {
   const out = [];
   for (const t of tags) { if (STYLES.has(t.toLowerCase().trim())) out.push(cap(t)); }
   return [...new Set(out)];
 }
 
+/** Run a query with a no-rows fallback on error (e.g. missing table in some envs). */
+async function querySafe(q, params) {
+  return (await pool.query(q, params).catch(() => ({ rows: [] }))).rows[0] || {};
+}
+
+/** Increment a facet counter. */
+function tally(obj, key) { if (key) obj[key] = (obj[key] || 0) + 1; }
+
 async function main() {
   const { rows } = await pool.query(`
     SELECT dw_sku, mfr_sku, handle, title, product_type, body_html, tags, metafields,
@@ -97,16 +114,15 @@ async function main() {
   `);
 
   // vendor ops meta for the internal PDP (Call Vendor / Our Account #). Defaults are fine if absent.
-  const VENDOR_CODE_REG = 'schumacher', FM_VENDOR_NAME = 'Schumacher';
-  const vr = (await pool.query(
+  const vr = await querySafe(
     `SELECT vendor_name, vendor_discount_pct, pricing_unit, pricing_model, pricing_notes, sample_price
-     FROM vendor_registry WHERE vendor_code = $1`, [VENDOR_CODE_REG]).catch(() => ({ rows: [] }))).rows[0] || {};
-  const fm = (await pool.query(
+     FROM vendor_registry WHERE vendor_code = $1`, ['schumacher']);
+  const fm = await querySafe(
     `SELECT phone, email_1, account_num FROM fmpro
      WHERE vendor_name ILIKE $1 AND account_num IS NOT NULL LIMIT 1`,
-    ['%schumacher%']).catch(() => ({ rows: [] }))).rows[0] || {};
+    ['%schumacher%']);
   const vendorMeta = {
-    name: vr.vendor_name || FM_VENDOR_NAME,
+    name: vr.vendor_name || 'Schumacher',
     phone: fm.phone || null, email: fm.email_1 || null, account_number: fm.account_num || null,
     discount_pct: vr.vendor_discount_pct != null ? Number(vr.vendor_discount_pct) : 15, // Schumacher trade = 15%
     pricing_unit: vr.pricing_unit || null, pricing_model: vr.pricing_model || 'quote',
@@ -115,13 +131,17 @@ async function main() {
   };
 
   const products = [];
+  const facets = { books: {}, series: {}, colors: {}, styles: {}, statuses: {} };
+
   for (const r of rows) {
     const tags = parseTags(r.tags);
-    let mf = {}; try { mf = typeof r.metafields === 'string' ? JSON.parse(r.metafields) : (r.metafields || {}); } catch { mf = {}; }
+    const mf = parseMetafields(r.metafields);
     const patternName = mf?.custom?.pattern_name?.value
       || (r.title || '').split(/\s*[|—-]\s*/)[0].trim() || null;
     const { name: color, bucket } = colorFromTags(tags);
     const styles = styleFromTags(tags);
+    const book = bookFor(r.product_type);
+
     products.push({
       handle: `${r.handle || 'schu'}--${(r.dw_sku || '').toLowerCase()}`,
       dw_sku: r.dw_sku,
@@ -131,7 +151,7 @@ async function main() {
       display_name: clean(r.title),
       series: clean(patternName) || null,
       color: color || null,
-      book: bookFor(r.product_type),
+      book,
       style: styles[0] || null,
       styles,
       color_bucket: bucket,
@@ -149,16 +169,14 @@ async function main() {
       published_at: r.created_at,
       inquiry_sku: r.dw_sku,
     });
-  }
 
-  const facets = { books: {}, series: {}, colors: {}, styles: {}, statuses: {} };
-  for (const p of products) {
-    if (p.book) facets.books[p.book] = (facets.books[p.book] || 0) + 1;
-    if (p.series) facets.series[p.series] = (facets.series[p.series] || 0) + 1;
-    if (p.color_bucket) facets.colors[p.color_bucket] = (facets.colors[p.color_bucket] || 0) + 1;
-    for (const s of p.styles) facets.styles[s] = (facets.styles[s] || 0) + 1;
-    if (p.status) facets.statuses[p.status] = (facets.statuses[p.status] || 0) + 1;
+    tally(facets.books, book);
+    tally(facets.series, clean(patternName) || null);
+    tally(facets.colors, bucket);
+    for (const s of styles) tally(facets.styles, s);
+    tally(facets.statuses, r.status || null);
   }
+
   const orderedColors = BUCKET_ORDER.filter(b => facets.colors[b]).map(b => [b, facets.colors[b]]);
 
   const snapshot = {
@@ -168,10 +186,10 @@ async function main() {
     vendor: vendorMeta,
     facets: {
       total: products.length,
-      books: Object.entries(facets.books).sort((a, b) => b[1] - a[1]),
-      series: Object.entries(facets.series).sort((a, b) => b[1] - a[1]).slice(0, 300),
-      colors: orderedColors,
-      styles: Object.entries(facets.styles).sort((a, b) => b[1] - a[1]).slice(0, 60),
+      books:    Object.entries(facets.books).sort((a, b) => b[1] - a[1]),
+      series:   Object.entries(facets.series).sort((a, b) => b[1] - a[1]).slice(0, MAX_SERIES),
+      colors:   orderedColors,
+      styles:   Object.entries(facets.styles).sort((a, b) => b[1] - a[1]).slice(0, MAX_STYLES),
       statuses: Object.entries(facets.statuses).sort((a, b) => b[1] - a[1]),
     },
     products,
diff --git a/scripts/verify-e2e.mjs b/scripts/verify-e2e.mjs
index 2c13b9f..217b56c 100644
--- a/scripts/verify-e2e.mjs
+++ b/scripts/verify-e2e.mjs
@@ -1,6 +1,8 @@
 // Spec verification gate: structural DOM invariant on Chromium + WebKit-with-creds-in-URL.
 import { chromium, webkit } from 'playwright';
-const BASE = 'http://admin:DW2024!@127.0.0.1:9946/';
+const AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
+const PORT = process.env.PORT || 9946;
+const BASE = `http://${AUTH}@127.0.0.1:${PORT}/`;
 async function run(engine, name) {
   const b = await engine.launch();
   const p = await b.newPage();
diff --git a/server.js b/server.js
index d2fc9fa..ac81db0 100644
--- a/server.js
+++ b/server.js
@@ -17,12 +17,12 @@ app.get('/healthz', (_req, res) => res.json({ ok: true, products: SNAP.products.
 
 // HTTP Basic Auth — internal-only gate (username + password). Override via
 // BASIC_AUTH="user:pass"; defaults to the DW standard admin / DW2024!.
-const [AUTH_USER, AUTH_PASS] = (process.env.BASIC_AUTH || 'admin:DW2024!').split(':');
+const _ba = (process.env.BASIC_AUTH || 'admin:DW2024!'); const AUTH_USER = _ba.slice(0, _ba.indexOf(':')); const AUTH_PASS = _ba.slice(_ba.indexOf(':') + 1);
 app.use((req, res, next) => {
   const hdr = req.headers.authorization || '';
   const [scheme, b64] = hdr.split(' ');
   if (scheme === 'Basic' && b64) {
-    const [u, p] = Buffer.from(b64, 'base64').toString('utf8').split(':');
+    const _decoded = Buffer.from(b64, 'base64').toString('utf8'); const u = _decoded.slice(0, _decoded.indexOf(':')); const p = _decoded.slice(_decoded.indexOf(':') + 1);
     if (u === AUTH_USER && p === AUTH_PASS) return next();
   }
   res.set('WWW-Authenticate', 'Basic realm="Schumacher - Designer Wallcoverings (INTERNAL ONLY)"');
@@ -36,6 +36,7 @@ let LIGHT = []; // grid-index payload — heavy PDP-only fields stripped
 let LAST_REFRESH = new Date().toISOString();
 const REFRESH_INTERVAL_SEC = Number(process.env.REFRESH_INTERVAL_SEC || 900); // 15 min, matches the cron
 function load() {
+  if (!fs.existsSync(DATA)) { console.warn('[schumacher] products.json not built yet — run scripts/build-products-json.js'); return; }
   SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
   // the index grid never renders body_html / images[] / eyebrow — keep those PDP-only
   LIGHT = SNAP.products.map(({ body_html, images, display_eyebrow, published_at, ...p }) => p);

← f3a51a8 update deploy port from 9946 (collision with fentucci-site)  ·  back to Schumacher Internal  ·  (newest)