← 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,'&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('');
  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>