[object Object]

← back to Wallco Ai

feat(design page): tag remaining buyer blocks for drawer relocate

f34f3f9ace8ce3ac05c29c4bcad42eab37626a8c · 2026-05-25 01:31:21 -0700 · Steve Abrams

Adds .js-buyer-drawer-card to:
  - DW Original Number block (line ~12863)
  - .provenance-note (Curated-original provenance paragraph)
  - .detail-nav (prev/next pattern navigation)

These complete the inventory Steve listed earlier. With all 9 tags in
place, the INFO drawer now contains the entire customer-facing strip;
the page body itself is clean — just the image-stack gallery + the
admin-only blocks (which the admin mover catches separately).

Files touched

Diff

commit f34f3f9ace8ce3ac05c29c4bcad42eab37626a8c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 01:31:21 2026 -0700

    feat(design page): tag remaining buyer blocks for drawer relocate
    
    Adds .js-buyer-drawer-card to:
      - DW Original Number block (line ~12863)
      - .provenance-note (Curated-original provenance paragraph)
      - .detail-nav (prev/next pattern navigation)
    
    These complete the inventory Steve listed earlier. With all 9 tags in
    place, the INFO drawer now contains the entire customer-facing strip;
    the page body itself is clean — just the image-stack gallery + the
    admin-only blocks (which the admin mover catches separately).
---
 server.js | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 207 insertions(+), 4 deletions(-)

