[object Object]

← back to Designer Wallcoverings

Add Daisy Bennett Vol 2 internal staging viewer (sort+density, settlement badges, basic auth)

ad9acbf8b70d2370c21be4e9224136c9d86827fa · 2026-07-06 11:35:30 -0700 · Steve

Files touched

Diff

commit ad9acbf8b70d2370c21be4e9224136c9d86827fa
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 11:35:30 2026 -0700

    Add Daisy Bennett Vol 2 internal staging viewer (sort+density, settlement badges, basic auth)
---
 scripts/wallquest-refresh/dbn-v2-viewer/.gitignore |   2 +
 .../dbn-v2-viewer/public/index.html                | 112 +++++++++++++++++++++
 scripts/wallquest-refresh/dbn-v2-viewer/server.cjs |  32 ++++++
 3 files changed, 146 insertions(+)

diff --git a/scripts/wallquest-refresh/dbn-v2-viewer/.gitignore b/scripts/wallquest-refresh/dbn-v2-viewer/.gitignore
new file mode 100644
index 00000000..ddbfab34
--- /dev/null
+++ b/scripts/wallquest-refresh/dbn-v2-viewer/.gitignore
@@ -0,0 +1,2 @@
+img/
+public/data.json
diff --git a/scripts/wallquest-refresh/dbn-v2-viewer/public/index.html b/scripts/wallquest-refresh/dbn-v2-viewer/public/index.html
new file mode 100644
index 00000000..56fb167e
--- /dev/null
+++ b/scripts/wallquest-refresh/dbn-v2-viewer/public/index.html
@@ -0,0 +1,112 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Daisy Bennett Naturals Vol 2 — staging</title>
+<style>
+  :root{--cols:5;--fs:13px}
+  *{box-sizing:border-box}
+  body{margin:0;font:var(--fs)/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:#1a1a1a;background:#faf9f7}
+  header{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e5e1da;padding:10px 16px;display:flex;flex-wrap:wrap;gap:14px;align-items:center}
+  h1{font-size:15px;font-weight:600;margin:0;letter-spacing:.3px}
+  .sub{color:#8a8378;font-size:12px}
+  .ctl{display:flex;align-items:center;gap:6px;font-size:12px;color:#6b6459}
+  select,input[type=range]{font:inherit}
+  select{padding:4px 6px;border:1px solid #d5cfc4;border-radius:6px;background:#fff}
+  .wrap{display:flex;gap:0;align-items:flex-start}
+  aside{position:sticky;top:52px;width:220px;flex:0 0 220px;padding:14px 12px;max-height:calc(100vh - 52px);overflow:auto;border-right:1px solid #eee}
+  aside h3{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#a29a8c;margin:16px 0 6px}
+  .frow{display:flex;justify-content:space-between;align-items:center;padding:3px 6px;border-radius:5px;cursor:pointer;color:#4a453d}
+  .frow:hover{background:#f0ece4}
+  .frow.active{background:#0f6b5c;color:#fff}
+  .frow .cnt{background:#e8e3d9;color:#6b6459;border-radius:10px;padding:0 7px;font-size:11px}
+  .frow.active .cnt{background:rgba(255,255,255,.25);color:#fff}
+  main{flex:1;padding:14px 16px}
+  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
+  .card{background:#fff;border:1px solid #e7e2d9;border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+  .card img{width:100%;aspect-ratio:1/1;object-fit:cover;background:#f0ece4;display:block}
+  .meta{padding:7px 9px;display:flex;flex-direction:column;gap:2px}
+  .fam{font-weight:600;font-size:calc(var(--fs) + 0px)}
+  .cw{color:#6b6459}
+  .sku{color:#a29a8c;font-size:11px;font-family:ui-monospace,Menlo,monospace}
+  .price{font-weight:600;color:#0f6b5c}
+  .strike{color:#b6ab9a;text-decoration:none;font-weight:400;font-size:11px}
+  .when{color:#a29a8c;font-size:11px}
+  .badge{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.04em;padding:2px 7px;border-radius:20px;width:fit-content}
+  .OK{background:#e3f3ec;color:#0f6b5c}
+  .REVIEW{background:#fcf3d9;color:#8a6d16}
+  .BLOCK{background:#fbe4e0;color:#b3311f}
+  .banner{background:#fff8e9;border:1px solid #f0d999;color:#7a5f11;padding:7px 12px;border-radius:8px;margin:0 0 12px;font-size:12px}
+</style></head><body>
+<header>
+  <h1>Daisy Bennett Naturals <span style="color:#0f6b5c">Vol 2</span></h1>
+  <span class="sub" id="count">…</span>
+  <span class="sub">· Phillipe Romano (private label) · <b>staging — not live</b></span>
+  <span style="flex:1"></span>
+  <label class="ctl">Sort
+    <select id="sort">
+      <option value="fam">Family</option>
+      <option value="new">Newest</option>
+      <option value="sku">DW SKU</option>
+      <option value="plo">Price ↑</option>
+      <option value="phi">Price ↓</option>
+      <option value="set">Settlement</option>
+    </select></label>
+  <label class="ctl">Density <input type="range" id="dens" min="3" max="9" value="5"></label>
+</header>
+<div class="wrap">
+  <aside>
+    <h3>Settlement</h3><div id="fSet"></div>
+    <h3>Family</h3><div id="fFam"></div>
+  </aside>
+  <main>
+    <div class="banner">⛔ <b>BLOCK</b> rows must not publish (Blossom Citrus Floral = chinoiserie with a bird). ⚠️ <b>REVIEW</b> needs the formal settlement gate. Pattern names are NULL — the Volume-1 session assigns them.</div>
+    <div class="grid" id="grid"></div>
+  </main>
+</div>
+<script>
+let DATA=[], fSet=null, fFam=null;
+const $=s=>document.querySelector(s);
+const money=v=>v==null?'—':'$'+Number(v).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
+const fmtWhen=iso=>{try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch(e){return''}};
+function counts(key){const m={};DATA.forEach(r=>{const k=r[key]||'—';m[k]=(m[k]||0)+1});return m;}
+function sidebar(){
+  const setC=counts('settlement_status'), famC=counts('family');
+  const order={BLOCK:0,REVIEW:1,OK:2};
+  $('#fSet').innerHTML=Object.entries(setC).sort((a,b)=>(order[a[0]]??9)-(order[b[0]]??9)).map(([k,n])=>
+    `<div class="frow ${fSet===k?'active':''}" data-set="${k}"><span>${k}</span><span class="cnt">${n}</span></div>`).join('');
+  $('#fFam').innerHTML=Object.entries(famC).sort().map(([k,n])=>
+    `<div class="frow ${fFam===k?'active':''}" data-fam="${k}"><span>${k}</span><span class="cnt">${n}</span></div>`).join('');
+  document.querySelectorAll('[data-set]').forEach(e=>e.onclick=()=>{fSet=fSet===e.dataset.set?null:e.dataset.set;render()});
+  document.querySelectorAll('[data-fam]').forEach(e=>e.onclick=()=>{fFam=fFam===e.dataset.fam?null:e.dataset.fam;render()});
+}
+function render(){
+  let rows=DATA.filter(r=>(!fSet||r.settlement_status===fSet)&&(!fFam||r.family===fFam));
+  const s=$('#sort').value;
+  const cmp={fam:(a,b)=>(a.family+a.dw_sku).localeCompare(b.family+b.dw_sku),
+    new:(a,b)=>new Date(b.created_at)-new Date(a.created_at),
+    sku:(a,b)=>a.dw_sku.localeCompare(b.dw_sku),
+    plo:(a,b)=>(a.our_price||0)-(b.our_price||0),
+    phi:(a,b)=>(b.our_price||0)-(a.our_price||0),
+    set:(a,b)=>({BLOCK:0,REVIEW:1,OK:2}[a.settlement_status]-{BLOCK:0,REVIEW:1,OK:2}[b.settlement_status])||a.family.localeCompare(b.family)};
+  rows.sort(cmp[s]);
+  $('#count').textContent=`${rows.length} of ${DATA.length} SKUs · ${new Set(DATA.map(r=>r.family)).size} families`;
+  $('#grid').innerHTML=rows.map(r=>`
+    <div class="card">
+      <img loading="lazy" src="/img/${r.dw_sku}.jpg" alt="${r.family} ${r.color_name}">
+      <div class="meta">
+        <span class="badge ${r.settlement_status}">${r.settlement_status}</span>
+        <span class="fam">${r.family}</span>
+        <span class="cw">${r.color_name||''}</span>
+        <span class="sku">${r.dw_sku} · ${r.mfr_sku}</span>
+        <span><span class="price">${money(r.our_price)}</span> <span class="strike">cost ${money(r.cost)}</span></span>
+        <span class="when" title="${r.created_at}">🕓 ${fmtWhen(r.created_at)}</span>
+      </div>
+    </div>`).join('');
+  sidebar();
+}
+$('#sort').onchange=()=>{localStorage.dbnSort=$('#sort').value;render()};
+$('#dens').oninput=e=>{document.documentElement.style.setProperty('--cols',e.target.value);localStorage.dbnDens=e.target.value};
+// restore prefs
+if(localStorage.dbnSort)$('#sort').value=localStorage.dbnSort;
+if(localStorage.dbnDens){$('#dens').value=localStorage.dbnDens;document.documentElement.style.setProperty('--cols',localStorage.dbnDens);}
+fetch(location.origin+'/public/data.json').then(r=>r.json()).then(d=>{DATA=d;render()});
+</script></body></html>
diff --git a/scripts/wallquest-refresh/dbn-v2-viewer/server.cjs b/scripts/wallquest-refresh/dbn-v2-viewer/server.cjs
new file mode 100644
index 00000000..7c776c0b
--- /dev/null
+++ b/scripts/wallquest-refresh/dbn-v2-viewer/server.cjs
@@ -0,0 +1,32 @@
+// Zero-dependency internal viewer for Daisy Bennett Naturals Vol 2 staging (65 SKUs).
+// Basic auth admin/DW2024!. Serves public/ + local img/. STAGING view only — nothing live.
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const DIR = __dirname;
+const USER = 'admin', PASS = 'DW2024!';
+const MIME = { '.html': 'text/html; charset=utf-8', '.json': 'application/json', '.js': 'text/javascript', '.css': 'text/css', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png' };
+
+const server = http.createServer((req, res) => {
+  const hdr = req.headers.authorization || '';
+  const [, b64] = hdr.split(' ');
+  const [u, p] = Buffer.from(b64 || '', 'base64').toString().split(':');
+  if (u !== USER || p !== PASS) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DBN V2"' }); return res.end('Auth required'); }
+
+  let rel = decodeURIComponent((req.url.split('?')[0]) || '/');
+  if (rel === '/') rel = '/public/index.html';
+  else if (rel.startsWith('/img/')) rel = rel; // served from DIR/img
+  else if (!rel.startsWith('/public/') && !rel.startsWith('/img/')) rel = '/public' + rel;
+  const file = path.join(DIR, rel);
+  if (!file.startsWith(DIR)) { res.writeHead(403); return res.end('no'); }
+  fs.readFile(file, (e, data) => {
+    if (e) { res.writeHead(404); return res.end('not found'); }
+    res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
+    res.end(data);
+  });
+});
+server.listen(0, '127.0.0.1', () => {
+  const { port } = server.address();
+  console.log(`DBN-V2 viewer → http://127.0.0.1:${port}/  (admin / DW2024!)`);
+  fs.writeFileSync('/tmp/dbn-v2-viewer.port', String(port));
+});

← 0862568a Daisy Bennett: review viewer + Shopify draft payloads (63 pr  ·  back to Designer Wallcoverings  ·  Daisy Bennett: material-based SKU prefixes (DWGRS/PWV/RAF/SI 4511065a →