[object Object]

← back to Wallco Ai

server: mount /admin/inspirations + /api/admin/inspirations/list + badge API

c79c5bcb1e44350c4c4657a861ce3818ee3ea008 · 2026-05-20 00:00:12 -0700 · Steve Abrams

Three new routes wired off the canonical isAdmin() gate (src/admin-gate.js):

  GET  /admin/inspirations                       admin HTML page
  GET  /api/admin/inspirations/list              JSON: seed × output pairs
  POST /api/admin/inspirations/regenerate        re-spawn generator for sku
  GET  /api/design/:id/inspired-by-badge         PUBLIC — boolean only, no
                                                 vendor name in response

The badge endpoint is the only inspired-by surface that's NOT admin-gated —
returns { inspired: true|false } with no vendor identity. The /design/:id
template fetches it client-side to show a "Inspired by an archival reference"
chip without ever leaking the source SKU or brand. Vendor identity stays
strictly in /admin/inspirations behind the admin gate.

Smoke test:
  /admin/inspirations          200 (localhost = admin)
  /api/admin/inspirations/list 5 rows (all 5 smoke-test designs)
  /api/design/9870/inspired-by-badge  {"inspired":true}
  /api/design/1/inspired-by-badge     {"inspired":false}
  /design/9870 — 0 vendor name occurrences in HTML

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit c79c5bcb1e44350c4c4657a861ce3818ee3ea008
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 20 00:00:12 2026 -0700

    server: mount /admin/inspirations + /api/admin/inspirations/list + badge API
    
    Three new routes wired off the canonical isAdmin() gate (src/admin-gate.js):
    
      GET  /admin/inspirations                       admin HTML page
      GET  /api/admin/inspirations/list              JSON: seed × output pairs
      POST /api/admin/inspirations/regenerate        re-spawn generator for sku
      GET  /api/design/:id/inspired-by-badge         PUBLIC — boolean only, no
                                                     vendor name in response
    
    The badge endpoint is the only inspired-by surface that's NOT admin-gated —
    returns { inspired: true|false } with no vendor identity. The /design/:id
    template fetches it client-side to show a "Inspired by an archival reference"
    chip without ever leaking the source SKU or brand. Vendor identity stays
    strictly in /admin/inspirations behind the admin gate.
    
    Smoke test:
      /admin/inspirations          200 (localhost = admin)
      /api/admin/inspirations/list 5 rows (all 5 smoke-test designs)
      /api/design/9870/inspired-by-badge  {"inspired":true}
      /api/design/1/inspired-by-badge     {"inspired":false}
      /design/9870 — 0 vendor name occurrences in HTML
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/server.js b/server.js
index ac22649..ddee727 100644
--- a/server.js
+++ b/server.js
@@ -908,6 +908,13 @@ const { isAdmin, requireAdmin } = require('./src/admin-gate');
 try { require('./src/fliepaper-bugs').mount(app, { isAdmin }); }
 catch (e) { console.error('Fliepaper-bugs mount failed:', e.message); }
 
+// ── /library — 57k professional vendor wallpapers from dw_unified
+// surfaced as a first-class catalog under the Designer Wallcoverings brand.
+// Vendor names are NEVER rendered. See src/library.js for the redaction +
+// sort + render pipeline. Cache builds via scripts/build-library-cache.js.
+try { require('./src/library').mount(app, { htmlHead, htmlHeader, getDesigns: () => DESIGNS }); }
+catch (e) { console.error('Library mount failed:', e.message); }
+
 // ── /admin/reload-designs — re-read data/designs.json without a pm2 reload.
 // Localhost-admin per src/admin-gate. Used by drunk-animals tick + future
 // generators to surface freshly persisted designs in the live grid without
@@ -923,6 +930,92 @@ app.post('/admin/reload-designs', (req, res) => {
   res.json({ ok: true, before, after: DESIGNS.length, catalog_last_modified: CATALOG_LAST_MODIFIED, by_color_cache_ver: _byColorCacheVer });
 });
 
