[object Object]

โ† back to Wallco Ai

cactus-curator: hover any chip to swap flat tile โ†’ room mockup (๐Ÿ›‹ badge marks the 164/3951 chips with room data; same hover-swap on by-base family cards)

5f263d7f8e284f568176d28a9f461e40c26f1794 ยท 2026-05-28 12:45:30 -0700 ยท Steve

Files touched

Diff

commit 5f263d7f8e284f568176d28a9f461e40c26f1794
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu May 28 12:45:30 2026 -0700

    cactus-curator: hover any chip to swap flat tile โ†’ room mockup (๐Ÿ›‹ badge marks the 164/3951 chips with room data; same hover-swap on by-base family cards)
---
 public/admin/cactus-curator.html | 30 ++++++++++++++++++++++++++++++
 server.js                        |  4 ++++
 2 files changed, 34 insertions(+)

diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index 1fe0848..8cac933 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -29,6 +29,12 @@
   .card.sel { outline:2px solid #4aa3ff; outline-offset:-2px; }
   .thumb { position:relative; aspect-ratio:1/1; background:#000; }
   .thumb img { width:100%; height:100%; object-fit:cover; display:block; -webkit-user-drag:none; user-select:none; }
+  /* On-hover room mockup overlay โ€” Steve 2026-05-28. Fades in over the flat
+     tile so the curator can see how the pattern reads on a wall in context. */
+  .thumb .card-room { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; opacity:0; transition:opacity .18s ease-out; pointer-events:none; z-index:5; }
+  .card:hover .thumb .card-room { opacity:1; }
+  /* Tiny corner badge so Steve sees which chips have a room mockup at all */
+  .thumb .b.room { background:#0ea5e9; color:#fff; }
   .sel-box { position:absolute; top:6px; left:6px; width:20px; height:20px; border-radius:5px; border:2px solid #fff;
              background:rgba(0,0,0,.45); cursor:pointer; z-index:3; }
   .card.sel .sel-box { background:#4aa3ff; border-color:#4aa3ff; }
@@ -324,6 +330,27 @@ function failReason(d){
 const TOP_RANK_N = 10;
 const FRESH_MS = 24 * 3600 * 1000;
 function isFreshIso(iso){ if(!iso) return false; try{ return (Date.now() - new Date(iso).getTime()) < FRESH_MS; }catch{return false;} }
+function pickRoomKey(rm){
+  if (!rm) return null;
+  // Shape varies: array ["living_room"] OR object {"living_room":"/path"}.
+  const keys = Array.isArray(rm) ? rm : (typeof rm === 'object' ? Object.keys(rm) : []);
+  if (!keys.length) return null;
+  return keys.includes('living_room') ? 'living_room'
+       : keys.includes('bedroom')     ? 'bedroom'
+       : keys.includes('office')      ? 'office'
+       : keys[0];
+}
+function roomImgHTML(d){
+  const k = pickRoomKey(d.room_mockups);
+  if (!k) return '';
+  return `<img class="card-room" loading="lazy" src="/designs/room/design_${d.id}_${k}.png" alt="" aria-hidden="true">`;
+}
+function roomBadgeHTML(d){
+  const k = pickRoomKey(d.room_mockups);
+  if (!k) return '';
+  return `<span class="b room" title="hover for ${k.replace(/_/g,' ')} mockup">๐Ÿ›‹</span>`;
+}
+
 function card(d, idx){
   const el = document.createElement('div');
   el.className = 'card';
@@ -360,10 +387,12 @@ function card(d, idx){
         ${d.tif_path
           ? `<span class="b tif" title="TIF archive: ${d.tif_w_px||'?'}ร—${d.tif_h_px||'?'}px ยท ${d.tif_bytes?(d.tif_bytes/1024/1024).toFixed(2)+' MB':'?'} ยท ${(d.tif_path||'').replace(/^.*\//,'')}">๐Ÿ“ฆ TIF ${d.tif_w_px||''}</span>`
           : `<span class="b notif" title="no TIF archive โ€” generate via /admin/etsy-bucket or scripts/archive-svg-5400.js">๐Ÿ“ฆ NO TIF</span>`}
+        ${roomBadgeHTML(d)}
         <span class="b ${verdictClass(d.seam_verdict)}">${d.seam_verdict||'no scan'}</span>
         ${failReason(d).map(r=>`<span class="b reason" title="failed: ${r}">${r}</span>`).join('')}
       </div>
       <img loading="lazy" src="/designs/img/by-id/${d.id}" alt="cactus ${d.id}">
+      ${roomImgHTML(d)}
       ${seamCallout}
       <button class="auto-id-btn" title="auto-detect bad areas (you confirm/delete)">๐Ÿ” Auto-ID</button>
       <a class="dbg-btn" href="${q('/admin/seam-debug/'+d.id)}" target="_blank" title="open seam-defect debugger โ€” see where the mid-line breaks">๐Ÿ”ฌ Debug seams</a>
@@ -887,6 +916,7 @@ load();
         </div>
         <a class="bb-link" href="/admin/seam-debug/${fam.id}" target="_blank" style="display:block;width:100%;height:100%">
           <img loading="lazy" src="/designs/img/by-id/${fam.id}" alt="base ${fam.id}">
+          ${roomImgHTML(fam)}
         </a>
         <div class="state" style="${stateLabel?'display:block':''}">${stateLabel}</div>
       </div>
diff --git a/server.js b/server.js
index f62eb99..62951d3 100644
--- a/server.js
+++ b/server.js
@@ -1747,6 +1747,10 @@ app.get('/api/admin/cactus/list', (req, res) => {
                // master ready vs none yet (Steve standing rule: every published
                // design must have a 150-DPI TIF archive).
                "d.tif_path, d.tif_w_px, d.tif_h_px, d.tif_bytes, " +
+               // room_mockups โ†’ fuels the hover-swap room preview on each chip
+               // (Steve 2026-05-28). Shape varies โ€” JSONB object {living_room:path}
+               // or JSON array ["living_room"]; the front-end normalizes both.
+               "d.room_mockups, " +
                "CASE " +
                  "WHEN r.vision_score IS NOT NULL AND r.seam_score IS NOT NULL " +
                    "THEN round(0.6*r.vision_score + 0.4*r.seam_score, 1) " +

โ† a2a7d70 /me/saved: add admin view-mode toggle pill (admin/live) โ€” in  ยท  back to Wallco Ai  ยท  scripts: push-shopify-digital-bundle.js โ€” Shopify counterpar f55cad1 โ†’