<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>chip-fetcher-lite</title>
<style>
  /* Adom brand fonts — Familjen Grotesk (headlines), Satoshi (body), JetBrains Mono (code).
     Per the Adom brand style guide (wiki: brand). Never fall back to system fonts as the primary face. */
  @font-face{font-family:'Familjen Grotesk';src:url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-700-normal.woff2') format('woff2');font-weight:700;font-display:swap}
  @font-face{font-family:'Familjen Grotesk';src:url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-500-normal.woff2') format('woff2');font-weight:500;font-display:swap}
  @font-face{font-family:'Satoshi';src:url('https://adom.inc/fonts/satoshi/satoshi-400-normal.woff2') format('woff2');font-weight:400;font-display:swap}
  @font-face{font-family:'Satoshi';src:url('https://adom.inc/fonts/satoshi/satoshi-500-normal.woff2') format('woff2');font-weight:500;font-display:swap}
  @font-face{font-family:'JetBrains Mono';src:url('https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwhsk.woff2') format('woff2');font-weight:400;font-display:swap}
  @font-face{font-family:'JetBrains Mono';src:url('https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwhsk.woff2') format('woff2');font-weight:500;font-display:swap}
  :root{
    --bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d;
    --border:#30363d; --border-muted:#21262d;
    --text:#e6edf3; --text-muted:#8b949e; --text-dim:#484f58;
    --accent:#00b8b0; --accent-bright:#00e6dc; --accent-dim:rgba(0,184,176,.12); --accent-border:rgba(0,184,176,.25);
    --green:#3fb950; --green-dim:rgba(63,185,80,.14);
    --yellow:#d29922; --yellow-dim:rgba(210,153,34,.14);
    --red:#f85149; --red-dim:rgba(248,81,73,.14);
    --purple:#8C6BF7; --row-hover:#1c2128;
  }
  *{box-sizing:border-box}
  html{height:100%}
  body{margin:0;min-height:100%}
  body{background:var(--bg);color:var(--text);font-family:'Satoshi',-apple-system,Segoe UI,Roboto,sans-serif;font-size:13px}
  .mono{font-family:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace}
  h1,h2,h3{font-family:'Familjen Grotesk',sans-serif;margin:0}
  button{font-family:inherit;cursor:pointer}

  /* header */
  .app-header{display:flex;align-items:center;gap:12px;height:44px;padding:0 14px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:30}
  .brand{display:flex;align-items:center;gap:9px}
  .logo-chip{display:block;color:var(--accent-bright)}
  .app-name{font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:15px;letter-spacing:.2px;color:var(--accent-bright)}
  .sep{color:var(--text-dim)}
  .app-subtitle{color:var(--text-muted);font-size:12px}
  .subject{margin-left:6px;color:var(--text-muted);font-size:12px}
  .subject b{color:var(--text);font-weight:600}
  .spacer{flex:1}
  .lib-status{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-muted);margin-right:6px}
  .lib-status .dot{width:8px;height:8px;border-radius:50%;background:var(--text-dim);flex:none}
  .lib-status.ok .dot{background:var(--green);box-shadow:0 0 6px var(--green)}
  .lib-status.warn .dot{background:var(--yellow)}
  .lib-status.none .dot{background:var(--red)}
  .send-status{display:inline-flex;align-items:center;gap:7px;font-size:12px;padding:4px 11px;border-radius:7px;margin-left:12px;font-weight:600;max-width:520px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .send-status.running{background:var(--accent-dim);border:1px solid var(--accent-border);color:var(--text)}
  .send-status.done{background:var(--green-dim);border:1px solid var(--green);color:var(--text)}
  .send-status.err{background:var(--red-dim);border:1px solid var(--red);color:var(--text)}

  /* controls bar */
  .controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;background:var(--bg);border-bottom:1px solid var(--border-muted);position:sticky;top:var(--header-h,44px);z-index:20}
  .add-box{display:flex;align-items:center;gap:6px;flex:1;min-width:260px;max-width:520px}
  input[type=text]{background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:7px;padding:7px 10px;font-size:13px;width:100%}
  input[type=text]:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}
  .btn{background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:7px;padding:7px 12px;font-size:12.5px;font-weight:600;transition:.15s;white-space:nowrap}
  .btn:hover{border-color:var(--accent-border);background:var(--elevated)}
  .btn.primary{background:var(--accent);border-color:var(--accent);color:#04231f}
  .btn.primary:hover{background:var(--accent-bright)}
  .btn:disabled{opacity:.45;cursor:not-allowed}
  .pills{display:flex;gap:6px}
  .pill{background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:999px;padding:5px 11px;font-size:12px}
  .pill.active{color:var(--text);border-color:var(--accent-border);background:var(--accent-dim)}
  .send-group{display:flex;align-items:center;gap:8px;padding:5px 9px;border:1px solid var(--border);border-radius:8px;background:var(--surface)}
  .send-group .lbl{color:var(--text-muted);font-size:11px;text-transform:uppercase;letter-spacing:.4px}
  .eda-checks{display:flex;gap:8px}
  .eda-checks label{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-muted);cursor:pointer;user-select:none}
  .eda-checks input{accent-color:var(--accent)}

  /* table */
  .wrap{padding:0 0 60px}
  table{width:100%;border-collapse:collapse}
  thead th{position:sticky;top:var(--stick-top,96px);z-index:10;background:var(--surface);color:var(--text-muted);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}
  .cat-row td{background:var(--elevated);border-top:1px solid var(--border);border-bottom:1px solid var(--border-muted);padding:6px 12px}
  .cat-name{font-family:'Satoshi',sans-serif;font-weight:600;font-size:12px;letter-spacing:.6px;text-transform:uppercase;color:var(--accent-bright)}
  .cat-count{color:var(--text-dim);font-size:11px;margin-left:8px}
  tbody tr.item{border-bottom:1px solid var(--border-muted);cursor:pointer}
  tbody tr.item:hover{background:var(--row-hover)}
  tbody tr.item.sel{background:var(--accent-dim);box-shadow:inset 3px 0 0 var(--accent)}
  td{padding:8px 12px;vertical-align:middle}
  td.sel-col{width:34px;text-align:center}
  input[type=checkbox]{accent-color:var(--accent);width:15px;height:15px;cursor:pointer}
  .name{font-weight:600}
  .name .mpn{color:var(--text-muted);font-size:11px;font-weight:400}
  .cdesc{color:var(--text-muted);font-size:11px;font-weight:400;margin-top:2px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .dest{color:var(--text);font-size:12px}
  .dest.empty{color:var(--text-dim)}
  .dest .basename{cursor:default;border-bottom:1px dotted var(--text-dim)}
  .namecell{min-width:180px}
  .nameinput{background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:5px;padding:4px 7px;font-size:12px;width:100%;font-family:'JetBrains Mono',ui-monospace,monospace}
  .nameinput:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-dim)}
  .destsub{font-size:10.5px;color:var(--text-dim);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .destsub .basename{border-bottom:1px dotted var(--text-dim);cursor:default;color:var(--text-muted)}
  .destsub .nodest{font-style:italic}

  /* EDA file-availability boxes — one per EDA, each colours on its own */
  .eda-boxes{display:flex;gap:6px}
  .checking-box{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:8px;padding:5px 10px;font-size:11px;color:var(--text-dim)}
  .eda-box{position:relative;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid var(--border);border-radius:8px;background:var(--surface);font-size:11.5px;font-weight:600;cursor:default}
  .eda-box .tally{font-family:'JetBrains Mono',monospace;font-size:9.5px;opacity:.85}
  .eda-box.jlc{cursor:default}
  .newtag{font-size:9px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;color:var(--accent-bright);border:1px solid var(--accent-border);background:var(--accent-dim);border-radius:4px;padding:0 4px;vertical-align:1px}
  .deftag{font-size:9px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;color:var(--purple-light);border:1px solid rgba(140,107,247,.4);background:rgba(140,107,247,.14);border-radius:4px;padding:0 4px;vertical-align:1px}
  .nameinput.locked{opacity:.7;cursor:not-allowed;border-style:dashed}
  .eda-box.green{border-color:var(--green);background:var(--green-dim);color:var(--text)}
  .eda-box.yellow{border-color:var(--yellow);background:var(--yellow-dim);color:var(--text)}
  .eda-box.red{border-color:var(--red);background:var(--red-dim);color:var(--text-muted)}
  .eda-pop{position:absolute;top:calc(100% + 6px);left:0;z-index:40;display:none;flex-direction:column;gap:5px;background:var(--overlay);border:1px solid var(--border);border-radius:8px;padding:9px 11px;min-width:140px;box-shadow:0 12px 34px rgba(0,0,0,.55)}
  .eda-box:hover .eda-pop{display:flex}
  .eda-pop .pophdr{font-size:10px;letter-spacing:.5px;text-transform:uppercase;color:var(--text-dim);margin-bottom:1px}
  .eda-pop .pk{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-dim)}
  .eda-pop .pk.on{color:var(--text)}
  .eda-pop .cb{width:15px;height:15px;border:1px solid var(--text-dim);border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;color:#04231f;flex:none}
  .eda-pop .pk.on .cb{background:var(--green);border-color:var(--green)}
  .eda-pop .pk .tag{margin-left:auto;font-size:9px;color:var(--text-dim);font-style:italic}
  .spin{width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;display:inline-block;animation:sp .7s linear infinite;vertical-align:-2px}
  @keyframes sp{to{transform:rotate(360deg)}}

  .row-actions{display:flex;gap:6px;justify-content:flex-end}
  .icon-btn{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);border-radius:6px;width:30px;height:28px;display:inline-flex;align-items:center;justify-content:center;font-size:13px}
  .icon-btn:hover{border-color:var(--accent-border);color:var(--text)}
  .icon-btn.del:hover{border-color:var(--red);color:var(--red)}
  .icon-btn:disabled{opacity:.3;cursor:not-allowed}
  .icon-btn:disabled:hover{border-color:var(--border);color:var(--text-muted)}
  .badge3d{font-size:10px;color:var(--text-dim);border:1px solid var(--border);border-radius:5px;padding:2px 5px}
  .badge3d.on{color:var(--green);border-color:var(--green)}

  .empty-state{text-align:center;color:var(--text-muted);padding:80px 20px}
  .empty-state h2{font-size:18px;margin-bottom:8px;color:var(--text)}

  /* modal */
  .modal-bg{position:fixed;inset:0;background:rgba(1,4,9,.7);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:50}
  .modal-bg.show{display:flex}
  .modal{background:var(--overlay);border:1px solid var(--border);border-radius:12px;padding:22px;width:440px;max-width:92vw;box-shadow:0 20px 60px rgba(0,0,0,.5)}
  .modal h2{font-size:17px;margin-bottom:6px}
  .modal p{color:var(--text-muted);margin:0 0 16px;line-height:1.5}
  .modal .eda-pick{display:flex;flex-direction:column;gap:10px;margin-bottom:18px}
  .modal .eda-pick label{display:flex;align-items:center;gap:9px;padding:10px 12px;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:13px}
  .modal .eda-pick label:hover{border-color:var(--accent-border);background:var(--accent-dim)}
  .modal-actions{display:flex;gap:8px;justify-content:flex-end}

  /* toast */
  #toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--elevated);border:1px solid var(--accent-border);color:var(--text);padding:10px 16px;border-radius:9px;font-size:12.5px;z-index:60;opacity:0;transition:.2s;pointer-events:none}
  #toast.show{opacity:1}
