← 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
M package-lock.jsonM package.jsonM scripts/build-products-json.jsM scripts/verify-e2e.mjsM server.js
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)