← 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 & 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">← ${prev.title}</a>` : '<span></span>'}
${next ? `<a href="/design/${next.id}" class="detail-nav-link next">${next.title} →</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 →