</style>
</head>
<body>
  <header class="app-header">
    <div class="brand">
      <svg class="logo-chip" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
        <rect x="7" y="7" width="10" height="10" rx="1.5"/>
        <rect x="10.5" y="10.5" width="3" height="3" rx=".5"/>
        <line x1="10" y1="7" x2="10" y2="4"/><line x1="14" y1="7" x2="14" y2="4"/>
        <line x1="10" y1="17" x2="10" y2="20"/><line x1="14" y1="17" x2="14" y2="20"/>
        <line x1="7" y1="10" x2="4" y2="10"/><line x1="7" y1="14" x2="4" y2="14"/>
        <line x1="17" y1="10" x2="20" y2="10"/><line x1="17" y1="14" x2="20" y2="14"/>
      </svg>
      <span class="app-name">chip-fetcher-lite</span>
    </div>
    <span class="subject">· <b id="count">0</b> components</span>
    <span class="send-status" id="sendStatus" style="display:none"></span>
    <span class="spacer"></span>
    <span class="lib-status" id="libStatus"><span class="dot"></span><span class="txt">libraries not probed</span></span>
    <button class="btn" id="probeBtn" title="Probe your EDA libraries">◎ Probe libraries</button>
  </header>

  <div class="controls">
    <div class="add-box">
      <input type="text" id="addInput" placeholder="Add a component — MPN or name, then Enter" autocomplete="off" />
      <button class="btn primary" id="addBtn">+ Add</button>
    </div>
    <div class="pills" id="pills">
      <button class="pill active" data-f="all">All</button>
      <button class="pill" data-f="green">Complete</button>
      <button class="pill" data-f="yellow">Partial</button>
      <button class="pill" data-f="red">Missing</button>
    </div>
    <span class="spacer"></span>
    <button class="btn" id="selectAll">✓ Select all</button>
    <div class="send-group">
      <span class="lbl">send to</span>
      <div class="eda-checks" id="sendEdas">
        <label><input type="checkbox" value="kicad" checked> KiCad</label>
        <label><input type="checkbox" value="altium"> Altium</label>
        <label><input type="checkbox" value="fusion"> Fusion</label>
      </div>
      <button class="btn primary" id="sendBtn">↑ Send to library</button>
    </div>
  </div>

  <div class="wrap">
    <table>
      <thead><tr>
        <th class="sel-col"></th>
        <th>Component</th>
        <th>Symbol name → dest</th>
        <th>Footprint name → dest</th>
        <th>3D dest</th>
        <th id="edaHead">EDA Files Available</th>
        <th style="text-align:right">Actions</th>
      </tr></thead>
      <tbody id="tbody"></tbody>
    </table>
    <div class="empty-state" id="empty" style="display:none">
      <h2>No components yet</h2>
      <div>Add a component above. It'll be checked against the Adom wiki for KiCad, Altium &amp; Fusion files.</div>
    </div>
  </div>

  <!-- probe modal -->
  <div class="modal-bg" id="probeModal">
    <div class="modal">
      <h2>Probe your EDA libraries?</h2>
      <p>To show where each component will be filed, I can scan your local libraries. Pick which EDA software you use:</p>
      <div class="eda-pick">
        <label><input type="checkbox" value="kicad" checked> <span>KiCad</span></label>
        <label><input type="checkbox" value="altium"> <span>Altium</span></label>
        <label><input type="checkbox" value="fusion"> <span>Fusion 360</span></label>
      </div>
      <div class="modal-actions">
        <button class="btn" id="probeSkip">Skip for now</button>
        <button class="btn primary" id="probeGo">⌖ Probe my libraries</button>
      </div>
    </div>
  </div>

  <input type="file" id="fileInput" style="display:none" />
  <div id="toast"></div>