+// ── Inspirations gallery (admin-only) ─────────────────────────────────────
+// Side-by-side viewer: real archival seed (vendor visible) ON LEFT, the
+// AI-generated wallco design (NO vendor leak) ON RIGHT. Powered by the new
+// scripts/inspiration_generator.js pipeline. Vendor identity is ADMIN-ONLY —
+// surfaced here, NEVER on /design/:id.
+app.get('/admin/inspirations', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  res.sendFile(path.join(__dirname, 'public', 'admin', 'inspirations.html'));
+});
+
+app.get('/api/admin/inspirations/list', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const limit  = Math.min(Math.max(parseInt(req.query.limit  || '50', 10), 1), 200);
+  const offset = Math.max(parseInt(req.query.offset || '0',  10), 0);
+  try {
+    const sql = `
+      SELECT json_agg(t) FROM (
+        SELECT
+          d.id, d.inspired_by_sku, d.inspired_by_vendor,
+          d.dominant_hex, d.palette, d.is_published, d.image_url,
+          d.local_path, d.created_at, d.prompt, d.notes,
+          sp.title         AS seed_title,
+          sp.image_url     AS seed_image_url,
+          sp.handle        AS seed_handle,
+          sp.product_type  AS seed_product_type,
+          sce.dominant_hex AS seed_dominant_hex,
+          sce.hex_codes    AS seed_hex_codes,
+          sce.design_era   AS seed_era
+        FROM all_designs d
+        LEFT JOIN shopify_products sp
+          ON sp.sku     = d.inspired_by_sku
+          OR sp.mfr_sku = d.inspired_by_sku
+          OR sp.handle  = d.inspired_by_sku
+        LEFT JOIN shopify_color_enrichment sce
+          ON sce.shopify_id = sp.shopify_id
+        WHERE d.inspired_by_sku IS NOT NULL
+        ORDER BY d.id DESC
+        LIMIT ${limit} OFFSET ${offset}
+      ) t;
+    `;
+    const raw = psqlQuery(sql);
+    const rows = raw ? JSON.parse(raw) : [];
+    res.json({ ok: true, count: (rows || []).length, rows: rows || [] });
+  } catch (e) {
+    console.error('inspirations list failed:', e.message);
+    res.status(500).json({ error: 'query failed', detail: e.message.slice(0, 200) });
+  }
+});
+
+// Per-design inspired-by lookup — minimal, NO vendor name in response so the
+// public-facing /design/:id template can use it safely. Vendor identity stays
+// behind the admin gate on /api/admin/inspirations/list.
+app.get('/api/design/:id/inspired-by-badge', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id)) return res.status(400).json({ error: 'bad id' });
+  try {
+    const sql = `SELECT (inspired_by_sku IS NOT NULL) AS inspired FROM all_designs WHERE id=${id};`;
+    const v = psqlQuery(sql);
+    const inspired = (v === 't' || v === 'true' || v === 'TRUE');
+    res.set('Cache-Control', 'public, max-age=600');
+    res.json({ inspired });
+  } catch (e) {
+    res.json({ inspired: false });
+  }
+});
+
+// Trigger another generation from the same seed (admin-only). Spawns the
+// generator as a detached child so the HTTP response returns fast.
+app.post('/api/admin/inspirations/regenerate', express.json(), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const sku = String(req.body?.sku || '').trim();
+  if (!sku) return res.status(400).json({ error: 'sku required' });
+  try {
+    const child = require('child_process').spawn(
+      'node',
+      [path.join(__dirname, 'scripts', 'inspiration_generator.js'), '--n', '1'],
+      { detached: true, stdio: 'ignore',
+        env: { ...process.env, INSPIRATION_BUDGET_N: '1', INSPIRATION_REGEN_SKU: sku } }
+    );
+    child.unref();
+    res.json({ ok: true, queued: true, sku });
+  } catch (e) {
+    res.status(500).json({ error: 'spawn failed', detail: e.message });
+  }
+});
+
 // ── Merge ────────────────────────────────────────────────────────────────
 // Take 1-3 designs (catalog IDs and/or uploaded files), pull element
 // descriptions from each, fuse into a single SDXL prompt, generate a new
@@ -6159,6 +6252,27 @@ ${htmlHeader('/designs')}
         })();
         </script>
 
+        <!-- ── Inspired-by-archival badge (public; NO vendor name reveal) ───
+             Hits /api/design/:id/inspired-by-badge → { inspired: bool }. The
+             admin-only inspired_by_vendor column NEVER appears here — that
+             metadata lives behind /admin/inspirations. -->
+        <div id="inspired-by-badge" data-design-id="${design.id}"
+             style="display:none;margin:14px 0 4px;padding:10px 14px;background:#f7f3eb;border:1px solid #e0d4b8;border-radius:6px;font:12px var(--sans,system-ui);color:#5a4628;letter-spacing:.03em;">
+          <span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#c9a14b;margin-right:8px;vertical-align:middle;"></span>
+          <strong style="font:600 10.5px var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:#8a6f44;">Inspired by an archival reference</strong>
+          <span style="display:block;margin-top:3px;color:#7a6e5a;font-size:11.5px;">Anchored to authentic designer-wallpaper DNA, reinterpreted as an original wallco composition.</span>
+        </div>
+        <script>
+        (function(){
+          var el = document.getElementById('inspired-by-badge');
+          if (!el) return;
+          fetch('/api/design/' + el.getAttribute('data-design-id') + '/inspired-by-badge', { credentials:'omit' })
+            .then(function(r){ return r.ok ? r.json() : null; })
+            .then(function(d){ if (d && d.inspired === true) el.style.display = 'block'; })
+            .catch(function(){});
+        })();
+        </script>
+
         ${design.category === 'mural-scenic' ? `
         <!-- ── LICENSE CTA — $950 infinite license + 150dpi download ─── -->
         <div id="license-panel" data-design-id="${design.id}"

← fd7b53d wallco /designs — remove studio-adjust modal (6 Photoshop-st  ·  back to Wallco Ai  ·  add /library route + APIs — surface 57k professional vendor 3cd8e28 →