← back to Wallco Ai
add /admin/defect-registry web viewer over wallco_defect_registry — browse every kept-but-rejected design + defect type + generator + prompt; sort + density slider + per-card recorded/created timestamps
a1cbb55522df4b5cfd1db8982819e14fe3d165d6 · 2026-05-27 09:26:17 -0700 · Steve Abrams
Files touched
A public/admin/defect-registry.htmlM server.js
Diff
commit a1cbb55522df4b5cfd1db8982819e14fe3d165d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 09:26:17 2026 -0700
add /admin/defect-registry web viewer over wallco_defect_registry — browse every kept-but-rejected design + defect type + generator + prompt; sort + density slider + per-card recorded/created timestamps
---
public/admin/defect-registry.html | 117 ++++++++++++++++++++++++++++++++++++++
server.js | 45 ++++++++++++++-
2 files changed, 161 insertions(+), 1 deletion(-)
diff --git a/public/admin/defect-registry.html b/public/admin/defect-registry.html
new file mode 100644
index 0000000..752dac3
--- /dev/null
+++ b/public/admin/defect-registry.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Defect Registry — wallco.ai</title>
+<style>
+ :root { --cols:5; --bg:#0e0f10; --panel:#17191b; --line:#272b2e; --ink:#e9ecef; --mut:#9aa3ab; --accent:#e0556b; }
+ * { box-sizing:border-box; }
+ body { margin:0; background:var(--bg); color:var(--ink); font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
+ header { position:sticky; top:0; z-index:5; background:rgba(14,15,16,.96); border-bottom:1px solid var(--line); padding:12px 16px; backdrop-filter:blur(6px); }
+ h1 { font-size:16px; margin:0 0 8px; font-weight:650; }
+ h1 .why { font-weight:400; color:var(--mut); font-size:12px; }
+ .controls { display:flex; flex-wrap:wrap; gap:14px; align-items:center; }
+ .controls label { color:var(--mut); font-size:12px; display:flex; gap:6px; align-items:center; }
+ select, input[type=range] { accent-color:var(--accent); background:var(--panel); color:var(--ink); border:1px solid var(--line); border-radius:6px; padding:3px 6px; }
+ input[type=range] { width:130px; padding:0; }
+ .count { color:var(--mut); font-size:12px; margin-left:auto; }
+ .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:12px; padding:14px 16px 120px; }
+ .card { background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
+ .card img { width:100%; aspect-ratio:1/1; object-fit:cover; object-position:center 12%; background:#000; display:block; }
+ .meta { padding:8px 9px 10px; }
+ .badge { display:inline-block; font-size:10px; font-weight:600; padding:2px 6px; border-radius:5px; background:#3a1d22; color:#f4a6b2; border:1px solid #5a2730; margin-bottom:5px; }
+ .gen { color:var(--mut); font-size:11px; font-variant-numeric:tabular-nums; }
+ .cat { font-size:11.5px; color:var(--ink); margin:2px 0; }
+ .prompt { color:var(--mut); font-size:10.5px; margin-top:5px; max-height:42px; overflow:hidden; cursor:pointer; }
+ .prompt.open { max-height:none; }
+ .when { margin-top:5px; color:var(--mut); font-size:10.5px; font-variant-numeric:tabular-nums; }
+ .id { color:#6b747c; font-size:10px; }
+ .empty { padding:60px; text-align:center; color:var(--mut); }
+</style>
+</head>
+<body>
+<header>
+ <h1>🗂️ Defect Registry <span class="why">— every kept-but-rejected design + the prompt/engine that produced it, so we never recreate the same error</span></h1>
+ <div class="controls">
+ <label>Defect <select id="ftype"><option value="">all types</option></select></label>
+ <label>Engine <select id="fgen"><option value="">all engines</option></select></label>
+ <label>Sort <select id="sort">
+ <option value="recorded">Recorded (newest)</option>
+ <option value="created">Created (newest)</option>
+ <option value="type">Defect type</option>
+ <option value="gen">Engine</option>
+ </select></label>
+ <label>Density <input type="range" id="density" min="2" max="9" step="1" value="5"></label>
+ <span class="count" id="count">loading…</span>
+ </div>
+</header>
+<div class="grid" id="grid"></div>
+
+<script>
+const $ = s => document.querySelector(s);
+let ALL = [], FACETS = {};
+
+function fmtDate(iso){
+ if(!iso) return '—';
+ try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
+ catch { return iso; }
+}
+function applyDensity(v){ document.documentElement.style.setProperty('--cols', v); localStorage.setItem('dr_cols', v); }
+function sorter(mode){
+ switch(mode){
+ case 'created': return (a,b)=> new Date(b.created_at||0)-new Date(a.created_at||0);
+ case 'type': return (a,b)=> (a.defect_type||'').localeCompare(b.defect_type||'');
+ case 'gen': return (a,b)=> (a.generator||'').localeCompare(b.generator||'');
+ default: return (a,b)=> new Date(b.recorded_at||0)-new Date(a.recorded_at||0);
+ }
+}
+function render(){
+ const items = [...ALL].sort(sorter($('#sort').value));
+ $('#count').textContent = `${items.length} shown · ${FACETS.total||0} total in registry`;
+ if(!items.length){ $('#grid').innerHTML = '<div class="empty">No defect records match.</div>'; return; }
+ $('#grid').innerHTML = items.map(d=>`
+ <div class="card">
+ <img loading="lazy" src="/designs/img/by-id/${d.design_id}" alt="defect ${d.design_id}" onerror="this.style.opacity=.25">
+ <div class="meta">
+ <span class="badge">${d.defect_type||'?'}</span>
+ <div class="gen">${d.generator||'?'} <span class="id">#${d.design_id}</span></div>
+ <div class="cat">${d.category||''}</div>
+ <div class="prompt" title="click to expand" onclick="this.classList.toggle('open')">${(d.prompt||'').replace(/</g,'<')||'<em>no prompt</em>'}</div>
+ <div class="when" title="recorded ${d.recorded_at||''} · created ${d.created_at||''}">🕓 rec ${fmtDate(d.recorded_at)} · made ${fmtDate(d.created_at)}</div>
+ </div>
+ </div>`).join('');
+}
+function fillFacets(){
+ const t=$('#ftype'), g=$('#fgen');
+ Object.entries(FACETS.by_type||{}).sort((a,b)=>b[1]-a[1]).forEach(([k,c])=>{
+ const o=document.createElement('option'); o.value=k; o.textContent=`${k} (${c})`; t.appendChild(o);
+ });
+ Object.entries(FACETS.by_gen||{}).sort((a,b)=>b[1]-a[1]).forEach(([k,c])=>{
+ const o=document.createElement('option'); o.value=k; o.textContent=`${k} (${c})`; g.appendChild(o);
+ });
+}
+async function load(){
+ const t=$('#ftype').value, g=$('#fgen').value;
+ const qs = new URLSearchParams({ limit:'1200' });
+ if(t) qs.set('defect_type', t);
+ if(g) qs.set('generator', g);
+ $('#count').textContent='loading…';
+ const r = await fetch('/api/admin/defect-registry/list?'+qs.toString());
+ const j = await r.json();
+ ALL = j.items||[]; FACETS = j.facets||{};
+ if(!$('#ftype').dataset.filled){ fillFacets(); $('#ftype').dataset.filled='1'; }
+ render();
+}
+$('#sort').onchange = render;
+$('#ftype').onchange = load;
+$('#fgen').onchange = load;
+$('#density').oninput = e => applyDensity(e.target.value);
+(function init(){
+ const c = localStorage.getItem('dr_cols'); if(c){ $('#density').value=c; }
+ applyDensity($('#density').value);
+ load();
+})();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 10cb1c9..31b4298 100644
--- a/server.js
+++ b/server.js
@@ -1124,6 +1124,49 @@ app.get('/admin/cactus-curator', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin', 'cactus-curator.html'));
});
+// ── /admin/defect-registry — read-only viewer over wallco_defect_registry ──
+// "Keep all bad items in their own db so we never recreate the same error"
+// (Steve 2026-05-27). Browse every catalogued defect with its image, defect
+// type, generator, and the prompt that produced it. Nothing destructive here.
+app.get('/admin/defect-registry', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ res.sendFile(path.join(__dirname, 'public', 'admin', 'defect-registry.html'));
+});
+
+app.get('/api/admin/defect-registry/list', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ const limit = Math.min(Math.max(parseInt(req.query.limit || '600', 10), 1), 3000);
+ const offset = Math.max(parseInt(req.query.offset || '0', 10), 0);
+ const dtype = (req.query.defect_type || '').replace(/[^a-z_]/g, '');
+ const gen = (req.query.generator || '').replace(/[^a-z0-9.\- ]/gi, '');
+ try {
+ const where = [];
+ if (dtype) where.push(`r.defect_type = '${dtype}'`);
+ if (gen) where.push(`r.generator = '${gen.replace(/'/g, "''")}'`);
+ const whereSql = where.length ? 'WHERE ' + where.join(' AND ') : '';
+ const rawJson = psqlQuery(
+ "SELECT COALESCE(json_agg(t ORDER BY t.recorded_at DESC NULLS LAST, t.design_id DESC), '[]'::json) FROM (" +
+ "SELECT r.design_id, r.category, r.generator, r.defect_type, r.defect_detail, " +
+ "left(r.prompt, 600) AS prompt, r.seed, r.source, r.recorded_at, d.created_at " +
+ "FROM wallco_defect_registry r LEFT JOIN all_designs d ON d.id = r.design_id " +
+ whereSql + " " +
+ `LIMIT ${limit} OFFSET ${offset}` +
+ ") t;"
+ );
+ // facet counts for the filter dropdowns
+ const facetsJson = psqlQuery(
+ "SELECT json_build_object(" +
+ "'by_type', (SELECT COALESCE(json_object_agg(defect_type, c),'{}'::json) FROM (SELECT defect_type, count(*) c FROM wallco_defect_registry GROUP BY 1) a)," +
+ "'by_gen', (SELECT COALESCE(json_object_agg(generator, c),'{}'::json) FROM (SELECT generator, count(*) c FROM wallco_defect_registry GROUP BY 1) b)," +
+ "'total', (SELECT count(*) FROM wallco_defect_registry)" +
+ ");"
+ );
+ res.json({ ok: true, items: JSON.parse(rawJson || '[]'), facets: JSON.parse(facetsJson || '{}') });
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// JSON list of every cactus design joined to its rank sidecar. rank_score is
// computed in SQL so it always reflects the latest scores: 0.6*vision +
// 0.4*seam when both present, else whichever exists. Default hides already-
@@ -1140,7 +1183,7 @@ app.get('/api/admin/cactus/list', (req, res) => {
"to_char(d.digital_file_at,'YYYY-MM-DD') AS digital_file_at, " +
"to_char(d.needs_fixing_at,'YYYY-MM-DD') AS needs_fixing_at, " +
"d.created_at, " +
- "r.seam_score, r.seam_verdict, r.vision_score, " +
+ "r.seam_score, r.seam_verdict, r.seam_hmid_raw, r.seam_vmid_raw, r.vision_score, " +
"CASE " +
"WHEN r.vision_score IS NOT NULL AND r.seam_score IS NOT NULL " +
"THEN round(0.6*r.vision_score + 0.4*r.seam_score, 1) " +
← 017a711 edges-review: sort by status (removed/active first) + dynami
·
back to Wallco Ai
·
defect-registry viewer: accurate filtered_total + Load-more ef42bbe →