<script>
const $ = s => document.querySelector(s);
let STATE = { components:[], probe:{edas:[]}, jobs:[] };
let FILTER = 'all';
let uploadTarget = null;

function toast(msg){ const t=$('#toast'); t.textContent=msg; t.classList.add('show'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('show'),2600); }
// Resolve API paths relative to the page (works behind the Hydrogen proxy at /proxy/<port>/).
function apiUrl(path){ return new URL('api'+path, document.baseURI).href; }
async function api(path, opts){ const r = await fetch(apiUrl(path), opts); if(!r.ok && r.status!==409){ const e=await r.json().catch(()=>({})); throw Object.assign(new Error(e.error||r.statusText),{status:r.status,body:e}); } return r.status===409 ? Object.assign({_conflict:true}, await r.json()) : r.json(); }

// EDAs the user actually probed for; before any probe, show all three.
function activeEdas(){
  const e=(STATE.probe&&STATE.probe.edas)||[];
  const v=e.filter(x=>['kicad','altium','fusion'].includes(x));
  return v.length?v:['kicad','altium','fusion'];
}
const KINDS=[['symbol','Symbol'],['footprint','Footprint'],['model3d','3D']];
// each EDA box colours independently by how many of symbol/footprint/3d it has
function edaColor(c,eda){
  const e=c.eda[eda]||{}; const n=['symbol','footprint','model3d'].filter(k=>e[k]&&e[k].present).length;
  return n===3?'green':n>=1?'yellow':'red';
}
// component-level status (for the filter pills): green=all active boxes green, red=all red, else yellow
function compStatus(c){
  if(c.wiki.status==='checking') return 'checking';
  const cols=['kicad','altium','fusion'].map(e=>edaColor(c,e));
  if(cols.every(x=>x==='green')) return 'green';
  if(cols.every(x=>x==='red')) return 'red';
  return 'yellow';
}
// editable library name (defaults to MPN) + the destination it'll be filed into
function destBasename(d){
  if(!d||!d.name) return '<span class="nodest">no destination yet</span>';
  let tag='';
  if(d.source==='kicad-default') tag=' <span class="deftag">KiCad default</span>';
  else if(d.exists===false) tag=' <span class="newtag">new</span>';
  return `→ <span class="basename" title="${esc(d.path||d.name)}">${esc(d.name)}</span>${tag}`;
}
function nameDestCell(c, kind){
  const d=c.dest&&c.dest[kind];
  const nm=(c.names&&c.names[kind])!=null?c.names[kind]:c.mpn;
  const locked = kind==='footprint' && c.footprintLocked;
  const attrs = locked ? 'readonly class="nameinput locked" title="Standard KiCad footprint — name is fixed"' : `class="nameinput" title="Name this ${kind} will get in the library"`;
  return `<td class="namecell"><input ${attrs} data-name="${kind}" data-id="${c.id}" value="${esc(nm)}" spellcheck="false"><div class="destsub">${destBasename(d)}</div></td>`;
}
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m])); }

