← Commit history

chip-fetcher: visible 3D source chips (KiCad lib vs Mfr direct) on every card; JST playbook §7a inbox-dedup rule (STEP-only, skip if in library, filtered alias, no resubmits)

John Lauer ·b98ddee621 ·3mo ago ·parent ca4cd2a
3 files changed +106−7
playbooks/jst.md+26−2
@@ -168,13 +168,37 @@ SH `SM02B-SRSS-TB`, GH `BM02B-GHS-TBT`, VH `B2P-VH` (VH STEP via email).  - **The full catalog is ~600 series × tens of thousands of parts** = that many   form submits + emails. Throttle hard (2-5s human-like delays, per critical-  rule #9), route mail to a filtered alias, and expect JST to rate-limit. Do-  NOT blast the whole catalog without the user scoping it.+  rule #9) and expect JST to rate-limit. Do NOT blast the whole catalog without+  the user scoping it. - One email per file, one file per `(series, doc, filename)` GET→POST. STEP and   IGES are separate requests/emails. - Heartbeat each stage: `adom-chip-fetcher heartbeat <PART> "probing jst-mfg.com"`   / `"requested STEP — awaiting email"` / `"imported"`. +### 7a. Inbox hygiene — DEDUP before every request (mandatory)++Every JST request emails the user a file. Re-requesting spams their inbox.+Before submitting the agreement form for any `(part, format)`:++1. **Skip if already in the library.** If `library/<MPN>/<MPN>.step` exists (or+   the format you want), do NOT request it again. Check first, every time.+2. **STEP only by default.** Request `doc=2` (STEP). Only fall back to `doc=1`+   (IGES) when the series has no STEP column. Never grab both "to be safe."+3. **One submit per file, ever.** Do not re-submit a part you already requested+   this session (track what you've sent). A double-click / retry = a second+   email. The form's success alert is your confirmation — don't resubmit to+   "make sure."+4. **Route to a filtered alias, not the raw inbox.** Use the user's plus-alias+   (e.g. `[email protected]`) with a label/filter so the batch doesn't bury+   their inbox. See [`jst-email-retrieval.md`](jst-email-retrieval.md).+5. **Clean up after a proof/demo.** Test/demo runs that submit the form generate+   real emails — trash the duplicates afterward+   (`adom-google api -X POST .../messages/<id>/trash`).++Skipping dedup is how a 5-part batch becomes a 12-email inbox flood (learned the+hard way 2026-06-27: 6 emails for 3 unique parts due to IGES+STEP grabs, a+demo re-submit, and a double-submit).+ ## Source plugin  `src/sources/jst.rs` — `series_index_url`, `series_url`, `search_url`,
src/serve.rs+3−3
@@ -986,7 +986,7 @@ fn per_source_breakdown(d: &std::path::Path, mpn: &str, has_global_step: bool) -         if ki.1 { files.push(dl(".kicad_mod", format!("{mpn}.kicad_mod"), false)); }         if has_global_step { files.push(dl(".step", format!("{mpn}.step"), false)); }         orig_zip(&mut files);-        lines.push(json!({ "label": format!("{src} - KiCad Library"), "sym": ki.0, "fp": ki.1,+        lines.push(json!({ "label": format!("KiCad library, from {src}"), "sym": ki.0, "fp": ki.1,             "three_d": has_global_step, "three_d_from_lib": three_lib, "files": files }));     }     let al = (has(format!("{mpn}.SchLib")), has(format!("{mpn}.PcbLib")));@@ -996,7 +996,7 @@ fn per_source_breakdown(d: &std::path::Path, mpn: &str, has_global_step: bool) -         if al.1 { files.push(dl(".PcbLib", format!("{mpn}.PcbLib"), false)); }         if has_global_step { files.push(dl(".step", format!("{mpn}.step"), false)); }         orig_zip(&mut files);-        lines.push(json!({ "label": format!("{src} - Altium Library"), "sym": al.0, "fp": al.1,+        lines.push(json!({ "label": format!("Altium library, from {src}"), "sym": al.0, "fp": al.1,             "three_d": has_global_step, "three_d_from_lib": three_lib, "files": files }));     }     // EAGLE/Fusion .lbr is a single file holding BOTH the device (symbol) + package (footprint).@@ -1004,7 +1004,7 @@ fn per_source_breakdown(d: &std::path::Path, mpn: &str, has_global_step: bool) -         let mut files = vec![dl(".lbr", format!("{mpn}.lbr"), false)];         if has_global_step { files.push(dl(".step", format!("{mpn}.step"), false)); }         orig_zip(&mut files);-        lines.push(json!({ "label": format!("{src} - Fusion 360 Library"), "sym": true, "fp": true,+        lines.push(json!({ "label": format!("Fusion 360 library, from {src}"), "sym": true, "fp": true,             "three_d": has_global_step, "three_d_from_lib": three_lib, "files": files }));     }     json!(lines)
src/ui/index.html+77−2
@@ -276,6 +276,17 @@   .card .thumb-3d-group .orient-more-btn { position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%); z-index: 5; background: var(--panel); border: 1px solid var(--border); color: var(--muted); border-radius: 3px; padding: 0 6px; font-size: 9px; line-height: 1.5; cursor: pointer; opacity: 0; transition: opacity .12s; }   .card:hover .thumb-3d-group .orient-more-btn, .card.selected .thumb-3d-group .orient-more-btn { opacity: 1; }   .card .thumb-3d-group .orient-more-btn:hover { color: var(--accent); border-color: var(--accent); }+  /* Always-visible 3D source chip (KiCad lib / Mfr direct / SnapMagic …). Color = source, not the model's CAD color. Namespaced src3d- to avoid the sym/fp carat's .src-chip. */+  .card .thumb-3d-group .src3d-chip { position: absolute; top: 3px; left: 3px; z-index: 6; display: inline-flex; align-items: center; gap: 4px;+    font-size: 9px; line-height: 1; font-weight: 600; letter-spacing: .2px; padding: 3px 6px; border-radius: 999px;+    background: rgba(13,17,23,.85); color: #cdd9e5; border: 1px solid var(--border); pointer-events: auto; }+  .card .thumb-3d-group .src3d-chip .src3d-dot { width: 7px; height: 7px; border-radius: 50%; background: #8b949e; flex: none; }+  .card .thumb-3d-group .src3d-chip.src3d-mfr     { color: #7ee2c8; border-color: #1f6f5c; } .src3d-chip.src3d-mfr .src3d-dot     { background: #19c37d; }+  .card .thumb-3d-group .src3d-chip.src3d-kicad   { color: #9fc6ff; border-color: #2c4a73; } .src3d-chip.src3d-kicad .src3d-dot   { background: #4f8cff; }+  .card .thumb-3d-group .src3d-chip.src3d-ul      { color: #ffd479; border-color: #6b5320; } .src3d-chip.src3d-ul .src3d-dot      { background: #e6b450; }+  .card .thumb-3d-group .src3d-chip.src3d-snap    { color: #d6a8ff; border-color: #4a2c6b; } .src3d-chip.src3d-snap .src3d-dot    { background: #b46bff; }+  .card .thumb-3d-group .src3d-chip.src3d-mouser, .card .thumb-3d-group .src3d-chip.src3d-dk { color: #9fc6ff; } .src3d-chip.src3d-mouser .src3d-dot, .src3d-chip.src3d-dk .src3d-dot { background: #5a8fd6; }+  .card .thumb-3d-group .src3d-chip.src3d-unknown { color: #ff9b8a; border-color: #6b2f24; } .src3d-chip.src3d-unknown .src3d-dot { background: #ff5a52; }   /* Card zoom — animate from the card's grid position to centered,      so the user tracks which card is zooming. Uses CSS transition on      top/left/transform with a short ease-out. */@@ -1072,6 +1083,7 @@ function render() {         <!-- Secondary row: 3D up-axis QA + the .lbr packaging action — smaller. -->         <div class="thumb-row secondary">           <div class="thumb-3d-group">+            ${srcChipHtml(c)}             ${renderThumb(c.mpn, 'chip3d',     c.has_thumb_chip3d,     'Z↑', { upAxis: 'z', chosen: c.chosen_up_axis, inferred: c.inferred_up_axis }, thumbErr(c, '3d'))}             ${renderThumb(c.mpn, 'chip3d-yup', c.has_thumb_chip3d_yup, 'Y↑', { upAxis: 'y', chosen: c.chosen_up_axis, inferred: c.inferred_up_axis }, thumbErr(c, '3d'))}             <button class="orient-more-btn" data-tooltip="Pick from all 6 face-up orientations.\nZ↑ and Y↑ cover 98% of chips. For unusual STEP files (WAGO connectors, through-hole parts), try X↑, X↓, Y↓, or Z↓. The chosen orientation is saved to info.json for downstream tools." data-mpn="${escapeHtml(c.mpn)}">▾</button>@@ -1097,12 +1109,12 @@ function render() {         ${lifecycleBadge}         ${stockBadge}         ${priceBadge}-        <span class="b ${c.has_step?'step':'miss'}" data-tooltip="${c.has_step?'3D STEP model present.\nUsed by chipsmith for pin detection + footprint validation, and by chip-thumbnailer for the 3D orientation renders you see above.' + (c.step_source ? '\nSource: ' + c.step_source : '') + provenanceSummary(c.info, c.file_provenance, c.mpn):'3D STEP model missing.\nNeeded for chipsmith validation + 3D thumbnails.' + provenanceSummary(c.info, c.file_provenance, c.mpn)}">STEP${c.has_step?'':' ?'}</span>+        <span class="b ${c.has_step?'step':'miss'}" data-tooltip="${c.has_step?'Source: ' + resolveStepSource(c).label + '\n\nThe 3D STEP model — used by chipsmith for pin detection + footprint validation, and by chip-thumbnailer for the orientation renders above.':'3D STEP model missing.\nNeeded for chipsmith validation + 3D thumbnails.'}">STEP${c.has_step?'':' ?'}</span>         ${ked}         ${alt}         ${fus}         <span class="b ${c.pdf_count>0?'pdf':'miss'}${c.has_pdf ? ' clickable' : ''}" data-tooltip="${c.pdf_count>0?c.pdf_count+' datasheet PDF'+(c.pdf_count>1?'s':'')+' in the chip directory.\nClick to open in the PDF viewer.':'No datasheet PDF yet.\nUse ⤓ → Manufacturer → Datasheet PDF to fetch it.'}" ${c.has_pdf ? `data-thumb-mpn="${escapeHtml(c.mpn)}" data-thumb-kind="datasheet"` : ''}>${c.pdf_count>0 ? c.pdf_count : '?'} PDF${c.pdf_count===1?'':'s'}</span>-        <span class="b src ${tier}" data-tooltip="STEP file source: ${escapeHtml(c.step_source)}.\n${c.step_source==='—'?'No STEP source recorded yet.':'This is where the 3D model came from. The sourcing ladder (Mfr → SnapMagic → Mouser → DigiKey → Arrow → CSE/UL → LCSC) determines priority.'}">${escapeHtml(srcShort(c.step_source))}</span>+        <span class="b src ${tier}" data-tooltip="${(function(){var r=resolveStepSource(c);return r.found?'STEP source: '+r.label+'.\nThis is where the 3D model came from. Ladder priority: Mfr → SnapMagic → Mouser → DigiKey → Arrow → CSE/UL → LCSC.':'STEP source '+r.label+'.\nchip-fetcher downloaded this STEP but did not record where from — refetch to capture it.';})()}">${escapeHtml((function(){var r=resolveStepSource(c);return r.found?srcShort(r.label):'src ?';})())}</span>       </div>     `;     card.onclick = (ev) => {@@ -1709,6 +1721,69 @@ function provenanceSummary(info, fileProv, mpn) {   return '\nSourced via: ' + sources.join(' > '); } +// Resolve the BEST available STEP source from every signal we have, instead of+// showing a bare "—" when the data is sitting right there. Priority:+//   1) per-file provenance for the .step (who MADE it + where we FOUND it)+//   2) the step_source field on info.json+//   3) the fetched_via_chain entry that succeeded+// Returns { label, found } — found=false means it genuinely was never recorded.+// Always-visible "where did this 3D model come from" chip (KiCad lib vs Mfr+// direct vs SnapMagic, etc.) — color is the CAD author's, NOT the source, so+// we label the source explicitly. Reads the same provenance resolveStepSource does.+function srcChipHtml(c) {+  if (!c.has_step) return '';+  var r = resolveStepSource(c);+  var s = (r.label || '').toLowerCase(), cls = 'other', short = r.label;+  if (/manufacturer|mfr|direct/.test(s))      { cls = 'mfr';     short = 'Mfr direct'; }+  else if (/kicad/.test(s))                   { cls = 'kicad';   short = 'KiCad lib'; }+  else if (/ultralib/.test(s))                { cls = 'ul';      short = 'Ultra Librarian'; }+  else if (/snap/.test(s))                    { cls = 'snap';    short = 'SnapMagic'; }+  else if (/mouser|cse|samacsys/.test(s))     { cls = 'mouser';  short = 'Mouser CSE'; }+  else if (/digikey/.test(s))                 { cls = 'dk';      short = 'DigiKey'; }+  else if (/lcsc/.test(s))                    { cls = 'lcsc';    short = 'LCSC'; }+  else if (!r.found || /not recorded|unknown|\?/.test(s)) { cls = 'unknown'; short = 'source ?'; }+  var tip = '3D model source: ' + r.label + '\n\nThe blue/grey model color is the CAD author choice, not its source. This chip is the real source.';+  return '<div class="src3d-chip src3d-' + cls + '" data-tooltip="' + escapeHtml(tip) + '">' ++         '<span class="src3d-dot"></span>' + escapeHtml(short) + '</div>';+}++function resolveStepSource(c) {+  var mpn = c.mpn, fp = c.file_provenance, info = c.info || {};+  // 1) per-file provenance for <mpn>.step+  if (fp && typeof fp === 'object') {+    for (var fn in fp) {+      if (!/\.step$/i.test(fn)) continue;+      var e = fp[fn] || {};+      var made = e.content_origin || '', found = e.discovery_source || '';+      if (made === 'unknown') made = '';+      if (found === 'unknown') found = '';+      // The KiCad standard library is one source, not "kicad (via kicad-library)".+      var kic = function(s){ return /^kicad(-library|_community)?$/.test(s); };+      if (kic(made) || kic(found)) {+        return { label: 'KiCad standard library' + (e.derived ? ' (derived)' : ''), found: true };+      }+      if (made || found) {+        var lbl = made || found;+        if (made && found && found !== made) lbl = made + ' (via ' + found + ')';+        if (e.derived) lbl += ' (derived)';   // backfilled, not recorded at fetch+        return { label: lbl, found: true };+      }+    }+  }+  // 2) explicit step_source field+  var ss = (c.step_source || '').trim();+  if (ss && ss !== '—' && ss.toLowerCase() !== 'none') return { label: ss, found: true };+  // 3) the chain entry that actually succeeded+  var chain = info.fetched_via_chain || [];+  for (var i = 0; i < chain.length; i++) {+    var o = (chain[i].outcome || '').toLowerCase();+    if (o.indexOf('ok') > -1 || o.indexOf('success') > -1 || o === 'got it') {+      return { label: chain[i].source || '?', found: true };+    }+  }+  return { label: 'not recorded — refetch to capture it', found: false };+}+ // One badge per EDA tool: green if both halves present, amber if partial, // grey "?" if missing. Fusion uses one .lbr containing both sym+mod, so // callers pass the same flag twice for that tool.