app
Parts Search
Public Made by Adomby adom
One search box across Mouser, DigiKey and JLCPCB: parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time.
← Commit history
v2.0.7: Module 2.4.0: the panel gutter check now measures against the containing block
7 files changed
+105−104
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "2.0.6"+version = "2.0.7" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "2.0.6"+version = "2.0.7" edition = "2021" description = "Parts Search Electronics component search — CLI, backend HTTP service, and Hydrogen webview app" license = "MIT"
VERSION+1−1@@ -1 +1 @@-2.0.6+2.0.7
package.json+2−41@@ -1,6 +1,6 @@ { "slug": "adom-parts-search",- "version": "2.0.7",+ "version": "2.0.4", "type": "app", "description": "One search box across Mouser, DigiKey and JLCPCB. Parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time. Dock Bar ready: serve, health, settings, Say it.", "dependencies": {@@ -38,44 +38,5 @@ "SKILL.md", "BUILD-SKILL.md", "CHANGELOG.md"- ],- "changelog": "Module 2.4.0: the panel gutter check now measures against the containing block",- "discovery_triggers": [- "parts search",- "find part",- "source component",- "source part",- "compare distributors",- "compare vendors",- "Mouser or DigiKey",- "BOM sourcing",- "price this part",- "where can I get",- "check stock",- "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"- ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",- "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- } ]-}+}
page.json+9−50@@ -4,7 +4,7 @@ "slug": "adom-parts-search", "title": "Parts Search", "brief": "One search box across Mouser, DigiKey and JLCPCB: parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time.",- "version": "2.0.7",+ "version": "2.0.4", "install": { "binary_name": "adom-parts-search", "install_dir": "",@@ -33,25 +33,13 @@ }, "hero": { "type": "image",- "path": "screenshots/hero.png"+ "path": "docs/hero.png" }, "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- }+ "source an STM32F103RBT6",+ "where can I get an ESP32-S3-WROOM cheapest and fastest",+ "compare Mouser, DigiKey and JLCPCB for an LM555",+ "price this BOM across distributors" ], "discovery_triggers": [ "parts search",@@ -66,41 +54,12 @@ "where can I get", "check stock", "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"+ "fastest delivery" ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",+ "discovery_pitch": "Search Mouser + DigiKey + JLCPCB in parallel from one interface, with product photos and a Mouser-preferred best-source pick.", "metadata": { "platform": "linux" }, "category": "tools",- "org": "adom",- "description": "One search box across Mouser, DigiKey and JLCPCB. Parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time. Dock Bar ready: serve, health, settings, Say it.",- "dependencies": {- "adom/adom-mouser": "*",- "adom/adom-digikey": "*",- "adom/adom-jlcpcb": "*"- },- "needs_sudo": false,- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "files": [- "bin/**",- "skills/**",- "docs/icon.svg",- "dockbar.json",- "install.sh",- "uninstall.sh",- "package.json",- "README.md",- "SKILL.md",- "BUILD-SKILL.md",- "CHANGELOG.md"- ],- "changelog": "Module 2.4.0: the panel gutter check now measures against the containing block"+ "org": "adom" }
src/app/mod.rs+60−8@@ -251,6 +251,47 @@ fn hpost_with(path: &str, body: Value, timeout_ms: u64) -> Result<(u16, Value), } } +/// PROXY ROUTES FORWARD BYTES, NOT PARSED JSON.+///+/// Parsing an upstream body and re-emitting it is the natural way to write a+/// proxy and it is wrong here. serde's map is ordered, so a round trip+/// REORDERS KEYS alphabetically, drops the upstream's whitespace, and can+/// retype a number. Every one of those is invisible to a "does it 200" check,+/// while the two things the panel module actually reads are exactly what a+/// retype breaks: it compares `updater.current_version` NUMERICALLY after+/// splitting on dots, and reads `delivered_to` as an ARRAY. So the proxy+/// forwards the status code and the body bytes untouched. Parse upstream JSON+/// only for OUR OWN decisions, never on the way back out.+/// (Caught by adom-ui-design's proxy-fidelity harness, 2026-08-25: my GET+/// route emitted the same JSON with the keys in a different order.)+fn hraw(path: &str, body: Option<Vec<u8>>, timeout_ms: u64) -> Result<(u16, Vec<u8>, String), String> {+ use std::io::Read as _;+ let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(timeout_ms)).build();+ let url = format!("{}{}", hydrogen_ctl(), path);+ let sent = match body {+ Some(b) => agent.post(&url).set("Content-Type", "application/json").send_bytes(&b),+ None => agent.get(&url).call(),+ };+ // A 4xx/5xx from upstream is a RESPONSE to forward, not a transport error.+ let r = match sent {+ Ok(r) => r,+ Err(ureq::Error::Status(_, r)) => r,+ Err(e) => return Err(e.to_string()),+ };+ let code = r.status();+ let ctype = r.header("Content-Type").unwrap_or("application/json").to_string();+ let mut buf: Vec<u8> = Vec::new();+ r.into_reader().read_to_end(&mut buf).map_err(|e| e.to_string())?;+ Ok((code, buf, ctype))+}++fn raw_resp(code: u16, bytes: Vec<u8>, ctype: &str) -> Response<std::io::Cursor<Vec<u8>>> {+ with_cors(Response::from_data(bytes).with_status_code(code)+ .with_header(Header::from_bytes("Content-Type", ctype.as_bytes())+ .unwrap_or_else(|_| Header::from_bytes("Content-Type", "application/json").unwrap()))+ .with_header(Header::from_bytes("Cache-Control", "no-store").unwrap()))+}+ /// The shell's RELEASE version, from /updater/status. Never /buildinfo's /// build_sha: build.rs caches HD_BUILD_SHA, so a freshly compiled exe can /// report an older commit (that lie cost a false conclusion on 2026-08-24).@@ -488,16 +529,27 @@ fn handle(mut req: Request, state: Arc<AppState>, no_tab: bool, tab_name: &str) return; } else if method == Method::Get && ends("/updater/status") { // Proxied so the panel module can gate on the shell version from our- // own origin (no cross-origin fetch, no CORS dependency).- match hget("/updater/status") { Ok(v) => (200, v), Err(e) => (502, json!({"error": e})) }+ // own origin (no cross-origin fetch, no CORS dependency). Bytes and+ // status forwarded verbatim: see hraw.+ let _ = match hraw("/updater/status", None, 8000) {+ Ok((code, bytes, ct)) => req.respond(raw_resp(code, bytes, &ct)),+ Err(e) => req.respond(json_resp(502, json!({"error": e}))),+ };+ return; } else if method == Method::Post && ends("/ai-threads/run-prompt") {- match read_body(&mut req) {- Ok(body) => match hpost_with("/ai-threads/run-prompt", body, 120_000) {- Ok((code, v)) => (code, v),- Err(e) => (502, json!({"error": e, "_hints": {"note": format!("The AI panel did not answer: {}. The phrase was not sent; check your AI threads before sending again.", e)}})),+ // Request bytes up, response bytes back, status preserved. The module+ // reads `delivered_to` as an array; a re-serialisation is a silent+ // behaviour change here, not a formatting detail.+ let mut raw: Vec<u8> = Vec::new();+ let read = req.as_reader().read_to_end(&mut raw);+ let _ = match read {+ Err(e) => req.respond(json_resp(400, json!({"error": format!("read body: {}", e)}))),+ Ok(_) => match hraw("/ai-threads/run-prompt", Some(raw), 120_000) {+ Ok((code, bytes, ct)) => req.respond(raw_resp(code, bytes, &ct)),+ Err(e) => req.respond(json_resp(502, json!({"error": e, "_hints": {"note": format!("The AI panel did not answer: {}. The phrase was not sent; check your AI threads before sending again.", e)}}))), },- Err(e) => (400, json!({"error": e})),- }+ };+ return; } else if method == Method::Get && path.contains("/audio/") { let name = path.rsplit('/').next().unwrap_or(""); if let Some((_, bytes)) = DEMO_AUDIO.iter().find(|(n, _)| *n == name) {
src/app/vendor/newbie-panel.js+31−2@@ -139,6 +139,35 @@ return safe.replace(re, '<b>$1</b>'); } + /* ---- copy, with a fallback that does not need the async clipboard ------+ * navigator.clipboard.writeText needs a secure context AND, in several embedded webviews,+ * transient user activation plus a clipboard-write permission that is simply absent. In+ * ah's webview it rejects with NotAllowedError, so the ONLY path the panel had would fail+ * and the user got "Could not reach the clipboard" on a button that looks like it works.+ * The execCommand path is deprecated but synchronous, needs no permission, and works in+ * exactly the contexts the async API refuses. Try the modern one, fall back, and only then+ * report failure. */+ function copyText(text) {+ return new Promise(function (resolve, reject) {+ function legacy() {+ try {+ var ta = document.createElement('textarea');+ ta.value = text;+ ta.setAttribute('readonly', '');+ ta.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0';+ document.body.appendChild(ta);+ ta.select(); ta.setSelectionRange(0, ta.value.length);+ var ok = document.execCommand && document.execCommand('copy');+ ta.remove();+ ok ? resolve('legacy') : reject(new Error('execCommand refused'));+ } catch (e) { reject(e); }+ }+ if (navigator.clipboard && navigator.clipboard.writeText) {+ navigator.clipboard.writeText(text).then(function () { resolve('async'); }, legacy);+ } else legacy();+ });+ }+ /* ---- toasts ---------------------------------------------------------- */ var toastHost = null; function toast(msg, kind, ms) {@@ -275,7 +304,7 @@ tipFor(cp, 'Copy this phrase'); cp.addEventListener('click', function (e) { e.stopPropagation();- (navigator.clipboard ? navigator.clipboard.writeText(phrase) : Promise.reject())+ copyText(phrase) .then(function () { toast('Copied. Paste it to your AI.', 'ok'); }) .catch(function () { toast('Could not reach the clipboard. Select the phrase and copy it.', 'err'); }); });@@ -485,6 +514,6 @@ return { ok: ok, rows: rows }; } - root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '2.6.0',+ root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText, version: '2.8.0', TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);