const EDA_NAME={kicad:'KiCad',altium:'Altium',fusion:'Fusion'};
function edaBox(c,eda){
  const e=c.eda[eda]||{};
  const n=['symbol','footprint','model3d'].filter(k=>e[k]&&e[k].present).length;
  const rows=KINDS.map(([k,label])=>{
    const v=e[k]||{}; const on=v.present;
    const tag=on?`<span class="tag">${v.source==='upload'?'uploaded':'wiki'}</span>`:'';
    return `<div class="pk ${on?'on':''}"><span class="cb">${on?'✓':''}</span>${label}${tag}</div>`;
  }).join('');
  return `<div class="eda-box ${edaColor(c,eda)}" title="Hover for symbol / footprint / 3D status">${EDA_NAME[eda]} <span class="tally">${n}/3</span>`+
         `<div class="eda-pop"><div class="pophdr">${EDA_NAME[eda]} files on wiki</div>${rows}</div></div>`;
}
function jlcBox(c){
  const on=!!c.jlc;
  const row = on
    ? `<div class="pk on"><span class="cb">✓</span>${esc(c.jlcPart||'available')}</div>`
    : `<div class="pk"><span class="cb"></span>Part number not on wiki</div>`;
  return `<div class="eda-box jlc ${on?'green':'red'}" data-jlcedit="${c.id}" style="cursor:pointer"><span style="pointer-events:none">JLC</span><div class="eda-pop"><div class="pophdr">JLCPCB part number — click to edit</div>${row}</div></div>`;
}
function statusCell(c){
  if(c.wiki.status==='checking') return `<div class="checking-box"><span class="spin"></span>&nbsp;checking wiki…</div>`;
  // always show all three EDA boxes + a JLC presence box; probe selection only affects the status text
  return `<div class="eda-boxes">${['kicad','altium','fusion'].map(e=>edaBox(c,e)).join('')}${jlcBox(c)}</div>`;
}

