← 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
A scripts/wallquest-refresh/dbn-v2-viewer/.gitignoreA scripts/wallquest-refresh/dbn-v2-viewer/public/index.htmlA scripts/wallquest-refresh/dbn-v2-viewer/server.cjs
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 →