← Commit history

v0.14.0: wire the Send-to-AI pass-through the module needs (/updater/status and /ai-threads/run-prompt forwarded to ah's control API, body returned BYTE FOR BYTE so the module can read delivered_to and separate delivered from opened-but-unconfirmed; control URL read from ~/.adom/hydrogen-control-url since the port is dynamic, 503 with a hint when absent rather than 404); newbie-panel module 2.8.0 (sha256 c04b3d5) for the synchronous clipboard fallback

John Lauer ·35ea651126 ·1mo ago ·parent d4bfb50
7 files changed +92−6
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-project-manager"-version = "0.13.4"+version = "0.14.0" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-project-manager"-version = "0.13.4"+version = "0.14.0" edition = "2021" description = "Adom Project Manager — the central hub for navigating wiki boards, molecules, and libraries into Hydrogen: browse your wiki boards/projects, view symbol + footprint + 3D and the footprint layer stackup, add components to your KiCad/Altium libraries, and deliver self-contained KiCad projects. Built on adom-lbr." 
bin/adom-project-manager
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-project-manager",-  "version": "0.13.4",+  "version": "0.14.0",   "type": "app",   "description": "Adom Project Manager — the central hub for navigating wiki boards, molecules and libraries into Hydrogen: browse your wiki boards/projects (Personal / Organization / Public), view symbol + footprint + 3D and the per-EDA footprint layer stackup, explore the board and schematic in INTERACTIVE cross-probing viewers (click a net, hover a part for stock and price), add components to your KiCad/Altium libraries, and deliver self-contained KiCad projects. Built on adom-lbr. (Formerly Adom Project Manager / adom-project-manager.)",   "dependencies": {
src/main.rs+57
@@ -351,6 +351,55 @@ fn api_status() -> Value {     json!({"state": "ok", "version": env!("APP_VERSION"), "boards_cached": boards, "pills": pills}) } ++/// ah's live control URL, as ah stamps it into the workspace. The port is dynamic+/// per launch, so this file is the only honest source; never hardcode a port.+fn hydrogen_control_url() -> Option<String> {+    let p = home_container_path(".adom/hydrogen-control-url");+    let t = std::fs::read_to_string(p).ok()?;+    let t = t.trim().trim_end_matches('/').to_string();+    if t.is_empty() { None } else { Some(t) }+}++/// Forward one request to ah's control API and return its response UNCHANGED —+/// same status, same bytes. See the route comments for why the body must not be+/// reshaped.+fn hydrogen_passthrough(method: &str, path: &str, body: &str) -> Response<Cursor<Vec<u8>>> {+    let base = match hydrogen_control_url() {+        Some(b) => b,+        None => {+            return Response::from_string(+                json!({"error":"no hydrogen control url",+                       "hint":"~/.adom/hydrogen-control-url is missing; this dashboard is not running inside an ah workspace"}).to_string())+                .with_status_code(503).with_header(hdr("application/json"));+        }+    };+    let url = format!("{base}{path}");+    let mut args: Vec<String> = vec!["-s".into(), "-o".into(), "-".into(),+        "-w".into(), "\n%{http_code}".into(), "--max-time".into(), "125".into(),+        "-X".into(), method.into()];+    if method == "POST" {+        args.push("-H".into()); args.push("Content-Type: application/json".into());+        args.push("--data-binary".into()); args.push(body.to_string());+    }+    args.push(url);+    let out = match Command::new("curl").args(&args).output() {+        Ok(o) => o,+        Err(e) => {+            return Response::from_string(json!({"error": format!("control api unreachable: {e}")}).to_string())+                .with_status_code(502).with_header(hdr("application/json"));+        }+    };+    let raw = String::from_utf8_lossy(&out.stdout).to_string();+    let (payload, code) = match raw.rfind('\n') {+        Some(i) => (raw[..i].to_string(), raw[i + 1..].trim().parse::<u16>().unwrap_or(502)),+        None => (raw.clone(), 502),+    };+    Response::from_string(payload)+        .with_status_code(code)+        .with_header(hdr("application/json"))+}+ fn print_serve_log_tail() {     let log = std::fs::read_to_string(home_container_path(".adom/project-manager-serve.log")).unwrap_or_default();     let lines: Vec<&str> = log.lines().collect();@@ -711,6 +760,14 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> {         // The SHARED newbie-panel module, served verbatim next to the dashboard.         // Never reformat it: it builds its regexes with RegExp() precisely because a         // page assembled as one string turns \b into a backspace.+        // ── AI-threads pass-through, for the newbie panel module ─────────────+        // The module fetches these SAME-ORIGIN against us, so we forward them to+        // ah's control API and return the response body BYTE FOR BYTE. Reshaping+        // the JSON would be actively harmful: the module reads delivered_to itself+        // to separate "delivered" from "opened but unconfirmed", and an app that+        // collapses those two states makes its own retry double-send.+        (Method::Get, "/updater/status") => hydrogen_passthrough("GET", "/updater/status", ""),+        (Method::Post, "/ai-threads/run-prompt") => hydrogen_passthrough("POST", "/ai-threads/run-prompt", body),         (Method::Get, "/newbie-panel.js") => Response::from_string(NEWBIE_PANEL_JS)             .with_header(hdr("application/javascript; charset=utf-8"))             .with_header(Header::from_bytes(&b"Cache-Control"[..], &b"no-store, must-revalidate"[..]).unwrap()),
src/manage.html+1−1
@@ -742,7 +742,7 @@ function nbInit(){    // Set LAST, and record what failed, so a diagnosis never has to be inferred   // from which regions happen to be painted.-  window.__pmBoot={complete:true,failed:fails,version:'0.13.4',+  window.__pmBoot={complete:true,failed:fails,version:'0.14.0',                    panelMounted:!!window.__pmPanelMounted,ledTimer:!!window.__pmLedTimer};   if(fails.length) toast('Part of this dashboard failed to start: '+fails.join('; '),'error'); }
src/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);