← Commit history

v2.0.15: Module 2.4.0: the panel gutter check now measures against the containing block

John Lauer ·a27ded107d ·29d ago ·parent 4937487
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.sh

  
package.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"-    }   ]-}\ No newline at end of file+}
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" }\ No newline at end of file
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