← back to Wallco Ai
public/admin/defect-registry.html
155 lines
<!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); }
.summary { display:flex; flex-wrap:wrap; gap:10px; margin-top:9px; }
.scard { background:var(--panel); border:1px solid var(--line); border-left:3px solid var(--accent); border-radius:8px; padding:7px 11px; font-size:11.5px; }
.scard b { font-size:14px; color:var(--ink); font-variant-numeric:tabular-nums; }
.scard .lbl { color:var(--mut); }
.scard.good { border-left-color:#7bc96f; }
.loadmore { grid-column:1/-1; display:flex; justify-content:center; padding:8px 0 40px; }
.loadmore button { background:var(--panel); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:9px 22px; cursor:pointer; font-size:13px; }
.loadmore button:hover { border-color:var(--accent); }
</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>
<div class="summary" id="summary"></div>
</header>
<div class="grid" id="grid"></div>
<script>
const $ = s => document.querySelector(s);
let ALL = [], FACETS = {}, FILTERED_TOTAL = 0;
const PAGE = 600;
function renderSummary(){
const g = FACETS.by_gen || {}, total = FACETS.total || 0;
const sum = (...keys) => keys.reduce((n,k)=> n + (g[k]||0), 0);
// bucket generators by root cause
let heal=0, repl=0, comfy=0;
for (const [k,v] of Object.entries(g)){
if (/heal|smart-fix/.test(k)) heal+=v;
else if (/replicate/.test(k)) repl+=v;
else if (k==='comfy') comfy+=v;
}
const pct = n => total? Math.round(100*n/total) : 0;
$('#summary').innerHTML = `
<div class="scard"><b>${total.toLocaleString()}</b> <span class="lbl">total defects kept</span></div>
<div class="scard"><b>${heal.toLocaleString()}</b> <span class="lbl">heal-generator blur (${pct(heal)}%) → never publish heal output</span></div>
<div class="scard"><b>${repl.toLocaleString()}</b> <span class="lbl">raw Replicate SDXL seams (${pct(repl)}%) → use ComfyUI workflow</span></div>
<div class="scard good"><b>${comfy.toLocaleString()}</b> <span class="lbl">ComfyUI (${pct(comfy)}%) → the clean engine ✓</span></div>`;
}
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));
const filterOn = $('#ftype').value || $('#fgen').value;
$('#count').textContent = `${items.length.toLocaleString()} of ${FILTERED_TOTAL.toLocaleString()}${filterOn?' (filtered)':''} · ${(FACETS.total||0).toLocaleString()} total in registry`;
if(!items.length){ $('#grid').innerHTML = '<div class="empty">No defect records match.</div>'; return; }
let html = 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('');
if(ALL.length < FILTERED_TOTAL){
html += `<div class="loadmore"><button id="more">Load more — ${(FILTERED_TOTAL-ALL.length).toLocaleString()} remaining</button></div>`;
}
$('#grid').innerHTML = html;
const mb = document.getElementById('more'); if(mb) mb.onclick = ()=> load(true);
}
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(append=false){
const t=$('#ftype').value, g=$('#fgen').value;
const offset = append ? ALL.length : 0;
const qs = new URLSearchParams({ limit:String(PAGE), offset:String(offset) });
if(t) qs.set('defect_type', t);
if(g) qs.set('generator', g);
if(!append) $('#count').textContent='loading…';
const r = await fetch('/api/admin/defect-registry/list?'+qs.toString());
const j = await r.json();
FACETS = j.facets||{}; FILTERED_TOTAL = j.filtered_total||0;
ALL = append ? ALL.concat(j.items||[]) : (j.items||[]);
if(!$('#ftype').dataset.filled){ fillFacets(); renderSummary(); $('#ftype').dataset.filled='1'; }
render();
}
$('#sort').onchange = render;
$('#ftype').onchange = ()=>load(false);
$('#fgen').onchange = ()=>load(false);
$('#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>
<script src="/admin/admin-modal-kit.js" defer></script>
</body>
</html>