[object Object]

← back to Wallco Ai

theme-gallery: Set Live promotes a variant to take over the bare /design/:id PDP (global, file-backed, ?classic=1 bypass)

6b5a6a16d844485259d482ee8a90865b99d7b6f4 · 2026-05-31 17:29:10 -0700 · Steve Abrams

Files touched

Diff

commit 6b5a6a16d844485259d482ee8a90865b99d7b6f4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 17:29:10 2026 -0700

    theme-gallery: Set Live promotes a variant to take over the bare /design/:id PDP (global, file-backed, ?classic=1 bypass)
---
 .deploy.conf                    |  2 +-
 public/admin/theme-gallery.html | 76 +++++++++++++++++++++++++++++++++++++++--
 2 files changed, 74 insertions(+), 4 deletions(-)

diff --git a/.deploy.conf b/.deploy.conf
index a18dbb4..1caf4db 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -13,4 +13,4 @@ HEALTH_URL="https://wallco.ai/health"
 # all WRITE to these files. Pre-2026-05-24 the deploy was rsync'ing local stale
 # copies over them and wiping fresh prod flags between scan + cleanup (lost the
 # 4 cactus-pine-scenic flags from the 2026-05-25 prod scan that way).
-RSYNC_EXTRA_EXCLUDES="data/generated data/generated_pre_seamless_backup data/images data/rooms data/spoonflower-pulled data/fliepaper-bugs public/uploads public/marketplace/uploads data/designs.json data/marketplace/events.jsonl data/ghost-scan-flagged.jsonl data/ghost-scan-results.jsonl data/ghost-scan-triage.jsonl data/ghost-scan-flagged.pre-*.jsonl data/ghost-scan-flagged.post-*.jsonl data/ghost-scan-results.pre-*.jsonl data/bad-aesthetic-patterns.jsonl data/bad-aesthetic-patterns.*.jsonl data/ghost-purge.jsonl data/ghost-labels.jsonl data/fixes-feed.jsonl data/.rescan-current.txt data/.rescan-ready.txt"
+RSYNC_EXTRA_EXCLUDES="data/generated data/generated_pre_seamless_backup data/images data/rooms data/spoonflower-pulled data/fliepaper-bugs public/uploads public/marketplace/uploads data/designs.json data/marketplace/events.jsonl data/ghost-scan-flagged.jsonl data/ghost-scan-results.jsonl data/ghost-scan-triage.jsonl data/ghost-scan-flagged.pre-*.jsonl data/ghost-scan-flagged.post-*.jsonl data/ghost-scan-results.pre-*.jsonl data/bad-aesthetic-patterns.jsonl data/bad-aesthetic-patterns.*.jsonl data/ghost-purge.jsonl data/ghost-labels.jsonl data/fixes-feed.jsonl data/.rescan-current.txt data/.rescan-ready.txt data/pdp-theme.json"
diff --git a/public/admin/theme-gallery.html b/public/admin/theme-gallery.html
index 87155cf..eddfe69 100644
--- a/public/admin/theme-gallery.html
+++ b/public/admin/theme-gallery.html
@@ -69,6 +69,13 @@
       <button data-w="390"  data-h="760">Mobile</button>
     </span>
   </div>
+  <div class="tg-row" style="margin-top:10px">
+    <span class="chip" id="liveChip" title="The variant currently serving every customer /design/:id page">
+      🟢 Live PDP theme: <strong id="liveTxt" style="margin-left:4px">— monolith (default) —</strong>
+    </span>
+    <button id="clearLiveBtn" title="Revert all PDPs to the built-in monolith template">Revert to monolith</button>
+    <span class="sub" id="setMsg" style="color:var(--accent)"></span>
+  </div>
 </header>
 <main>
   <div class="grid" id="grid"></div>
@@ -99,7 +106,56 @@ const VARIANTS = [
   ['roomshow',   'V17 · Room-Left Showcase'],
 ];
 const $ = s => document.querySelector(s);
+const LABELS = Object.fromEntries(VARIANTS);
 let curId = null;
