← back to Dw Universe

server.js

183 lines

// DW Universe — fleet-wide search + constellation across all DW family niche sites.
// Boots by reading every ~/Projects/<slug>/data/products.json into memory once.
// Exposes /api/search and /api/sites; serves single-page UI from /public.

const express = require('express');
const fs = require('fs');
const path = require('path');
const os = require('os');

const PORT = process.env.PORT || 9466;
const HOME = os.homedir();

// 41 active niches (3 redirected ones excluded: recycled, string, embroidered).
// Category groups the constellation visually + tints each node.
const NICHES = [
  // material
  { slug: 'silkwallpaper',          label: 'Silk',                cat: 'material' },
  { slug: 'silkwallcoverings',      label: 'Silk Wallcoverings',  cat: 'material' },
  { slug: 'linenwallpaper',         label: 'Linen',               cat: 'material' },
  { slug: 'jutewallpaper',          label: 'Jute',                cat: 'material' },
  { slug: 'raffiawallcoverings',    label: 'Raffia',              cat: 'material' },
  { slug: 'raffiawalls',            label: 'Raffia Walls',        cat: 'material' },
  { slug: 'corkwallcovering',       label: 'Cork',                cat: 'material' },
  { slug: 'fabricwallpaper',        label: 'Fabric',              cat: 'material' },
  { slug: 'textilewallpaper',       label: 'Textile',             cat: 'material' },
  { slug: 'metallicwallpaper',      label: 'Metallic',            cat: 'material' },
  { slug: 'silverleafwallpaper',    label: 'Silver Leaf',         cat: 'material' },
  { slug: 'vinylwallpaper',         label: 'Vinyl',               cat: 'material' },
  { slug: 'micawallpaper',          label: 'Mica',                cat: 'material' },
  { slug: 'madagascarwallpaper',    label: 'Madagascar',          cat: 'material' },
  { slug: 'mylarwallpaper',         label: 'Mylar',               cat: 'material' },
  { slug: 'suedewallpaper',         label: 'Suede',               cat: 'material' },
  // decade
  { slug: '1800swallpaper',         label: '1800s',               cat: 'decade' },
  { slug: '1890swallpaper',         label: '1890s',               cat: 'decade' },
  { slug: '1900swallpaper',         label: '1900s',               cat: 'decade' },
  { slug: '1920swallpaper',         label: '1920s',               cat: 'decade' },
  { slug: '1930swallpaper',         label: '1930s',               cat: 'decade' },
  { slug: '1940swallpaper',         label: '1940s',               cat: 'decade' },
  { slug: '1950swallpaper',         label: '1950s',               cat: 'decade' },
  { slug: '1960swallpaper',         label: '1960s',               cat: 'decade' },
  { slug: '1970swallpaper',         label: '1970s',               cat: 'decade' },
  { slug: '1980swallpaper',         label: '1980s',               cat: 'decade' },
  { slug: 'retrowalls',             label: 'Retro Walls',         cat: 'decade' },
  { slug: 'wallpapersback',         label: 'Wallpapers Back',     cat: 'decade' },
  // craft / aesthetic
  { slug: 'agedwallpaper',          label: 'Aged',                cat: 'craft' },
  { slug: 'handcraftedwallpaper',   label: 'Handcrafted',         cat: 'craft' },
  { slug: 'museumwallpaper',        label: 'Museum',              cat: 'craft' },
  { slug: 'restorationwallpaper',   label: 'Restoration',         cat: 'craft' },
  { slug: 'pastelwallpaper',        label: 'Pastel',              cat: 'craft' },
  { slug: 'glitterwalls',           label: 'Glitter',             cat: 'craft' },
  { slug: 'greenwallcoverings',     label: 'Green',               cat: 'craft' },
  { slug: 'naturalwallcoverings',   label: 'Natural',             cat: 'craft' },
  { slug: 'saloonwallpaper',        label: 'Saloon',              cat: 'craft' },
  // use-case
  { slug: 'contractwallpaper',      label: 'Contract',            cat: 'use' },
  { slug: 'hotelwallcoverings',     label: 'Hotel',               cat: 'use' },
  { slug: 'hospitalitywallpaper',   label: 'Hospitality',         cat: 'use' },
  { slug: 'healthcarewallpaper',    label: 'Healthcare',          cat: 'use' },
  { slug: 'restaurantwallpaper',    label: 'Restaurant',          cat: 'use' },
  { slug: 'architecturalwallcoverings', label: 'Architectural',   cat: 'use' },
];

// Boot: load every site's product cache + niche poems + neighbor map + pair rules.
let POEMS = {}, NEIGHBORS = {}, PAIR_RULES = {}, PAIR_BLURBS = [];
try { POEMS = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'niche-poems.json'), 'utf8')); } catch (e) { console.warn('[dw-universe] niche-poems.json:', e.message); }
try { NEIGHBORS = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'niche-neighbors.json'), 'utf8')); } catch (e) { console.warn('[dw-universe] niche-neighbors.json:', e.message); }
try {
  const pr = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'pair-rules.json'), 'utf8'));
  PAIR_RULES = pr.rules || {};
  PAIR_BLURBS = pr.blurbs || [];
} catch (e) { console.warn('[dw-universe] pair-rules.json:', e.message); }