const EDA_LABEL={kicad:'KiCad',altium:'Altium',fusion:'Fusion'};
function updateLibStatus(){
  const el=$('#libStatus'); if(!el) return;
  const p=STATE.probe||{};
  const found=p.libraries?Object.keys(p.libraries):[];
  let cls,txt;
  if(p.probedAt){
    if(found.length){ cls='ok'; txt='libraries found: '+found.map(e=>EDA_LABEL[e]||e).join(', '); }
    else { cls='none'; txt='no libraries found'; }
  } else if(p.status==='requested'){ cls='warn'; txt='probing libraries…'; }
  else { cls='none'; txt='libraries not probed'; }
  el.className='lib-status '+cls;
  el.querySelector('.txt').textContent=txt;
}
function updateSendStatus(){
  const el=$('#sendStatus'); if(!el) return;
  const s=STATE.send||{};
  if(s.status==='running'){ el.style.display='inline-flex'; el.className='send-status running'; el.innerHTML=`<span class="spin"></span> Sending ${s.done||0}/${s.total||0} — ${esc(s.current||'')}`; }
  else if(s.status==='done'){ el.style.display='inline-flex'; el.className='send-status done'; el.textContent='✓ '+(s.current||'Sent to library'); }
  else if(s.status==='error'){ el.style.display='inline-flex'; el.className='send-status err'; el.textContent='Send error: '+(s.current||''); }
  else { el.style.display='none'; }
}
function updateStickyOffsets(){
  const h=$('.app-header'), c=$('.controls'); if(!h||!c) return;
  const hh=h.offsetHeight, ch=c.offsetHeight;
  document.documentElement.style.setProperty('--header-h', hh+'px');
  document.documentElement.style.setProperty('--stick-top', (hh+ch)+'px');
}
window.addEventListener('resize', updateStickyOffsets);
function render(){
  $('#count').textContent = STATE.components.length;
  updateLibStatus();
  updateSendStatus();
  updateStickyOffsets();
  const list = STATE.components.filter(c=>{
    if(FILTER==='all') return true;
    if(c.wiki.status==='checking') return false;
    return compStatus(c)===FILTER;
  });
  $('#empty').style.display = STATE.components.length? 'none':'block';
  // group by type
  const groups={};
  for(const c of list){ const t=c.type||'IC / Other'; (groups[t]=groups[t]||[]).push(c); }
  const tbody=$('#tbody'); tbody.innerHTML='';
  const types=Object.keys(groups).sort();
  for(const t of types){
    const cat=document.createElement('tr'); cat.className='cat-row';
    cat.innerHTML=`<td colspan="7"><span class="cat-name">${esc(t)}</span><span class="cat-count">${groups[t].length}</span></td>`;
    tbody.appendChild(cat);
    for(const c of groups[t]){
      const tr=document.createElement('tr'); tr.className='item'+(c.selected?' sel':''); tr.dataset.id=c.id;
      const d3 = c.dest&&c.dest.model3d;
      const has3d = c.wikiFiles && c.wikiFiles['3d'];
      tr.innerHTML =
        `<td class="sel-col"><input type="checkbox" ${c.selected?'checked':''} data-sel="${c.id}"></td>`+
        `<td class="name">${esc(c.name)}${c.description?`<div class="cdesc" title="Symbol description">${esc(c.description)}</div>`:''}</td>`+
        nameDestCell(c,'symbol')+
        nameDestCell(c,'footprint')+
        `<td class="dest ${d3&&d3.name?'':'empty'}">${d3&&d3.name?`<span class="basename" title="${esc(d3.path||d3.name)}">${esc(d3.name)}</span>${d3.exists===false?' <span class="newtag">new</span>':''}`:(has3d?'<span class="badge3d on">3D on wiki</span>':'—')}</td>`+
        `<td>${statusCell(c)}</td>`+
        `<td><div class="row-actions">`+
          (c.wiki&&c.wiki.url
            ? `<button class="icon-btn" data-wiki="${esc(c.wiki.url)}" title="Open wiki page: ${esc(c.wiki.url)}">↗</button>`
            : `<button class="icon-btn" disabled title="No wiki page found">↗</button>`)+
          `<button class="icon-btn" data-up="${c.id}" title="Upload a file">⬆</button>`+
          `<button class="icon-btn" data-check="${c.id}" title="Re-check wiki">↻</button>`+
          `<button class="icon-btn del" data-del="${c.id}" title="Remove">✕</button>`+
        `</div></td>`;
      tbody.appendChild(tr);
    }
  }
}

