app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
← 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)
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.