← 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
M public/admin/cactus-curator.htmlM server.js
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=<line>"></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 →