// ---- events
$('#addBtn').onclick = addComponent;
$('#addInput').addEventListener('keydown', e=>{ if(e.key==='Enter') addComponent(); });
async function addComponent(){
  const inp=$('#addInput'); const name=inp.value.trim(); if(!name) return;
  inp.value='';
  try{ const c=await api('/components',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
    if(c._duplicate || STATE.components.some(x=>x.id===c.id)){ toast(`${c.name} is already in the list`); render(); return; }
    STATE.components.unshift(c); render(); toast(`Added ${name} — checking wiki…`);
  }catch(e){ toast('Add failed: '+e.message); }
}

$('#tbody').addEventListener('click', async e=>{
  const t=e.target;
  if(t.dataset.del){ if(!confirm('Remove this component and its staged files?'))return; await api(`/components/${t.dataset.del}`,{method:'DELETE'}); STATE.components=STATE.components.filter(x=>x.id!==t.dataset.del); render(); return; }
  if(t.dataset.check){ const id=t.dataset.check; const c=STATE.components.find(x=>x.id===id); if(c){c.wiki.status='checking';render();} const nc=await api(`/components/${id}/check`,{method:'POST'}); replaceComp(nc); render(); return; }
  if(t.dataset.up){ uploadTarget=t.dataset.up; $('#fileInput').value=''; $('#fileInput').click(); return; }
  if(t.dataset.wiki){ window.open(t.dataset.wiki,'_blank','noopener'); return; }
  const jb=t.closest('[data-jlcedit]');
  if(jb){ const id=jb.dataset.jlcedit; const c=STATE.components.find(x=>x.id===id); if(!c)return;
    const pn=prompt('JLCPCB / LCSC part number for '+c.name+':', c.jlcPart||'');
    if(pn===null) return;
    const nc=await api(`/components/${id}/jlc`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jlcPart:pn.trim()})});
    replaceComp(nc); render(); toast(pn.trim()?('JLC PN set: '+pn.trim()):'JLC PN cleared'); return; }
  if(t.dataset.sel!==undefined){ return setSelected(t.dataset.sel, t.checked); } // checkbox
  // click anywhere else on the row toggles selection (but not on inputs/buttons/eda boxes)
  if(t.closest('input,button,.eda-box,.nameinput,.icon-btn,a')) return;
  const tr=t.closest('tr.item'); if(!tr) return;
  const c=STATE.components.find(x=>x.id===tr.dataset.id); if(!c) return;
  setSelected(tr.dataset.id, !c.selected);
});
async function setSelected(id, on){
  const c=STATE.components.find(x=>x.id===id); if(c) c.selected=!!on; render();
  try{ await api(`/components/${id}/select`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({on:!!on})}); }catch(e){}
}

