← 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

barrett-land ·f53d70f674 ·3mo ago ·parent b0f2886
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());