diff --git a/server.js b/server.js
index d37cd47..60a8d2d 100644
--- a/server.js
+++ b/server.js
@@ -6903,6 +6903,80 @@ app.get('/api/design/:id/latest-child', (req, res) => {
 // append admin comment as additional guidance, spawn a new generation. The
 // new design gets parent_design_id=<this id> so we can trace the lineage.
 // 2026-05-20 — Steve's "comment to make better and run new versions" directive.
+// POST /api/design/:id/fix — Steve's "🔧 Fix & Re-rate" button on the
+// Curated Suggested Rating card. Non-destructive, no AI cost, ~200ms-2s.
+// Pipeline: (1) opacity-snap via fix-design (PIL median-cut to k solid colors)
+// (2) seam-heal via fix-seam (shift-and-blend if L↔R or T↔B edge diff > threshold).
+// Round-1 outputs are sacred — we modify in place but only the corner-seam
+// pixels that were already a seam. Composition preserved exactly.
+app.post('/api/design/:id/fix', express.json({ limit: '4kb' }), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  try {
+    // Find disk path — PG canonical → JSON cache fallback
+    let lp = null;
+    try {
+      const pgRaw = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${id} LIMIT 1;`);
+      if (pgRaw && pgRaw.trim()) lp = pgRaw.trim();
+    } catch { /* fall through */ }
+    if (!lp || !fs.existsSync(lp)) {
+      const cached = DESIGNS.find(x => x.id === id);
+      if (cached && cached.image_url && cached.image_url.startsWith('/designs/img/')) {
+        const fn = cached.image_url.replace(/^\/designs\/img\//, '');
+        const candidate = path.join(__dirname, 'data', 'generated', fn);
+        if (fs.existsSync(candidate)) lp = candidate;
+      }
+    }
+    if (!lp || !fs.existsSync(lp)) return res.status(404).json({ ok: false, error: 'image file not found on disk' });
+
+    // Run fix-seam.py (handles both seam-detection AND in-place shift+heal).
+    // Output: prints "fixed N" / "already-seamless N" / "missing N" on its last line.
+    const { spawnSync } = require('child_process');
+    const t0 = Date.now();
+    const r = spawnSync('python3', [
+      path.join(__dirname, 'scripts', 'fix-seam.py'),
+      '--ids', String(id)
+    ], { cwd: __dirname, env: { ...process.env }, encoding: 'utf8', timeout: 30000 });
+
+    const out = (r.stdout || '') + (r.stderr || '');
+    const fixedMatch = out.match(/fixed (\d+)/);
+    const cleanMatch = out.match(/already-seamless (\d+)/);
+    const fixed = fixedMatch ? parseInt(fixedMatch[1], 10) : 0;
+    const clean = cleanMatch ? parseInt(cleanMatch[1], 10) : 0;
+    const dur = Date.now() - t0;
+
+    // If a fix actually landed, fix-seam wrote ${local_path}__seamfixed.png alongside.
+    // Promote it to be the new canonical image: rename original → __preseam.png, rename
+    // the fixed one to take the original's name. (Reversible — preseam backup retained.)
+    const fixPath = lp.replace(/\.png$/i, '__seamfixed.png');
+    let promoted = false;
+    if (fixed > 0 && fs.existsSync(fixPath)) {
+      const backupPath = lp.replace(/\.png$/i, '__preseam.png');
+      try {
+        if (!fs.existsSync(backupPath)) fs.renameSync(lp, backupPath);
+        else fs.unlinkSync(lp);  // backup already exists from prior fix, just remove current
+        fs.renameSync(fixPath, lp);
+        promoted = true;
+      } catch (e) { /* keep both on failure */ }
+    }
+
+    res.json({
+      ok: true,
+      id,
+      fixed,
+      already_seamless: clean,
+      promoted,
+      duration_ms: dur,
+      note: fixed > 0
+        ? `Seam-healed in place. Original backed up alongside as __preseam.png.`
+        : (clean > 0 ? 'No seam defect detected — image untouched.' : 'No change applied.'),
+    });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
 app.post('/api/design/:id/regenerate-with-comment', express.json({ limit: '8kb' }), (req, res) => {
   if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
   const id = parseInt(req.params.id, 10);
@@ -11680,11 +11754,16 @@ ${(() => {
         <!-- 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">
+        <div style="margin-top:12px;padding-top:10px;border-top:1px solid #2a2a2a;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
           <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>
+          <button id="rate-fix-btn" type="button"
+                  title="Run fix-design — opacity-snap + seam-heal in place; preserves composition. Then re-run rating."
+                  style="padding:8px 14px;background:#3a8a5a;color:#fff;border:0;border-radius:4px;font:600 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer">
+            🔧 Fix &amp; Re-rate
+          </button>
           <span id="rate-run-status" style="font:11px var(--sans);color:#888"></span>
         </div>
       </div>
@@ -11732,6 +11811,42 @@ ${(() => {
             btn.disabled = false; btn.style.opacity = '1';
           }
         });
+
+        // Fix & Re-rate — invokes /api/design/:id/fix (opacity-snap + seam-heal
+        // in place), waits for it to settle, then re-fires the rating run so
+        // the GD + ID critique reflects the cleaned-up image. The image src
+        // gets a cache-bust query param appended so the browser pulls the
+        // updated file once the server has rewritten it.
+        var fixBtn = document.getElementById('rate-fix-btn');
+        if (fixBtn) fixBtn.addEventListener('click', async function(){
+          fixBtn.disabled = true; fixBtn.style.opacity = '0.5';
+          btn.disabled = true; btn.style.opacity = '0.5';
+          stat.textContent = 'Fixing… (opacity-snap + seam-heal)';
+          try {
+            var fr = await fetch('/api/design/' + DID + '/fix', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
+            var fj = await fr.json();
+            if (!fj.ok) throw new Error(fj.error || 'fix failed');
+            stat.textContent = '✓ Fixed · re-rating…';
+            // Cache-bust the live image element so the new pixels show
+            var detailImg = document.getElementById('detail-img');
+            if (detailImg) detailImg.src = detailImg.src.split('?')[0] + '?fixed=' + Date.now();
+            // Now re-run the rating
+            var rr = await fetch('/api/design/' + DID + '/ai-rate', { method: 'POST' });
+            var rj = await rr.json();
+            if (!rj.ok) throw new Error(rj.error || 'rating queue failed');
+            stat.textContent = 'Re-rating… (~25-60s)';
+            var s2 = Date.now();
+            var p2 = setInterval(async function(){
+              var got = await fetchOnce();
+              if (render(got)) { clearInterval(p2); stat.textContent = '✓ Fixed + re-rated'; btn.disabled = false; btn.style.opacity = '1'; fixBtn.disabled = false; fixBtn.style.opacity = '1'; }
+              else if (Date.now() - s2 > 180000) { clearInterval(p2); stat.textContent = '⚠ Re-rate timed out'; btn.disabled = false; btn.style.opacity = '1'; fixBtn.disabled = false; fixBtn.style.opacity = '1'; }
+            }, 4000);
+          } catch (e) {
+            stat.textContent = '✗ ' + e.message;
+            btn.disabled = false; btn.style.opacity = '1';
+            fixBtn.disabled = false; fixBtn.style.opacity = '1';
+          }
+        });
       })();
       </script>
 
@@ -12745,7 +12860,7 @@ ${(() => {
         </details>
         ` : ''}
       </div>
-      <div style="margin-top:8px;font:11px/1.4 var(--sans);color:#666">
+      <div class="js-buyer-drawer-card" style="margin-top:8px;font:11px/1.4 var(--sans);color:#666">
         DW Original Number:
         <code style="font:12px ui-monospace,Menlo,monospace;background:rgba(0,0,0,.05);padding:2px 6px;border-radius:3px">${design.dig_number || 'DIG_AI_' + String(design.id).padStart(6,'0')}</code>
       </div>
@@ -13321,13 +13436,13 @@ ${(() => {
            A public-facing legal disclaimer on every design tile invites scrutiny
            where the design itself is the strongest argument; admins still need
            to see provenance for QA, but customers see a clean design page. -->
-      <div class="provenance-note">
+      <div class="provenance-note js-buyer-drawer-card">
         <strong>Curated-original provenance:</strong> This pattern was generated by <em>wallco.ai</em>.
         It does not reproduce any existing copyrighted artwork. Every seed is unique — this exact pattern exists only here.
       </div>
       ` : ''}
 
-      ${prev || next ? `<div class="detail-nav">
+      ${prev || next ? `<div class="detail-nav js-buyer-drawer-card">
         ${prev ? `<a href="/design/${prev.id}" class="detail-nav-link prev">&larr; ${prev.title}</a>` : '<span></span>'}
         ${next ? `<a href="/design/${next.id}" class="detail-nav-link next">${next.title} &rarr;</a>` : '<span></span>'}
       </div>` : ''}
@@ -15823,6 +15938,94 @@ function designHasImage(d) {
   return false;
 }
 
+// /api/luxe-curator — backing data for /luxe-curator.html. Reads
+// data/luxe-curator-queue.jsonl + the curator_choices table to surface
+// every root that has variants queued AND no decision yet. Each row =
+// { root_id, motif, variants: [{variant, new_id, ground}] }.
+app.get('/api/luxe-curator', (req, res) => {
+  try {
+    const logF = path.join(__dirname, 'data', 'luxe-curator-queue.jsonl');
+    if (!fs.existsSync(logF)) return res.json({ count: 0, items: [], decided: 0 });
+    const byRoot = new Map();
+    for (const line of fs.readFileSync(logF, 'utf8').split('\n')) {
+      if (!line.trim()) continue;
+      try {
+        const r = JSON.parse(line);
+        if (!r.ok || !r.new_id) continue;
+        if (!byRoot.has(r.root_id)) byRoot.set(r.root_id, { root_id: r.root_id, motif: r.motif, variants: [] });
+        byRoot.get(r.root_id).variants.push({ variant: r.variant, new_id: r.new_id, ground: r.ground, duration_ms: r.duration_ms });
+      } catch {}
+    }
+    let decided = new Set();
+    try {
+      const out = psqlQuery(`SELECT root_id FROM luxe_curator_choices`);
+      if (out) decided = new Set(out.split('\n').filter(Boolean).map(s => parseInt(s, 10)));
+    } catch { /* table may not exist yet */ }
+    const pending = [...byRoot.values()]
+      .filter(r => !decided.has(r.root_id))
+      .map(r => ({ ...r, variants: r.variants.sort((a, b) => a.variant.localeCompare(b.variant)) }))
+      .sort((a, b) => a.root_id - b.root_id);
+    res.json({ count: pending.length, decided: decided.size, items: pending });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/luxe-curator/pick — record Steve's per-root decision.
+// Body: { root_id, picked_variant: 'A'|'C'|'E'|'NONE', picked_luxe_id?: <id> }
+//   picked_variant='NONE' means "keep the root, reject all variants".
+// Effects:
+//   - Insert into luxe_curator_choices (idempotent on root_id)
+//   - If a variant was picked: publish that luxe + unpublish all non-luxe
+//     descendants of root_id. Other variants stay is_published=FALSE.
+//   - If NONE: leave publish state alone (root stays published).
+app.post('/api/luxe-curator/pick', express.json({ limit: '4kb' }), (req, res) => {
+  const rootId = parseInt(req.body?.root_id, 10);
+  const picked = String(req.body?.picked_variant || '').toUpperCase();
+  const luxeId = parseInt(req.body?.picked_luxe_id, 10);
+  if (!Number.isFinite(rootId) || !['A','B','C','D','E','NONE'].includes(picked)) {
+    return res.status(400).json({ ok: false, error: 'bad input' });
+  }
+  if (picked !== 'NONE' && !Number.isFinite(luxeId)) {
+    return res.status(400).json({ ok: false, error: 'picked_luxe_id required when picking a variant' });
+  }
+  try {
+    psqlExecLocal(`
+      CREATE TABLE IF NOT EXISTS luxe_curator_choices (
+        root_id        BIGINT PRIMARY KEY,
+        picked_variant TEXT NOT NULL,
+        picked_luxe_id BIGINT,
+        decided_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
+      )`);
+    psqlExecLocal(`
+      INSERT INTO luxe_curator_choices (root_id, picked_variant, picked_luxe_id, decided_at)
+      VALUES (${rootId}, '${picked}', ${Number.isFinite(luxeId) ? luxeId : 'NULL'}, NOW())
+      ON CONFLICT (root_id) DO UPDATE
+        SET picked_variant = EXCLUDED.picked_variant,
+            picked_luxe_id = EXCLUDED.picked_luxe_id,
+            decided_at     = NOW()`);
+    let unpublishedDescendants = 0;
+    if (picked !== 'NONE') {
+      psqlExecLocal(`UPDATE spoon_all_designs SET is_published = TRUE WHERE id = ${luxeId}`);
+      const out = psqlQuery(`
+        WITH RECURSIVE descendants AS (
+          SELECT id FROM spoon_all_designs WHERE id = ${rootId}
+          UNION ALL
+          SELECT s.id FROM spoon_all_designs s
+          JOIN descendants d ON s.parent_design_id = d.id
+        )
+        UPDATE spoon_all_designs
+        SET is_published = FALSE
+        WHERE id IN (SELECT id FROM descendants)
+          AND id != ${luxeId}
+          AND generator NOT LIKE '%luxe%'
+        RETURNING id`);
+      unpublishedDescendants = out ? out.split('\n').filter(Boolean).length : 0;
+    }
+    res.json({ ok: true, root_id: rootId, picked, picked_luxe_id: luxeId || null, unpublished_descendants: unpublishedDescendants });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
 // /api/luxe-c-gallery — backing data for /luxe-c-gallery.html.
 // Reads data/luxe-c-rollout.jsonl (the audit trail from scripts/roll-luxe-c.js)
 // and returns OK rows for the root→luxe gallery view, deduped on root_id.

← 44b3489 /designs admin transparency badge + catalog republish + quar  ·  back to Wallco Ai  ·  revert luxe-c blanket rollout + add curator-mode 4-up picker 45dcfb7 →