+let liveTheme = null;   // the variant slug currently serving all customer PDPs (null = monolith)
+
+async function loadLiveTheme(){
+  try {
+    const r = await fetch('/api/pdp-theme');
+    const j = await r.json();
+    liveTheme = j && j.theme || null;
+  } catch(e){ liveTheme = null; }
+  renderLiveChip();
+  markLive();
+}
+function renderLiveChip(){
+  $('#liveTxt').textContent = liveTheme
+    ? (LABELS[liveTheme] || liveTheme)
+    : '— monolith (default) —';
+  $('#clearLiveBtn').style.display = liveTheme ? '' : 'none';
+}
+function markLive(){
+  document.querySelectorAll('.tcard').forEach(card => {
+    const isLive = card.dataset.slug === liveTheme;
+    const badge = card.querySelector('.livebadge');
+    if (badge) badge.hidden = !isLive;
+    card.style.outline = isLive ? '2px solid #0a7d3a' : '';
+    const btn = card.querySelector('.setlive');
+    if (btn){ btn.textContent = isLive ? '✓ Live' : 'Set Live'; btn.disabled = isLive;
+              btn.classList.toggle('primary', false); }
+  });
+}
+async function setLive(slug){
+  const msg = $('#setMsg');
+  msg.textContent = 'Setting…';
+  try {
+    const r = await fetch('/api/admin/pdp-theme', {
+      method:'POST', headers:{'Content-Type':'application/json'},
+      credentials:'same-origin', body: JSON.stringify({ theme: slug })
+    });
+    const j = await r.json();
+    if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP '+r.status));
+    liveTheme = j.theme || null;
+    renderLiveChip(); markLive();
+    msg.textContent = slug ? ('✓ Live → '+(LABELS[slug]||slug)+'. Every /design/:id now serves it.')
+                           : '✓ Reverted to monolith.';
+    setTimeout(()=>{ msg.textContent=''; }, 4000);
+  } catch(e){
+    msg.textContent = '✗ '+e.message; msg.style.color = '#b00';
+    setTimeout(()=>{ msg.textContent=''; msg.style.color='var(--accent)'; }, 5000);
+  }
+}
 let logicalW = +(localStorage.getItem('tg-vw') || 1280);
 let logicalH = +(localStorage.getItem('tg-vh') || 1700);
 
@@ -114,14 +170,18 @@ function buildGrid(id){
   curId = id;
   const grid = $('#grid'); grid.innerHTML = '';
   VARIANTS.forEach(([slug,label]) => {
-    const card = document.createElement('div'); card.className = 'tcard';
+    const card = document.createElement('div'); card.className = 'tcard'; card.dataset.slug = slug;
     const url = `/design/${id}/${slug}`;
     card.innerHTML =
-      `<h2><span>${label} <span class="slug">/${slug}</span></span>`+
-      `<a class="btn" href="${url}" target="_blank" rel="noopener" style="padding:3px 9px;font-size:11px">open ↗</a></h2>`+
+      `<h2><span>${label} <span class="slug">/${slug}</span><span class="livebadge" hidden style="margin-left:8px;color:#0a7d3a;font-weight:700;font-size:11px">● LIVE</span></span>`+
+      `<span style="display:flex;gap:6px">`+
+        `<button class="setlive" data-slug="${slug}" style="padding:3px 9px;font-size:11px">Set Live</button>`+
+        `<a class="btn" href="${url}" target="_blank" rel="noopener" style="padding:3px 9px;font-size:11px">open ↗</a>`+
+      `</span></h2>`+
       `<div class="frame" data-url="${url}"></div>`;
     grid.appendChild(card);
   });
+  markLive();
   rescale();
   // lazy-mount iframes as cards scroll into view
   const io = new IntersectionObserver((ents,obs)=>{
@@ -237,10 +297,20 @@ syncSliderUI();
 $('#loadBtn').addEventListener('click', ()=>go($('#idIn').value));
 $('#idIn').addEventListener('keydown', e=>{ if(e.key==='Enter') go($('#idIn').value); });
 $('#newestBtn').addEventListener('click', newest);
+// Delegated Set-Live clicks (cards are rebuilt on every design load)
+$('#grid').addEventListener('click', e=>{
+  const b = e.target.closest('.setlive'); if(!b) return;
+  e.preventDefault();
+  if (confirm('Make "'+(LABELS[b.dataset.slug]||b.dataset.slug)+'" the LIVE theme for EVERY customer /design/:id page?')) setLive(b.dataset.slug);
+});
+$('#clearLiveBtn').addEventListener('click', ()=>{
+  if (confirm('Revert ALL customer PDPs to the built-in monolith template?')) setLive(null);
+});
 window.addEventListener('resize', ()=>{ clearTimeout(window._rt); window._rt=setTimeout(rescale,150); });
 
 // init: ?id= > last-used > newest
 (function init(){
+  loadLiveTheme();
   const q = new URLSearchParams(location.search).get('id');
   const last = localStorage.getItem('tg-id');
   if(q) go(q);

← 606f45c designer-studio compose: async background generation — retur  ·  back to Wallco Ai  ·  Fix two more PDP runtime JS errors (hint TDZ + mailto newlin 47df49c →