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.3.1: authenticate wiki fetches so private/org component pages list (fixes #120); find + pre-fill the footprint library too (with base-part match, e.g. BMA580→Bosch_BLT); in-library tag moved below the component name
2 files changed
+113−38
src/main.rs+110−35@@ -190,7 +190,7 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { .unwrap_or_default() .split(',').filter(|s| !s.is_empty()).map(String::from).collect(); let matched: std::collections::HashMap<String, Value> = components_in_libs(&slugs)- .into_iter().map(|(s, by, lib)| (s, json!({"by": by, "lib": lib}))).collect();+ .into_iter().map(|(s, by, lib, fp)| (s, json!({"by": by, "lib": lib, "fpLib": fp}))).collect(); json_resp(&json!(matched).to_string()) } (Method::Get, p) if p.starts_with("/api/suggest-path") => {@@ -264,11 +264,34 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { // Wiki access (via curl — no HTTP-client dep) // --------------------------------------------------------------------------- +/// The wiki API token (same source the adom-wiki CLI uses), cached. Without it,+/// unauthenticated requests return only PUBLIC pages — so private/org component+/// pages would never be listed. Read once; None if unavailable.+static WIKI_TOKEN: Mutex<Option<Option<String>>> = Mutex::new(None);+fn wiki_token() -> Option<String> {+ let mut c = WIKI_TOKEN.lock().unwrap();+ if let Some(v) = c.as_ref() {+ return v.clone();+ }+ let t = std::fs::read_to_string("/var/run/adom/api-key").ok()+ .map(|s| s.trim().to_string())+ .filter(|s| !s.is_empty())+ .or_else(|| std::env::var("ADOM_API_KEY").ok().filter(|s| !s.is_empty()));+ *c = Some(t.clone());+ t+}+ fn curl(url: &str) -> Option<Vec<u8>> {- let out = Command::new("curl")- .args(["-s", "-L", "--max-time", "12", url])- .output()- .ok()?;+ let mut args: Vec<String> = vec!["-s".into(), "-L".into(), "--max-time".into(), "12".into()];+ // authenticate wiki requests so private/org pages are included (issue #120)+ if url.contains("wiki.adom.inc") {+ if let Some(tok) = wiki_token() {+ args.push("-H".into());+ args.push(format!("Authorization: Bearer {tok}"));+ }+ }+ args.push(url.to_string());+ let out = Command::new("curl").args(&args).output().ok()?; if out.status.success() && !out.stdout.is_empty() { Some(out.stdout) } else {@@ -1554,47 +1577,99 @@ fn value_package_sig(sym_name: &str, fp_name: &str) -> Option<(String, String)> if v.len() >= 2 { Some((v, pkg)) } else { None } } -/// Which of the given component slugs are ALREADY in the user's libraries, each-/// with the reason ("mpn" | "symbol" | "value+package"). Matches by MPN (grep, or-/// a library symbol name that PREFIXES the MPN — e.g. "BMA580" for "BMA580-561000",-/// which covers stub components with no symbol/footprint on the wiki), else exact-/// symbol name, else value + package. Reused by the delivery and the live preview.-fn components_in_libs(slugs: &[String]) -> Vec<(String, String, String)> {+/// Footprint name (normalized) → the user's `.pretty` library that contains it.+fn footprint_index(dirs: &[String]) -> std::collections::HashMap<String, String> {+ let mut idx = std::collections::HashMap::new();+ let dirs: Vec<&String> = dirs.iter().filter(|d| !d.is_empty()).collect();+ if dirs.is_empty() {+ return idx;+ }+ let (os, _) = machine();+ let out = if os == "windows" {+ let cmds: Vec<String> = dirs.iter().map(|d| format!("dir /b /s \"{}\\*.kicad_mod\" 2>nul", d.replace('"', ""))).collect();+ ad_run(&cmds.join(" & "))+ } else {+ let quoted = dirs.iter().map(|d| sh_quote(d)).collect::<Vec<_>>().join(" ");+ ad_run(&format!("for d in {quoted}; do ls \"$d\"/*.kicad_mod 2>/dev/null; done"))+ };+ if let Some(out) = out {+ for line in out.lines() {+ let p = line.trim();+ if p.is_empty() {+ continue;+ }+ let fpname = p.rsplit(['/', '\\']).next().unwrap_or(p).trim_end_matches(".kicad_mod");+ let lib = parent_dir(p).rsplit(['/', '\\']).next().unwrap_or("").trim_end_matches(".pretty").to_string();+ if !fpname.is_empty() {+ idx.entry(norm_val(fpname)).or_insert_with(|| lib.clone());+ // also index the base part name (before the package suffix) — e.g.+ // "BMA580" from "BMA580_WLCSP-6" — so stub parts match by base-part.+ if let Some(base) = fpname.split('_').next() {+ if base != fpname && base.len() >= 3 {+ idx.entry(norm_val(base)).or_insert_with(|| lib.clone());+ }+ }+ }+ }+ }+ idx+}++/// Which of the given component slugs are ALREADY in the user's libraries, with+/// the reason and the SYMBOL + FOOTPRINT library each is found in — so neither a+/// duplicate symbol nor a duplicate footprint gets added. Symbol match: MPN (grep+/// or a lib symbol name that PREFIXES the MPN, covering stub parts), exact symbol+/// name, else value + package. Footprint match: the `.kicad_mod` is present in a+/// `.pretty`. Returns (slug, reason, symLib, fpLib) — either lib may be empty.+fn components_in_libs(slugs: &[String]) -> Vec<(String, String, String, String)> { let libs = user_libraries();- let sym_files: Vec<String> = libs.get("symLibs").and_then(|a| a.as_array())- .map(|a| a.iter().filter_map(|l| l.get("uri").and_then(|u| u.as_str()).map(String::from)).collect())- .unwrap_or_default();- if sym_files.is_empty() || slugs.is_empty() {+ let uris = |key: &str| -> Vec<String> {+ libs.get(key).and_then(|a| a.as_array())+ .map(|a| a.iter().filter_map(|l| l.get("uri").and_then(|u| u.as_str()).map(String::from)).collect())+ .unwrap_or_default()+ };+ let sym_files = uris("symLibs");+ let fp_dirs = uris("fpLibs");+ if (sym_files.is_empty() && fp_dirs.is_empty()) || slugs.is_empty() { return vec![]; } let mpn_map = mpns_present_in_libs(&sym_files, &slugs.iter().map(|s| up(s)).collect::<Vec<_>>());- // normalized symbol name -> the library it lives in let norm_index: std::collections::HashMap<String, String> = existing_symbol_index(&sym_files).into_iter().map(|(n, l)| (norm_val(&n), l)).collect();+ let fp_index = footprint_index(&fp_dirs); let mut out = vec![]; for slug in slugs { let nmpn = norm_val(&up(slug));- if let Some(lib) = mpn_map.get(&up(slug).to_lowercase()) {- out.push((slug.clone(), "mpn".to_string(), lib.clone()));- continue;- }- if let Some((_, lib)) = norm_index.iter().find(|(n, _)| n.len() >= 5 && nmpn.starts_with(n.as_str())) {- out.push((slug.clone(), "mpn".to_string(), lib.clone()));- continue;- }- if let Ok((part, _)) = load_part(slug, &up(slug)) {- let sn = norm_val(&part.symbol.name);- if sn.len() >= 3 {- if let Some(lib) = norm_index.get(&sn) {- out.push((slug.clone(), "symbol".to_string(), lib.clone()));- continue;- }- }- if let Some((v, p)) = value_package_sig(&part.symbol.name, &part.footprint.name) {- if let Some((_, lib)) = norm_index.iter().find(|(n, _)| n.contains(&v) && n.contains(&p)) {- out.push((slug.clone(), "value+package".to_string(), lib.clone()));+ let part = load_part(slug, &up(slug)).ok().map(|(p, _)| p);+ // symbol library this part already lives in (if any) + why+ let (reason, sym_lib): (&str, String) = if let Some(lib) = mpn_map.get(&up(slug).to_lowercase()) {+ ("mpn", lib.clone())+ } else if let Some((_, lib)) = norm_index.iter().find(|(n, _)| n.len() >= 5 && nmpn.starts_with(n.as_str())) {+ ("mpn", lib.clone())+ } else if let Some(p) = &part {+ let sn = norm_val(&p.symbol.name);+ if sn.len() >= 3 && norm_index.contains_key(&sn) {+ ("symbol", norm_index[&sn].clone())+ } else if let Some((v, pk)) = value_package_sig(&p.symbol.name, &p.footprint.name) {+ match norm_index.iter().find(|(n, _)| n.contains(&v) && n.contains(&pk)) {+ Some((_, lib)) => ("value+package", lib.clone()),+ None => ("", String::new()), }+ } else {+ ("", String::new()) }+ } else {+ ("", String::new())+ };+ // footprint library this part's .kicad_mod already lives in (if any) —+ // by exact footprint name, else a footprint base name that PREFIXES the MPN+ let fp_lib = part.as_ref()+ .and_then(|p| fp_index.get(&norm_val(&p.footprint.name)).cloned())+ .or_else(|| fp_index.iter().find(|(n, _)| n.len() >= 5 && nmpn.starts_with(n.as_str())).map(|(_, l)| l.clone()))+ .unwrap_or_default();+ if !sym_lib.is_empty() || !fp_lib.is_empty() {+ let reason = if sym_lib.is_empty() { "footprint" } else { reason };+ out.push((slug.clone(), reason.to_string(), sym_lib, fp_lib)); } } out
src/manage.html+3−3@@ -65,7 +65,7 @@ .libsel option{background:#262c36;color:var(--ink)} .inlib td{color:#4ade80} .inlib .lib{color:#4ade80}- .inlibtag{color:#4ade80;font-size:10px;font-weight:600;letter-spacing:.03em;white-space:nowrap}+ .inlibtag{display:block;margin-top:2px;color:#4ade80;font-size:10px;font-weight:600;letter-spacing:.03em;white-space:nowrap} .rmrow td{color:#f87171} .rmtag{color:#f87171;font-size:10px;font-weight:600;letter-spacing:.03em;white-space:nowrap} .lib.addcell{color:#4ade80}@@ -583,7 +583,7 @@ function setLibStatus(){ const el=document.getElementById('modal-status'); if(!e else { el.className=''; el.textContent=''; } } function checkInLib(slugs,cb){ inLibLoading=true; libChecked=false; setLibStatus(); if(cb)cb(); fetch('api/in-library?slugs='+encodeURIComponent(slugs.join(','))).then(r=>r.json())- .then(m=>{ inLib=m||{}; curPlan.forEach(p=>{const h=inLib[p.slug]; if(h&&h.lib)p.symLib=h.lib;}); inLibLoading=false; libChecked=true; setLibStatus(); if(cb)cb(); })+ .then(m=>{ inLib=m||{}; curPlan.forEach(p=>{const h=inLib[p.slug]; if(h){if(h.lib)p.symLib=h.lib; if(h.fpLib)p.fpLib=h.fpLib;}}); inLibLoading=false; libChecked=true; setLibStatus(); if(cb)cb(); }) .catch(()=>{ inLibLoading=false; libChecked=true; setLibStatus(); if(cb)cb(); }); } // project-only mode: diff the board against the project library on disk (add/keep/remove) let projDiff={}, projLoading=false, projChecked=false;@@ -646,7 +646,7 @@ function openAddProj(){ const draw=()=>{ const editable=mode==='personal'; curPlan=buildPlan(slugs,mode==='project',projName);- if(editable)curPlan.forEach(p=>{const h=inLib[p.slug]; if(h&&h.lib)p.symLib=h.lib;});+ if(editable)curPlan.forEach(p=>{const h=inLib[p.slug]; if(h){if(h.lib)p.symLib=h.lib; if(h.fpLib)p.fpLib=h.fpLib;}}); document.getElementById('modal-preview').innerHTML=`<div class="note">${mode==='project'?'A self-contained <b>'+projName+'</b> project library is created/updated — <span style="color:#4ade80">green</span> = added, white = already in the library, <span style="color:#f87171">red</span> = removed.':'Pick which of your libraries each symbol + footprint goes into. Parts already in your libraries are marked; keep the library to skip, or change it to add a copy there.'}</div>`+previewTable(curPlan,editable); if(editable)wireLibSels(); modalGo=()=>doApply(curPlan,'kicad',mode==='project',boardSlug,destPath());