← back to All Designerwallcoverings
auto-save: 2026-07-09T22:12:24 (1 files) — server.js
f37876cb7634d4dd9967a33742527cb3e3ee7cc4 · 2026-07-09 22:12:25 -0700 · Steve Abrams
Files touched
Diff
commit f37876cb7634d4dd9967a33742527cb3e3ee7cc4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 9 22:12:25 2026 -0700
auto-save: 2026-07-09T22:12:24 (1 files) — server.js
---
server.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 48 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index b12101e..9d44706 100644
--- a/server.js
+++ b/server.js
@@ -46,8 +46,11 @@ const PRICE_BANDS = [
];
const PRICE_ORDER = PRICE_BANDS.map((b) => b.key);
-// Three-way lifecycle Steve lists on this surface, ordered live → not-yet-live → gone.
-const LIFE_ORDER = ['Active on site', 'Staged', 'Archived'];
+// Lifecycle Steve lists on this surface, ordered live → not-yet-live → internal-only → gone.
+// 'Internal' = deliberately never front-facing (permanent), distinct from 'Staged' (a candidate
+// to go live). Internal lines STAY on this internal grid + facets; they are excluded only from
+// the genuinely front-facing paths (public storefront URL + Google feed) — see isInternalRow.
+const LIFE_ORDER = ['Active on site', 'Staged', 'Internal', 'Archived'];
const priceBand = (p) => { if (!p || p <= 0) return null; const b = PRICE_BANDS.find((x) => x.test(p)); return b ? b.key : null; };
const titleCase = (s) => s.replace(/\w\S*/g, (t) => t[0].toUpperCase() + t.slice(1));
@@ -83,6 +86,39 @@ const cleanMfr = (raw) => String(raw || '').split(/--|·|;|\bMUST\b|\bQUOTE\b/i)
// whose vendor OR title leaks one are excluded (their products surface under the label).
const BANNED = /brewster|york|wallquest|wall\s*quest|newwall|new\s*wall\b|command\s*54|justin\s*david|nicolette\s*mayer|seabrook|chesapeake|nextwall|desima|carlsten/i;
+// ── INTERNAL lines — deliberately NEVER front-facing (permanent), distinct from Staged ────────────
+// The durable registry lives in the shopify repo (config/internal-lines.json). A product whose
+// VENDOR is in that registry, OR that carries the `internal` tag, is INTERNAL: it STAYS on this
+// internal grid + facets (all.dw is an internal tool) with lifecycle 'Internal', but is EXCLUDED
+// from every genuinely front-facing output (public storefront URL + Google feed). Reused via the
+// shopify repo's config; falls back to the four known luxury lines if the file can't be read.
+const INTERNAL_REGISTRY_PATHS = [
+ path.join(os.homedir(), 'Projects', 'Designer-Wallcoverings', 'shopify', 'config', 'internal-lines.json'),
+ '/root/Projects/Designer-Wallcoverings/shopify/config/internal-lines.json', // Kamatera path
+];
+let INTERNAL_VENDORS = new Set(['fromental', 'gracie', 'zuber', 'crezana design']);
+let INTERNAL_TAG = 'internal';
+(function loadInternalRegistry() {
+ for (const p of INTERNAL_REGISTRY_PATHS) {
+ try {
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
+ if (Array.isArray(j.vendors) && j.vendors.length) INTERNAL_VENDORS = new Set(j.vendors.map((v) => String(v).toLowerCase().trim()));
+ if (typeof j.tag === 'string' && j.tag.trim()) INTERNAL_TAG = j.tag.trim().toLowerCase();
+ console.log(`internal-lines registry loaded from ${p}: ${[...INTERNAL_VENDORS].join(', ')}`);
+ return;
+ } catch { /* try next path */ }
+ }
+ console.log('internal-lines registry not found — using built-in fallback (fromental/gracie/zuber/crezana design)');
+})();
+// True when a DB/microsite row is an INTERNAL line — vendor in the registry OR the `internal` tag.
+function isInternalRow(r) {
+ const v = String(r.vendor || '').toLowerCase().trim();
+ if (v && INTERNAL_VENDORS.has(v)) return true;
+ const tags = String(r.tags || '').toLowerCase();
+ if (tags && new RegExp(`(^|,)\\s*${INTERNAL_TAG}\\s*($|,)`).test(tags)) return true;
+ return false;
+}
+
// ── INTERNAL full-catalog feed sanitizer (/api/catalog-full) ──────────────────────
// The internal feed (auth-gated) INCLUDES private-label products so the substitute + photo
// tools see EVERY SKU — but the real upstream manufacturer name must NEVER be emitted, even
@@ -345,7 +381,11 @@ function deriveRow(r) {
// "Active on site" only when the ACTIVE product is actually published to the Online
// Store — Shopify status and sales-channel publish state are orthogonal, so an
// ACTIVE-but-unpublished SKU (never went live) reads as Staged, same as a DRAFT.
+ // INTERNAL is checked FIRST (before Staged/Active): a registry vendor or `internal`-tagged
+ // product is deliberately never front-facing, so it reads 'Internal' regardless of Shopify
+ // status — EXCEPT still honor 'Archived' (a dead internal line is gone, not internal-live).
const lifecycle = (r.status === 'ARCHIVED' || r.status === 'DELETED_FROM_SHOPIFY') ? 'Archived'
+ : isInternalRow(r) ? 'Internal'
: r.status === 'DRAFT' ? 'Staged'
: (r.status === 'ACTIVE' && r.online_store_published) ? 'Active on site'
: 'Staged';
@@ -405,7 +445,9 @@ function deriveRow(r) {
materials: [...new Set(materials)],
image_state: primaryImg ? 'Has image' : 'No image',
// Website = customer-facing storefront page (only when actually live on the Online Store).
- url: (r.online_store_published && r.handle) ? `${STOREFRONT}/products/${r.handle}` : null,
+ // Internal lines are deliberately never front-facing → never emit a public storefront URL,
+ // even in the impossible case one is published (defense-in-depth behind the activation gate).
+ url: (r.online_store_published && r.handle && !isInternalRow(r)) ? `${STOREFRONT}/products/${r.handle}` : null,
// Internal cluster — three destinations per SKU, each resolves or the control dims:
// Admin = Shopify admin product view (any row with a shopify_id GID),
// Line Viewer = the vendor's live internal microsite (astek-landing style),
@@ -526,7 +568,9 @@ function deriveMicrositeRow(p, feed) {
fm_present: false, fm_mfr_number: null, fm_discontinued: false, fm_disco_date: null,
fm_name: null, fm_width: null, fm_specs: null, fm_handle: null, mfr_mismatch: false,
status: 'STAGED',
- lifecycle: 'Staged', // these lines are staged (not on the customer store)
+ // The 4 microsite lines (Fromental/Gracie/Zuber/Crezana) ARE the internal registry vendors,
+ // so they read 'Internal' when registered (never front-facing), else fall back to 'Staged'.
+ lifecycle: isInternalRow({ vendor: feed.vendor }) ? 'Internal' : 'Staged',
image: primaryImg,
images,
image_count: images.length,
← 9d3836e microsite crawler: seed from Cloudflare zone (authoritative,
·
back to All Designerwallcoverings
·
internal category: plug 3 front-facing leaks — build-data.py 1fa387b →