← Commit history

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

John Lauer ·4f68e74a1c ·1mo ago ·parent 79bcef7
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"-    }   ]-}\ 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.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" }\ No newline at end of file
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);