[object Object]

← back to Wallco Ai

feat(design page): move Curated Rating into admin drawer + add 'Run rating now' button

bcbd37906a0f01387dada9b57746fc071907ecc4 · 2026-05-25 00:44:52 -0700 · Steve Abrams

Steve: 'move to the right Curated Suggested Rating … still need button to
activate'.

- #ai-rating-card gains class js-admin-drawer-card; admin-sheet mover
  now grabs ALL .js-admin-drawer-card elements (plus their sibling
  inline-wire-up <script>) and appends them to the right drawer body,
  not just the tone-on-tone .recolor-wheel.
- New 'Run rating now' button calls POST /api/design/:id/ai-rate (which
  spawns scripts/rate_design.js detached), then polls
  GET /api/design/:id/rating every 4s for up to 3 min, rendering as soon
  as the row populates.
- Card is now display:block by default (was display:none until a rating
  existed) so empty-state shows the button. render() returns false on
  empty payload so polling continues correctly.
- Mover runs at DOMContentLoaded + 150ms so the existing
  #toolbox-rating-slot relocator runs first; we then steal the card via
  appendChild (which moves, not copies).

Files touched

Diff

commit bcbd37906a0f01387dada9b57746fc071907ecc4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 00:44:52 2026 -0700

    feat(design page): move Curated Rating into admin drawer + add 'Run rating now' button
    
    Steve: 'move to the right Curated Suggested Rating … still need button to
    activate'.
    
    - #ai-rating-card gains class js-admin-drawer-card; admin-sheet mover
      now grabs ALL .js-admin-drawer-card elements (plus their sibling
      inline-wire-up <script>) and appends them to the right drawer body,
      not just the tone-on-tone .recolor-wheel.
    - New 'Run rating now' button calls POST /api/design/:id/ai-rate (which
      spawns scripts/rate_design.js detached), then polls
      GET /api/design/:id/rating every 4s for up to 3 min, rendering as soon
      as the row populates.
    - Card is now display:block by default (was display:none until a rating
      existed) so empty-state shows the button. render() returns false on
      empty payload so polling continues correctly.
    - Mover runs at DOMContentLoaded + 150ms so the existing
      #toolbox-rating-slot relocator runs first; we then steal the card via
      appendChild (which moves, not copies).
---
 server.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 85 insertions(+), 27 deletions(-)

