[object Object]

← 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 →