[object Object]

← back to All Dw Landing

auto-save: 2026-07-02T09:16:46 (4 files) — .gitignore config/ scripts/ server.js

9107b64af6abe329ad8d4adce83c375dffb02a12 · 2026-07-02 09:16:49 -0700 · Steve Abrams

Files touched

Diff

commit 9107b64af6abe329ad8d4adce83c375dffb02a12
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 09:16:49 2026 -0700

    auto-save: 2026-07-02T09:16:46 (4 files) — .gitignore config/ scripts/ server.js
---
 .gitignore                   |   9 +++
 config/known-subdomains.json |  40 +++++++++++++
 scripts/build-sites-json.js  | 122 ++++++++++++++++++++++++++++++++++++++
 server.js                    | 137 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 308 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb5ec89
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+data/probe-results.json
diff --git a/config/known-subdomains.json b/config/known-subdomains.json
new file mode 100644
index 0000000..c19434d
--- /dev/null
+++ b/config/known-subdomains.json
@@ -0,0 +1,40 @@
+{
+  "_comment": "Hand-maintained registry of *.designerwallcoverings.com subdomains that are KNOWN to exist (or have a local project). Brand microsites match DB vendors by slug; internal sites are non-brand tools. Edit this file, then re-run scripts/build-sites-json.js.",
+  "brands": [
+    {
+      "slug": "artmura",
+      "vendor": "Artmura",
+      "note": "LIVE reference build for the dw-vendor-landing template",
+      "hasMicrosite": true
+    },
+    {
+      "slug": "astek",
+      "vendor": "Astek",
+      "knownProject": "~/Projects/astek-landing",
+      "localUrl": "http://127.0.0.1:9944",
+      "note": "Internal-only editorial landing (Basic-Auth), running on Mac2 :9944",
+      "hasMicrosite": true
+    },
+    {
+      "slug": "reidwitlin",
+      "vendor": "Reid Witlin",
+      "knownProject": "~/Projects/reidwitlin-landing",
+      "note": "Local project built; deploy status tracked by probe",
+      "hasMicrosite": true
+    },
+    {
+      "slug": "quadrille",
+      "vendor": "Quadrille",
+      "knownProject": "~/Projects/quadrille-house-site",
+      "note": "Local project built; deploy status tracked by probe",
+      "hasMicrosite": true
+    }
+  ],
+  "internal": [
+    { "slug": "new", "label": "New-import catalog viewer", "note": "DO NOT TOUCH the new-import-viewer project (live Shopify write machinery)" },
+    { "slug": "pairs", "label": "Pairs-well-with viewer" },
+    { "slug": "chat", "label": "Chat agent" },
+    { "slug": "appointments", "label": "Appointments / smart scheduling" },
+    { "slug": "all", "label": "This directory site (all-dw-landing)" }
+  ]
+}
diff --git a/scripts/build-sites-json.js b/scripts/build-sites-json.js
new file mode 100644
index 0000000..fc7c166
--- /dev/null
+++ b/scripts/build-sites-json.js
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+/**
+ * all.designerwallcoverings.com — build data/sites.json
+ *
+ * READ-ONLY query against the LOCAL dw_unified mirror: every distinct vendor
+ * with active products (count + one sample product image), slugified to its
+ * candidate <slug>.designerwallcoverings.com subdomain, merged with the
+ * hand-maintained config/known-subdomains.json (existing microsites +
+ * non-brand internal subdomains, marked type:"internal").
+ *
+ * Output: data/sites.json  [{slug, vendor, activeProducts, sampleImage, type, knownProject?, ...}]
+ * $0 (local PG read).
+ */
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { Pool } = require('pg');
+
+const OUT = path.join(__dirname, '..', 'data', 'sites.json');
+const KNOWN = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config', 'known-subdomains.json'), 'utf8'));
+
+// Same connection pattern as astek-landing/scripts/build-products-json.js
+const PW = (() => {
+  try {
+    const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+    const m = env.match(/^DW_ADMIN_DB_PASSWORD=(.*)$/m);
+    if (m) return m[1].replace(/^["']|["']$/g, '').trim();
+  } catch {}
+  return process.env.PGPASSWORD || '';
+})();
+const pool = process.env.DATABASE_URL
+  ? new Pool({ connectionString: process.env.DATABASE_URL })
+  : new Pool({ host: '127.0.0.1', port: 5432, user: 'dw_admin', database: 'dw_unified', password: PW });
+
+// Banned-word scrub on every DISPLAY string (standing rule). Raw vendor stays
+// internal for slug derivation only.
+const clean = s => (s == null ? s : String(s)
+  .replace(/\bWallpapers\b/gi, 'Wallcoverings')
+  .replace(/\bWallpaper\b/gi, 'Wallcovering'));
+
+// vendor -> candidate subdomain slug: lowercase, alphanumeric only.
+const slugify = v => String(v || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/&/g, 'and').replace(/[^a-z0-9]/g, '');
+
+async function main() {
+  // One row per vendor with ≥1 ACTIVE product; sample image = most recent non-null image.
+  const { rows } = await pool.query(`
+    SELECT vendor,
+           COUNT(*)::int AS active_products,
+           (ARRAY_REMOVE(ARRAY_AGG(image_url ORDER BY created_at_shopify DESC NULLS LAST), NULL))[1] AS sample_image
+    FROM shopify_products
+    WHERE UPPER(status) = 'ACTIVE' AND vendor IS NOT NULL AND vendor <> ''
+    GROUP BY vendor
+    ORDER BY COUNT(*) DESC
+  `);
+
+  const knownBrands = new Map((KNOWN.brands || []).map(b => [b.slug, b]));
+  const sites = [];
+  const seenSlugs = new Set();
+
+  for (const r of rows) {
+    const slug = slugify(r.vendor);
+    if (!slug || seenSlugs.has(slug)) { if (seenSlugs.has(slug)) console.warn(`  ! slug collision skipped: ${r.vendor} -> ${slug}`); continue; }
+    seenSlugs.add(slug);
+    const k = knownBrands.get(slug) || {};
+    sites.push({
+      slug,
+      vendor: clean(r.vendor),
+      activeProducts: r.active_products,
+      sampleImage: r.sample_image || null,
+      type: 'brand',
+      hasMicrosite: !!k.hasMicrosite,
+      knownProject: k.knownProject || null,
+      localUrl: k.localUrl || null,
+      note: k.note || null,
+    });
+  }
+
+  // Known brand sites whose vendor slug didn't come out of the active query still get a card.
+  for (const [slug, k] of knownBrands) {
+    if (seenSlugs.has(slug)) continue;
+    seenSlugs.add(slug);
+    sites.push({
+      slug, vendor: clean(k.vendor || slug), activeProducts: 0, sampleImage: null,
+      type: 'brand', hasMicrosite: !!k.hasMicrosite,
+      knownProject: k.knownProject || null, localUrl: k.localUrl || null, note: k.note || null,
+    });
+  }
+
+  // Non-brand internal subdomains.
+  for (const i of (KNOWN.internal || [])) {
+    if (seenSlugs.has(i.slug)) continue;
+    seenSlugs.add(i.slug);
+    sites.push({
+      slug: i.slug, vendor: clean(i.label || i.slug), activeProducts: null, sampleImage: null,
+      type: 'internal', hasMicrosite: null, knownProject: i.knownProject || null,
+      localUrl: i.localUrl || null, note: i.note || null,
+    });
+  }
+
+  const brands = sites.filter(s => s.type === 'brand');
+  const activeBrands = brands.filter(s => s.activeProducts > 0);
+  const withMicrosite = activeBrands.filter(s => s.hasMicrosite);
+
+  const snapshot = {
+    built_at: new Date().toISOString(),
+    source: 'dw_unified.shopify_products (local mirror, read-only) + config/known-subdomains.json',
+    counts: {
+      sites: sites.length,
+      brands: brands.length,
+      activeBrands: activeBrands.length,
+      internal: sites.length - brands.length,
+      brandsWithMicrosite: withMicrosite.length,
+    },
+    sites,
+  };
+  fs.writeFileSync(OUT, JSON.stringify(snapshot, null, 1));
+  console.log(`sites.json -> ${OUT}`);
+  console.log(`  sites: ${sites.length} (brands: ${brands.length}, active brands: ${activeBrands.length}, internal: ${sites.length - brands.length})`);
+  console.log(`  rollout: ${withMicrosite.length} of ${activeBrands.length} active brands have a microsite (${(100 * withMicrosite.length / Math.max(1, activeBrands.length)).toFixed(1)}%)`);
+  await pool.end();
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..16ff472
--- /dev/null
+++ b/server.js
@@ -0,0 +1,137 @@
+/* all.designerwallcoverings.com — internal Basic-Auth directory of every
+   *.designerwallcoverings.com brand-microsite line (rollout tracker).
+   Serves data/sites.json merged with periodic live HTTPS probes of each
+   subdomain. No Cloudflare API dependency. READ-ONLY against the world. */
+const express = require('express');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = Number(process.env.PORT || 9946);
+const DATA = path.join(__dirname, 'data', 'sites.json');
+const PROBE_CACHE = path.join(__dirname, 'data', 'probe-results.json');
+const PROBE_INTERVAL_SEC = Number(process.env.PROBE_INTERVAL_SEC || 900); // 15 min
+const PROBE_TIMEOUT_MS = Number(process.env.PROBE_TIMEOUT_MS || 5000);
+const PROBE_CONCURRENCY = Number(process.env.PROBE_CONCURRENCY || 10);
+
+const app = express();
+
+// HTTP Basic Auth — internal-only gate. Override via BASIC_AUTH="user:pass".
+const [AUTH_USER, AUTH_PASS] = (process.env.BASIC_AUTH || 'admin:DW2024!').split(':');
+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(':');
+    if (u === AUTH_USER && p === AUTH_PASS) return next();
+  }
+  res.set('WWW-Authenticate', 'Basic realm="All DW Microsites (internal)"');
+  return res.status(401).send('Authentication required.');
+});
+
+app.use(express.json({ limit: '64kb' }));
+
+// ── manifest ────────────────────────────────────────────────────────────────
+let SNAP = { counts: {}, sites: [] };
+function load() {
+  SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
+  console.log(`[all-dw] loaded ${SNAP.sites.length} sites (built ${SNAP.built_at})`);
+}
+load();
+fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
+  if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[all-dw] reload failed:', e.message); } }
+});
+
+// ── probe loop ──────────────────────────────────────────────────────────────
+// status per slug: PROBING (not yet probed) | LIVE-GATED (401) | LIVE-OPEN (2xx/3xx)
+// | BROKEN (404/other 4xx/5xx) | MISSING (DNS / SSL / timeout / conn failure)
+let PROBES = {};             // slug -> { status, httpStatus, detail, lastProbe, ms }
+try { PROBES = JSON.parse(fs.readFileSync(PROBE_CACHE, 'utf8')); } catch {}
+let LAST_CYCLE = null;       // ISO of last completed full cycle
+let CYCLE_RUNNING = false;
+
+function probeOne(slug) {
+  return new Promise((resolve) => {
+    const host = `${slug}.designerwallcoverings.com`;
+    const t0 = Date.now();
+    const done = (status, httpStatus, detail) => resolve({
+      status, httpStatus: httpStatus || null, detail: detail || null,
+      lastProbe: new Date().toISOString(), ms: Date.now() - t0,
+    });
+    const req = https.get({ host, path: '/', timeout: PROBE_TIMEOUT_MS, headers: { 'User-Agent': 'all-dw-landing-probe/1.0 (internal rollout tracker)' } }, (res) => {
+      res.resume(); // discard body
+      const c = res.statusCode;
+      if (c === 401) return done('LIVE-GATED', c, 'exists behind auth');
+      if (c >= 200 && c < 400) return done('LIVE-OPEN', c);
+      return done('BROKEN', c, `HTTP ${c}`);
+    });
+    req.on('timeout', () => { req.destroy(new Error('timeout')); });
+    req.on('error', (e) => done('MISSING', null, e.code || e.message));
+  });
+}
+
+async function runProbeCycle(reason) {
+  if (CYCLE_RUNNING) return { skipped: true, reason: 'cycle already running' };
+  CYCLE_RUNNING = true;
+  const slugs = SNAP.sites.map(s => s.slug);
+  console.log(`[all-dw] probe cycle start (${slugs.length} subdomains, reason=${reason})`);
+  const queue = [...slugs];
+  const workers = Array.from({ length: Math.min(PROBE_CONCURRENCY, queue.length) }, async () => {
+    while (queue.length) {
+      const slug = queue.shift();
+      PROBES[slug] = await probeOne(slug);
+    }
+  });
+  await Promise.all(workers);
+  LAST_CYCLE = new Date().toISOString();
+  try { fs.writeFileSync(PROBE_CACHE, JSON.stringify(PROBES)); } catch {}
+  CYCLE_RUNNING = false;
+  const tally = {};
+  for (const p of Object.values(PROBES)) tally[p.status] = (tally[p.status] || 0) + 1;
+  console.log(`[all-dw] probe cycle done: ${JSON.stringify(tally)}`);
+  return { done: true, tally };
+}
+runProbeCycle('boot');                                   // serve immediately; results fill in
+setInterval(() => runProbeCycle('interval'), PROBE_INTERVAL_SEC * 1000).unref();
+
+// ── API ─────────────────────────────────────────────────────────────────────
+function merged() {
+  return SNAP.sites.map(s => {
+    const pr = PROBES[s.slug] || { status: 'PROBING', httpStatus: null, detail: null, lastProbe: null, ms: null };
+    return { ...s, url: `https://${s.slug}.designerwallcoverings.com/`, probe: pr };
+  });
+}
+
+app.get('/api/sites', (_req, res) => {
+  const sites = merged();
+  const brands = sites.filter(s => s.type === 'brand' && s.activeProducts > 0);
+  const live = new Set(['LIVE-GATED', 'LIVE-OPEN']);
+  const withSite = brands.filter(s => s.hasMicrosite || live.has(s.probe.status));
+  res.json({
+    built_at: SNAP.built_at,
+    counts: { ...SNAP.counts, brandsWithMicrosite: withSite.length },
+    rollout: {
+      withMicrosite: withSite.length,
+      activeBrands: brands.length,
+      pct: brands.length ? +(100 * withSite.length / brands.length).toFixed(1) : 0,
+    },
+    sites,
+  });
+});
+
+app.get('/api/meta', (_req, res) => res.json({
+  lastRefresh: LAST_CYCLE || SNAP.built_at,
+  intervalSec: PROBE_INTERVAL_SEC,
+  probing: CYCLE_RUNNING,
+  count: SNAP.sites.length,
+  now: new Date().toISOString(),
+}));
+
+app.post('/api/probe', async (_req, res) => {
+  const r = await runProbeCycle('manual');
+  res.json(r);
+});
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.listen(PORT, () => console.log(`all-dw-landing → http://127.0.0.1:${PORT}  (Basic Auth ${AUTH_USER}/***)`));

(oldest)  ·  back to All Dw Landing  ·  register internal viewers: quadrille :9947, artmura :9948, r cbfb266 →