[object Object]

← 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

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,'&lt;')||'<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 →