// editable symbol/footprint library names
$('#tbody').addEventListener('change', async e=>{
  const t=e.target; if(t.dataset.name===undefined) return;
  const id=t.dataset.id, kind=t.dataset.name, val=t.value.trim();
  const c=STATE.components.find(x=>x.id===id); if(c){ c.names=c.names||{}; c.names[kind]=val; }
  try{ await api(`/components/${id}/names`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({[kind]:val})}); toast(`${kind[0].toUpperCase()+kind.slice(1)} name → ${val}`); }
  catch(err){ toast('Rename failed: '+err.message); }
});
$('#tbody').addEventListener('keydown', e=>{ if(e.target.classList.contains('nameinput') && e.key==='Enter') e.target.blur(); });

$('#fileInput').addEventListener('change', async e=>{
  const f=e.target.files[0]; if(!f||!uploadTarget) return;
  const id=uploadTarget; const buf=await f.arrayBuffer();
  const q=`?filename=${encodeURIComponent(f.name)}`;
  try{
    let res=await api(`/components/${id}/upload${q}`,{method:'POST',body:buf});
    if(res._conflict){
      if(!confirm(`A ${res.existing.eda} ${res.existing.kind} file (${res.existing.name}) is already staged. Replace it with ${f.name}?`)) return;
      res=await api(`/components/${id}/upload${q}&replace=1`,{method:'POST',body:buf});
    }
    replaceComp(res); render(); toast(`Uploaded ${f.name}`);
  }catch(err){ toast('Upload failed: '+err.message); }
});

