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.15: Module 2.4.0: the panel gutter check now measures against the containing block
8 files changed
+53−105
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "2.0.14"+version = "2.0.15" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "2.0.14"+version = "2.0.15" 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.14+2.0.15
install.shpackage.json+2−41@@ -1,6 +1,6 @@ { "slug": "adom-parts-search",- "version": "2.0.15",+ "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.15",+ "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+39−11@@ -49,6 +49,17 @@ const FONTS: &[(&str, &[u8])] = &[ ]; const CONSOLE_MAX: usize = 500; const STATUS_CACHE_MS: u128 = 5000;+/// HOW OFTEN THIS APP MAY ASK THE VENDOR BACKENDS IF THEY ARE UP.+/// The Hydrogen dock bar polls /api/status every 15s for as long as the app+/// runs, and each status used to re-probe all three backends every 5s. On+/// 2026-09-21 that was ~4,400 health checks a day on EACH backend from a+/// single running copy, the largest source of traffic the services saw, and+/// it grows linearly with every user who leaves Parts Search running. A status+/// light does not need 5s freshness: healthy readings are reused for 60s. A+/// reading with anything wrong is retried after 15s so a recovery still shows+/// quickly. A search that fails reports its own error either way.+const PROBE_TTL_OK_MS: u128 = 60_000;+const PROBE_TTL_BAD_MS: u128 = 15_000; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); const APP_TAG: &str = "app:adom/adom-parts-search"; fn hydrogen_ctl() -> String {@@ -78,6 +89,8 @@ struct AppState { toasts: Mutex<VecDeque<Value>>, clicks: Mutex<VecDeque<Value>>, status_cache: Mutex<Option<(Instant, Value)>>,+ /// Vendor /health readings, kept longer than the status itself. See PROBE_TTL_OK_MS.+ probe_cache: Mutex<Option<(Instant, Vec<Value>)>>, status_refreshing: std::sync::atomic::AtomicBool, pending_eval: Mutex<Option<(String, String)>>, eval_results: Mutex<std::collections::HashMap<String, Value>>,@@ -144,7 +157,9 @@ fn probe_backend(label: &str, url: &str, enabled: bool) -> Value { } let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(2500)).build(); let t0 = Instant::now();- match agent.get(&format!("{}/health", url.trim_end_matches('/'))).call() {+ // Labelled, so the backends' usage stats can tell a status light from a person.+ match agent.get(&format!("{}/health", url.trim_end_matches('/')))+ .set("X-Adom-Caller", "parts-search-status").call() { Ok(r) => { let ms = t0.elapsed().as_millis() as u64; let v: Value = r.into_json().unwrap_or(json!({}));@@ -169,16 +184,27 @@ fn probe_backend(label: &str, url: &str, enabled: bool) -> Value { fn build_status(state: &AppState) -> Value { let s = state.settings.lock().unwrap().clone(); let (m, d, j) = backends(state);- let (tx, rx) = std::sync::mpsc::channel();- for (label, url, on) in [("Mouser", m.clone(), s.vendor_mouser), ("DigiKey", d.clone(), s.vendor_digikey), ("JLCPCB", j.clone(), s.vendor_jlcpcb)] {- let tx = tx.clone();- std::thread::spawn(move || { let _ = tx.send((label, probe_backend(label, &url, on))); });- }- drop(tx);- let mut leds: Vec<Value> = vec![];- let mut got: Vec<(&str, Value)> = rx.iter().collect();- got.sort_by_key(|(l, _)| match *l { "Mouser" => 0, "DigiKey" => 1, _ => 2 });- for (_, v) in got { leds.push(v); }+ let cached = state.probe_cache.lock().unwrap().clone();+ let reuse = cached.filter(|(at, v)| {+ let healthy = v.iter().all(|l| l.get("state").and_then(|x| x.as_str()) == Some("ok"));+ at.elapsed().as_millis() < if healthy { PROBE_TTL_OK_MS } else { PROBE_TTL_BAD_MS }+ });+ let mut leds: Vec<Value> = match reuse {+ Some((_, v)) => v,+ None => {+ let (tx, rx) = std::sync::mpsc::channel();+ for (label, url, on) in [("Mouser", m.clone(), s.vendor_mouser), ("DigiKey", d.clone(), s.vendor_digikey), ("JLCPCB", j.clone(), s.vendor_jlcpcb)] {+ let tx = tx.clone();+ std::thread::spawn(move || { let _ = tx.send((label, probe_backend(label, &url, on))); });+ }+ drop(tx);+ let mut got: Vec<(&str, Value)> = rx.iter().collect();+ got.sort_by_key(|(l, _)| match *l { "Mouser" => 0, "DigiKey" => 1, _ => 2 });+ let v: Vec<Value> = got.into_iter().map(|(_, v)| v).collect();+ *state.probe_cache.lock().unwrap() = Some((Instant::now(), v.clone()));+ v+ }+ }; let searching = state.searching.lock().unwrap().clone(); leds.push(json!({"label": "Search", "state": "ok", "short": match &searching { Some(_) => "searching".to_string(), None => "idle".to_string() },@@ -470,6 +496,7 @@ pub fn run(args: AppArgs) -> Result<(), String> { toasts: Mutex::new(VecDeque::new()), clicks: Mutex::new(VecDeque::new()), status_cache: Mutex::new(None),+ probe_cache: Mutex::new(None), status_refreshing: std::sync::atomic::AtomicBool::new(false), pending_eval: Mutex::new(None), eval_results: Mutex::new(std::collections::HashMap::new()),@@ -650,6 +677,7 @@ fn handle(mut req: Request, state: Arc<AppState>, no_tab: bool, tab_name: &str) Ok(()) => { *state.settings.lock().unwrap() = s.clone(); *state.status_cache.lock().unwrap() = None;+ *state.probe_cache.lock().unwrap() = None; (200, json!({"ok": true, "changed": changed, "settings": settings::as_json(&s)})) } Err(e) => (500, json!({"ok": false, "error": e})),
uninstall.sh