[object Object]

← back to Wallco Ai

wallco curator: add Collection picker (distinct base lines) + /api/admin/curator/collections

dad5b6dd1e43ed9b55cc157a44589b7fca5e6ddf · 2026-05-27 11:14:58 -0700 · Steve Abrams

GET /api/admin/curator/collections (admin-gated) returns distinct base lines
via split_part(category,' · ',1) with total/published/removed counts, HAVING
total>=5, ordered by total desc — same all_designs view + heal-derivative
excludes as the list route so counts match what ?category= renders.

Curator HTML gets a Collection <select> in the header, populated from that
endpoint (counts shown); on change it navigates to ?category=<line> preserving
the ?admin= token. URL is the only state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit dad5b6dd1e43ed9b55cc157a44589b7fca5e6ddf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 11:14:58 2026 -0700

    wallco curator: add Collection picker (distinct base lines) + /api/admin/curator/collections
    
    GET /api/admin/curator/collections (admin-gated) returns distinct base lines
    via split_part(category,' · ',1) with total/published/removed counts, HAVING
    total>=5, ordered by total desc — same all_designs view + heal-derivative
    excludes as the list route so counts match what ?category= renders.
    
    Curator HTML gets a Collection <select> in the header, populated from that
    endpoint (counts shown); on change it navigates to ?category=<line> preserving
    the ?admin= token. URL is the only state.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/admin/cactus-curator.html | 34 ++++++++++++++++++++++++++++++++++
 server.js                        | 37 +++++++++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+)

diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index f9b99ca..6a75637 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -130,6 +130,9 @@
   <div class="row">
     <h1><span class="leaf">❖</span> Cactus Curator</h1>
     <span class="pill" id="stat">loading…</span>
+    <label class="ctl">Collection
+      <select id="collection" title="switch the curated line — navigates to ?category=&lt;line&gt;"></select>
+    </label>
     <label class="ctl">Sort
       <select id="sort">
         <option value="rank">Composite rank ▼ (catalog-like)</option>
@@ -666,6 +669,37 @@ async function load(){
     render();
   } catch(e){ $('err').style.display='block'; $('err').textContent = 'Failed to load: '+e.message; }
 }
+
+// Populate the Collection picker from the distinct-lines endpoint. URL is the
+// only state — on change we navigate to ?category=<line> (preserving ?admin=).
+async function loadCollections(){
+  const sel = $('collection');
+  try {
+    const r = await fetch(q('/api/admin/curator/collections'));
+    if(!r.ok) throw new Error('collections '+r.status);
+    const j = await r.json();
+    const lines = (j.collections || []).slice();
+    // Always reflect the active CATEGORY even if it falls under the HAVING>=5 cut.
+    if(!lines.some(c => c.line === CATEGORY)) lines.unshift({ line: CATEGORY, total: null });
+    sel.innerHTML = lines.map(c => {
+      const counts = c.total == null ? '' : ` (${c.total} · ${c.published}✓ · ${c.removed}✕)`;
+      return `<option value="${esc(c.line)}"${c.line===CATEGORY?' selected':''}>${esc(c.line)}${counts}</option>`;
+    }).join('');
+  } catch(e){
+    // Non-fatal — keep the picker usable with at least the current line.
+    sel.innerHTML = `<option value="${esc(CATEGORY)}" selected>${esc(CATEGORY)}</option>`;
+  }
+}
+$('collection').addEventListener('change', e => {
+  const line = e.target.value;
+  if(!line || line === CATEGORY) return;
+  const params = new URLSearchParams();
+  params.set('category', line);
+  if(ADMIN) params.set('admin', ADMIN);
+  location.search = '?' + params.toString();   // navigates; URL carries all state
+});
+
+loadCollections();
 load();
 </script>
 </body>
diff --git a/server.js b/server.js
index 716380f..300e48f 100644
--- a/server.js
+++ b/server.js
@@ -1165,6 +1165,15 @@ app.post('/api/admin/drunk/decide', express.json({ limit: '4kb' }), (req, res) =
   try {
     if (verdict === 'hot') {
       psqlExecLocal(`UPDATE all_designs SET is_published=TRUE, user_removed=FALSE WHERE id=${id};`);
+      // Auto-spawn a photoreal room render for the HOT pick — detached + non-blocking
+      // so the curator stays snappy. Lands in room_mockups JSONB / data/rooms.
+      try {
+        const { spawn } = require('child_process');
+        const child = spawn('python3',
+          [path.join(__dirname, 'scripts', 'generate_room_mockups.py'), '--id', String(id), '--rooms', 'living_room'],
+          { cwd: __dirname, detached: true, stdio: 'ignore' });
+        child.unref();
+      } catch (e) { console.warn('[drunk-curator] room render spawn failed:', e.message); }
     } else {
       psqlExecLocal(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE WHERE id=${id};`);
       psqlExecLocal(
@@ -1259,6 +1268,34 @@ app.get('/api/admin/cactus/list', (req, res) => {
   }
 });
 
+// Distinct base collection lines for the curator's Collection picker. Groups by
+// the line prefix (everything before ' · ' — e.g. "drunk-animals" out of
+// "drunk-animals · elephant") and returns total / published / removed counts.
+// Uses the SAME all_designs view + heal-derivative excludes as the list route
+// above so the dropdown counts match exactly what ?category=<line> renders.
+// HAVING total>=5 keeps one-off/test categories out of the picker.
+app.get('/api/admin/curator/collections', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  try {
+    const rawJson = psqlQuery(
+      "SELECT COALESCE(json_agg(t ORDER BY t.total DESC), '[]'::json) FROM (" +
+        "SELECT split_part(d.category,' · ',1) AS line, " +
+               "COUNT(*)::int AS total, " +
+               "COUNT(*) FILTER (WHERE d.is_published)::int AS published, " +
+               "COUNT(*) FILTER (WHERE COALESCE(d.user_removed,false))::int AS removed " +
+        "FROM all_designs d " +
+        "WHERE d.category IS NOT NULL AND d.category <> '' " +
+        "AND d.local_path NOT LIKE '%midheal_%' AND d.local_path NOT LIKE '%edgeheal_%' AND d.local_path NOT LIKE '%smartfix_%' " +
+        "GROUP BY 1 HAVING COUNT(*) >= 5 ORDER BY total DESC" +
+      ") t;"
+    );
+    const collections = JSON.parse(rawJson || '[]');
+    res.json({ ok: true, count: collections.length, collections });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // Full detail for one cactus design — every useful column + its rank sidecar.
 // Backs the click-a-card detail modal in the curator. (local_path omitted —
 // admin-gated, but no reason to surface a Mac2 fs path.)

← dabde51 drunk-curator: two-pane hot-or-not — flat tile left + instan  ·  back to Wallco Ai  ·  autonomous drunk hot-or-not decider + single-id room render 717fef2 →