app
Adom Project Manager
Public Made by Adomby adom
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 Manag
← 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
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);