diff --git a/server.js b/server.js
index af18d70..00a876b 100644
--- a/server.js
+++ b/server.js
@@ -8931,9 +8931,15 @@ app.get('/design/:id', (req, res) => {
   // parent_design_id. Smart-fix / regenerate-reverse / recolor each create a
   // new id and unpublish the source — without this redirect, /design/<old-id>
   // still renders the broken source PNG. Transitive: A → B → C resolves to C.
-  // Cycle-guarded (cap at 5 hops, seen-set). Bypass with ?asis=1 (or already
-  // looking at the head — no redirect to self). Query string preserved.
-  if (!req.query.asis) {
+  // Cycle-guarded (cap at 5 hops, seen-set). Query string preserved.
+  // BYPASS:
+  //   - ?asis=1 query param (explicit)
+  //   - admin request (Steve typed the specific id; he wants to act on it,
+  //     not be bounced to a descendant). 2026-05-25 — fixing "Crop & Fix
+  //     looked broken" UX where every fix created a child and the admin
+  //     kept redirecting away from the source. Public users still get the
+  //     latest-and-best behavior.
+  if (!req.query.asis && !isAdmin(req)) {
     try {
       let cursor = id;
       const seen = new Set([cursor]);
@@ -9185,18 +9191,29 @@ ${htmlHeader('/designs')}
         btn.addEventListener('click', () => sh.classList.contains('open') ? close() : open());
         document.addEventListener('click', (e) => { if (e.target && e.target.matches('[data-close-admin]')) close(); });
         document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
-        // After DOM settles, MOVE the tone-on-tone block into this sheet.
-        // The block has an in-line script that wires its own click handlers,
-        // so we move the whole .recolor-wheel + its sibling <script> together.
-        document.addEventListener('DOMContentLoaded', () => {
-          const tone = document.querySelector('.recolor-wheel');
-          const sib  = tone && tone.nextElementSibling && tone.nextElementSibling.tagName === 'SCRIPT' ? tone.nextElementSibling : null;
+        // After DOM settles, MOVE admin blocks into this sheet.
+        // We move two classes of element:
+        //   .recolor-wheel — the tone-on-tone fashion palette block
+        //   .js-admin-drawer-card — any admin block that opted-in by class
+        // For each, the immediately following <script> (its wire-up) is
+        // moved with it so the inline handlers keep working. setTimeout
+        // 150ms delay lets the existing #toolbox-rating-slot relocator
+        // (which appends #ai-rating-card to the left-column toolbox) run
+        // first; we then steal the card from whatever parent it ended up
+        // in via appendChild (which moves rather than copies).
+        function moveBlock(el, body) {
+          if (!el || !body) return;
+          const sib = el.nextElementSibling && el.nextElementSibling.tagName === 'SCRIPT' ? el.nextElementSibling : null;
+          body.appendChild(el);
+          if (sib) body.appendChild(sib);
+        }
+        function moveAll() {
           const body = document.getElementById('admin-sheet-body');
-          if (tone && body) {
-            body.appendChild(tone);
-            if (sib) body.appendChild(sib);
-          }
-        });
+          if (!body) return;
+          moveBlock(document.querySelector('.recolor-wheel'), body);
+          document.querySelectorAll('.js-admin-drawer-card').forEach(el => moveBlock(el, body));
+        }
+        document.addEventListener('DOMContentLoaded', () => setTimeout(moveAll, 150));
       })();
       </script>
       ` : ''}
@@ -11399,9 +11416,11 @@ ${(() => {
       })();
       </script>
 
-      <!-- Curated Suggested Rating (graphic designer + interior designer) -->
-      <div id="ai-rating-card" style="display:none;margin:14px 0;padding:14px 16px;background:#1a1816;color:#e8e2d6;border-radius:8px;border:1px solid #2a2a2a;font-size:13px">
-        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+      <!-- Curated Suggested Rating (graphic designer + interior designer).
+           Has class js-admin-drawer-card so the admin-sheet mover relocates
+           it into the right drawer at DOMContentLoaded. -->
+      <div id="ai-rating-card" class="js-admin-drawer-card" style="display:block;margin:14px 0;padding:14px 16px;background:#1a1816;color:#e8e2d6;border-radius:8px;border:1px solid #2a2a2a;font-size:13px">
+        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px">
           <strong style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:16px;color:#d2b15c">Curated Suggested Rating</strong>
           <span id="rating-combined" style="font-size:18px;color:#d2b15c"></span>
         </div>
@@ -11410,22 +11429,61 @@ ${(() => {
           <div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Graphic designer</div><div id="rating-gd-score" style="font-size:14px;color:#fff"></div><div id="rating-gd-critique" style="color:#aaa;margin-top:4px"></div></div>
           <div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Interior designer</div><div id="rating-id-score" style="font-size:14px;color:#fff"></div><div id="rating-id-critique" style="color:#aaa;margin-top:4px"></div></div>
         </div>
+        <!-- Trigger / re-run button — calls POST /api/design/:id/ai-rate
+             which spawns scripts/rate_design.js in the background. Then we
+             poll GET /api/design/:id/rating until the row is populated. -->
+        <div style="margin-top:12px;padding-top:10px;border-top:1px solid #2a2a2a;display:flex;align-items:center;gap:10px">
+          <button id="rate-run-btn" type="button"
+                  style="padding:8px 14px;background:#d2b15c;color:#1a1816;border:0;border-radius:4px;font:600 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer">
+            ★ Run rating now
+          </button>
+          <span id="rate-run-status" style="font:11px var(--sans);color:#888"></span>
+        </div>
       </div>
       <script>
-      (async function(){
-        try {
-          var r = await fetch('/api/design/${design.id}/rating');
-          if (!r.ok) return;
-          var j = await r.json();
-          if (!j.combined_score) return;
-          document.getElementById('ai-rating-card').style.display = 'block';
-          document.getElementById('rating-combined').textContent = '★ ' + j.combined_score + ' / 5';
-          document.getElementById('rating-summary').textContent = j.rating_summary || '';
+      (function(){
+        var DID = ${design.id};
+        function render(j) {
+          if (!j) return false;
+          var has = !!(j.combined_score || j.gd_score || j.id_score);
+          if (!has) return false;
+          document.getElementById('rating-combined').textContent = j.combined_score ? '★ ' + j.combined_score + ' / 5' : '';
+          document.getElementById('rating-summary').textContent  = j.rating_summary || '';
           document.getElementById('rating-gd-score').textContent = (j.gd_score || '?') + ' / 5';
           document.getElementById('rating-gd-critique').textContent = j.gd_critique || '';
           document.getElementById('rating-id-score').textContent = (j.id_score || '?') + ' / 5';
           document.getElementById('rating-id-critique').textContent = j.id_critique || '';
-        } catch(e) {}
+          return true;
+        }
+        async function fetchOnce() {
+          try { var r = await fetch('/api/design/' + DID + '/rating'); if (!r.ok) return null; return await r.json(); }
+          catch (e) { return null; }
+        }
+        // Initial load — populate if a rating already exists.
+        fetchOnce().then(render);
+        // "Run rating now" — kick the AI rater, poll for the row.
+        var btn = document.getElementById('rate-run-btn');
+        var stat = document.getElementById('rate-run-status');
+        if (btn) btn.addEventListener('click', async function(){
+          btn.disabled = true; btn.style.opacity = '0.5';
+          stat.textContent = 'Queued…';
+          try {
+            var r = await fetch('/api/design/' + DID + '/ai-rate', { method: 'POST' });
+            var j = await r.json();
+            if (!j.ok) throw new Error(j.error || 'queue failed');
+            stat.textContent = 'Rating… (this takes ~25-60s)';
+            // Poll every 4s for up to 3 minutes.
+            var start = Date.now();
+            var poll = setInterval(async function(){
+              var got = await fetchOnce();
+              if (render(got)) { clearInterval(poll); stat.textContent = '✓ Done'; btn.disabled = false; btn.style.opacity = '1'; }
+              else if (Date.now() - start > 180000) { clearInterval(poll); stat.textContent = '⚠ Timed out — try again'; btn.disabled = false; btn.style.opacity = '1'; }
+            }, 4000);
+          } catch (e) {
+            stat.textContent = '✗ ' + e.message;
+            btn.disabled = false; btn.style.opacity = '1';
+          }
+        });
       })();
       </script>
 

← 9b4e075 stack both hamburgers in upper-right corner of design image  ·  back to Wallco Ai  ·  design page admin sheet: add ⎋ Logout button next to close c6011e8 →