const DB = []; // flattened: each row tagged with slug + label + cat
const SITE_STATS = {}; // slug -> { count, label, cat, domain, poem }
for (const n of NICHES) {
  const f = path.join(HOME, 'Projects', n.slug, 'data', 'products.json');
  let products = [];
  try { products = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { /* skip missing */ }
  SITE_STATS[n.slug] = {
    count: products.length, label: n.label, cat: n.cat,
    domain: n.slug + '.com', poem: POEMS[n.slug] || '',
    neighbors: NEIGHBORS[n.slug] || [],
  };
  for (const p of products) DB.push({ ...p, _slug: n.slug, _niche: n.label, _cat: n.cat });
}
console.log(`[dw-universe] loaded ${DB.length} products across ${NICHES.length} niches`);

// Search: simple substring match across title/tags/vendor with niche-aware ranking.
function search(q, limit = 24) {
  if (!q) return [];
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
  const scored = [];
  for (const p of DB) {
    const hay = [p.title || '', (p.tags || []).join(' '), p.vendor || '', p._niche]
      .join(' ').toLowerCase();
    let score = 0;
    for (const t of terms) {
      if (!hay.includes(t)) { score = 0; break; }
      if ((p.title || '').toLowerCase().includes(t)) score += 3;
      else if ((p.tags || []).some(tg => tg.toLowerCase().includes(t))) score += 2;
      else score += 1;
    }
    if (score > 0) scored.push([score, p]);
  }
  scored.sort((a, b) => b[0] - a[0]);
  return scored.slice(0, limit).map(([s, p]) => ({
    sku: p.sku, title: p.title, image_url: p.image_url, vendor: p.vendor,
    max_price: p.max_price, niche: p._niche, slug: p._slug,
    product_url: p.product_url || `https://${p._slug}.com/`,
  }));
}

const app = express();
app.disable('x-powered-by');

app.get('/api/search', (req, res) => {
  const q = String(req.query.q || '').slice(0, 200);
  const t = Date.now();
  const results = search(q, 24);
  res.json({ q, count: results.length, ms: Date.now() - t, results });
});

app.get('/api/sites', (req, res) => {
  const total = Object.values(SITE_STATS).reduce((s, x) => s + x.count, 0);
  res.json({ total_products: total, niches: SITE_STATS });
});

// Pairing Oracle — given a sku from any niche, return one cross-niche soulmate + an editorial blurb.
app.get('/api/pair', (req, res) => {
  const sku = String(req.query.sku || '').slice(0, 200);
  const slug = String(req.query.slug || '').slice(0, 64);
  if (!sku) return res.status(400).json({ error: 'sku required' });
  const source = DB.find(p => p.sku === sku && (!slug || p._slug === slug));
  if (!source) return res.status(404).json({ error: 'source product not found' });
  const candidates = PAIR_RULES[source._slug] || [];
  // P1: filter candidates + fallback to only niches with loaded products
  const validCandidates = candidates.filter(s => SITE_STATS[s] && SITE_STATS[s].count > 0);
  let targetSlug = validCandidates[Math.floor(Math.random() * validCandidates.length)];
  if (!targetSlug) {
    const otherSlugs = NICHES.map(n => n.slug).filter(s => s !== source._slug && SITE_STATS[s] && SITE_STATS[s].count > 0);
    targetSlug = otherSlugs[Math.floor(Math.random() * otherSlugs.length)];
  }
  const pool = DB.filter(p => p._slug === targetSlug && p.image_url);
  if (!pool.length) return res.status(404).json({ error: 'no candidates in target niche' });
  const paired = pool[Math.floor(Math.random() * pool.length)];
  const blurb = PAIR_BLURBS[Math.floor(Math.random() * PAIR_BLURBS.length)] || '';
  function shape(p) {
    return {
      sku: p.sku, title: p.title, image_url: p.image_url, vendor: p.vendor,
      max_price: p.max_price, niche: p._niche, slug: p._slug,
      product_url: p.product_url || `https://${p._slug}.com/`,
    };
  }
  res.json({ source: shape(source), paired: shape(paired), blurb });
});

app.get('/api/sample', (req, res) => {
  // 24 random products to seed the page before user types
  const out = [];
  const seen = new Set();
  while (out.length < 24 && seen.size < DB.length) {
    const i = Math.floor(Math.random() * DB.length);
    if (seen.has(i)) continue;
    seen.add(i);
    const p = DB[i];
    if (!p.image_url) continue;
    out.push({
      sku: p.sku, title: p.title, image_url: p.image_url, vendor: p.vendor,
      max_price: p.max_price, niche: p._niche, slug: p._slug,
      product_url: p.product_url || `https://${p._slug}.com/`,
    });
  }
  res.json({ results: out });
});

app.use(express.static(path.join(__dirname, 'public')));

app.listen(PORT, '127.0.0.1', () => {
  console.log(`[dw-universe] http://127.0.0.1:${PORT}/  (${DB.length} products · ${NICHES.length} niches)`);
});