$('#pills').addEventListener('click', e=>{ if(!e.target.dataset.f)return; FILTER=e.target.dataset.f; [...$('#pills').children].forEach(p=>p.classList.toggle('active',p.dataset.f===FILTER)); render(); });

let allOn=false;
$('#selectAll').onclick=async()=>{ allOn=!allOn; await api('/select-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({on:allOn})}); STATE.components.forEach(c=>c.selected=allOn); $('#selectAll').textContent=(allOn?'✕ Deselect all':'✓ Select all'); render(); };

$('#sendBtn').onclick=async()=>{
  const ids=STATE.components.filter(c=>c.selected).map(c=>c.id);
  if(!ids.length){ toast('Select components first'); return; }
  const edas=[...$('#sendEdas').querySelectorAll('input:checked')].map(i=>i.value);
  if(!edas.length){ toast('Pick at least one EDA target'); return; }
  try{
    await api('/send-to-library',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids,edas})});
    toast(`Sending ${ids.length} component(s) to ${edas.join(', ')}…`); poll();
  }catch(e){ toast('Send failed: '+e.message); }
};

// ---- probe
function showProbe(){ $('#probeModal').classList.add('show'); }
$('#probeBtn').onclick=showProbe;
$('#probeSkip').onclick=()=>$('#probeModal').classList.remove('show');
$('#probeGo').onclick=async()=>{
  const edas=[...$('#probeModal').querySelectorAll('.eda-pick input:checked')].map(i=>i.value);
  if(!edas.length){ toast('Pick at least one EDA'); return; }
  await api('/probe-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({edas})});
  // sync the send-to targets to what they use
  $('#sendEdas').querySelectorAll('input').forEach(i=>i.checked=edas.includes(i.value));
  $('#probeModal').classList.remove('show');
  toast('Probe requested — the agent will scan your '+edas.join(', ')+' libraries.');
};

function replaceComp(nc){ const i=STATE.components.findIndex(x=>x.id===nc.id); if(i>=0)STATE.components[i]=nc; }

// ---- poll
async function poll(){
  try{ const s=await api('/state'); STATE=s;
    // don't rebuild the table out from under an in-progress edit
    if(!document.activeElement || !document.activeElement.classList.contains('nameinput')) render();
  }catch(e){}
}
async function init(){
  await poll();
  // offer the probe on first launch if not done yet
  if(!STATE.probe || (!STATE.probe.probedAt && STATE.probe.status!=='requested')) showProbe();
  setInterval(poll, 2500);
}
init();
</script>
</body>
</html>