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
board view: serve the interactive adom-layout-viewer/adom-schematic-viewer embeds (per-version, viewers/<version>/), and fix /board-svg/ slug parsing for owner-qualified slugs
2 files changed
+89−12
src/main.rs+51−8@@ -357,12 +357,25 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { } (Method::Get, p) if p.starts_with("/board-svg/") => { // forms: /board-svg/<slug>/pcb or /board-svg/<slug>/sch/<idx> [?v=<version>]+ //+ // The slug itself CONTAINS a '/' when it is owner-qualified (`adom/usb3-…`,+ // which is every org board), so parse the view selector off the END. Splitting+ // from the front took `adom` as the whole slug and 404'd both panes. let (path, ver) = split_ver(url);- let segs: Vec<&str> = path["/board-svg/".len()..].splitn(3, '/').collect();- let slug = segs.first().copied().unwrap_or("");- let sub = |dir: &str| match segs.get(1).copied() {- Some("pcb") => Some(format!("{dir}pcb.svg")),- Some("sch") => segs.get(2).and_then(|i| i.parse::<usize>().ok()).map(|i| format!("{dir}sheet-{i:03}.svg")),+ let rest = &path["/board-svg/".len()..];+ let (slug, kind, sch_idx): (&str, &str, usize) = if let Some(s) = rest.strip_suffix("/pcb") {+ (s, "pcb", 0)+ } else if let Some((head, i)) = rest.rsplit_once('/') {+ match (head.strip_suffix("/sch"), i.parse::<usize>().ok()) {+ (Some(s), Some(n)) => (s, "sch", n),+ _ => ("", "", 0),+ }+ } else {+ ("", "", 0)+ };+ let sub = |dir: &str| match kind {+ "pcb" => Some(format!("{dir}pcb.svg")),+ "sch" => Some(format!("{dir}sheet-{sch_idx:03}.svg")), _ => None, }; // a selected PAST version → serve from its render/<version>/ archive;@@ -370,9 +383,9 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { let svg = ver.as_ref() .and_then(|v| sub(&format!("render/{v}/"))) .and_then(|f| wiki_bytes(slug, &f)).and_then(|b| String::from_utf8(b).ok())- .or_else(|| cached_board(slug).and_then(|(sheets, pcb, _)| match segs.get(1).copied() {- Some("pcb") => pcb,- Some("sch") => segs.get(2).and_then(|i| i.parse::<usize>().ok()).and_then(|i| sheets.get(i).map(|(_, s)| s.clone())),+ .or_else(|| cached_board(slug).and_then(|(sheets, pcb, _)| match kind {+ "pcb" => pcb,+ "sch" => sheets.get(sch_idx).map(|(_, s)| s.clone()), _ => None, })) .or_else(|| sub("render/").and_then(|f| wiki_bytes(slug, &f)).and_then(|b| String::from_utf8(b).ok()));@@ -381,6 +394,33 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { None => Response::from_string("not rendered").with_status_code(404), } }+ (Method::Get, p) if p.starts_with("/board-viewer/") => {+ // forms: /board-viewer/<slug>/<name> [?v=<version>]+ // Serves the self-contained INTERACTIVE viewer HTML (adom-layout-viewer /+ // adom-schematic-viewer `embed` output): real net highlighting, layer+ // toggles incl. inner copper, component hover. These are archived per board+ // version under viewers/<version>/, mirroring render/<version>/.+ //+ // With ?v= we deliberately do NOT fall back to the current viewer: a board+ // version that predates the viewer archive must show its own static SVG+ // rather than silently render a DIFFERENT revision of the board.+ let (path, ver) = split_ver(url);+ let rest = &path["/board-viewer/".len()..];+ let (slug, name) = match rest.rsplit_once('/') { Some(x) => x, None => ("", "") };+ let safe = !name.is_empty()+ && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');+ let html = if !safe || slug.is_empty() {+ None+ } else if let Some(v) = ver.as_ref() {+ wiki_bytes(slug, &format!("viewers/{v}/{name}.html"))+ } else {+ wiki_bytes(slug, &format!("viewers/{name}.html"))+ };+ match html {+ Some(b) => Response::from_data(b).with_header(hdr("text/html; charset=utf-8")),+ None => Response::from_string("no viewer").with_status_code(404),+ }+ } (Method::Get, p) if p.starts_with("/board-glb/") => { let (path, ver) = split_ver(url); let slug = &path["/board-glb/".len()..];@@ -1925,6 +1965,9 @@ fn board_media(slug: &str) -> Value { "has_pcb": m.get("pcb").and_then(|b| b.as_bool()).unwrap_or(false), // version archive (empty on legacy flat manifests → the UI shows single-current) "versions": m.get("versions").cloned().unwrap_or_else(|| json!([])),+ // interactive viewer exports available for the CURRENT version (e.g.+ // ["layout","sch-usb",…]). Per-version availability rides on versions[].viewers.+ "viewers": m.get("viewers").cloned().unwrap_or_else(|| json!([])), "current": m.get("current").cloned().unwrap_or(Value::Null)}); } // no stored render/ folder from here on — the board live-renders instead.
src/manage.html+38−4@@ -206,6 +206,8 @@ .bo-viewbody{height:260px;background:#161b22;border:1px solid var(--edge);border-radius:8px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;touch-action:none} .bo-viewbody svg{width:100%;height:100%;display:block} .bo-na{color:#8b93a3;font-size:13px;text-align:center;padding:20px;line-height:1.5}+ /* self-contained interactive board/schematic viewer (adom-layout-viewer embed) */+ .bo-viewer{width:100%;height:100%;border:0;display:block;background:#0d1117} /* In-viewer loading spinner (3D / schematic / PCB): Adom mark + percent below */ .vload{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;width:100%;gap:10px;background:#0d0f14} .vload.ov{position:absolute;inset:0;z-index:5}@@ -722,19 +724,51 @@ async function mountBoardAssets(slug){ } else d3.innerHTML=`<div class="bo-na">board 3D file not available</div>`; } const sheets=m.sheets||[],sel=document.getElementById('bo-sheet');+ // Interactive viewers available for the version being shown. On a PAST version this+ // is that version's own archived list, so a board revision without archived viewers+ // falls back to its own static SVG instead of showing a newer revision.+ const vlist=viewersFor(m);+ const schViewer=(i)=>{ const n='sch-'+String(sheets[i]||'').toLowerCase(); return vlist.includes(n)?n:null; }; if(sch){ if(sheets.length){+ const showSheet=async(i)=>{+ if(boardSlug!==slug)return;+ const vn=schViewer(i);+ if(vn) return mountViewer(sch, `board-viewer/${slug}/${vn}${verQ()}`);+ const p=showViewerLoader(sch);+ mountSvg(sch, await fetchTextProgress(`board-svg/${slug}/sch/${i}${verQ()}`,p), schOpt);+ }; if(sel){ sel.innerHTML=sheets.map((n,i)=>`<option value="${i}">${n}</option>`).join(''); sel.style.display=sheets.length>1?'':'none';- sel.onchange=async()=>{ if(boardSlug!==slug)return; const p=showViewerLoader(sch); mountSvg(sch, await fetchTextProgress(`board-svg/${slug}/sch/${sel.value}${verQ()}`,p), schOpt); };+ sel.onchange=()=>showSheet(sel.value); }- const p=showViewerLoader(sch);- mountSvg(sch, await fetchTextProgress(`board-svg/${slug}/sch/0${q}`,p), schOpt);+ await showSheet(0); } else { if(sel)sel.style.display='none'; sch.innerHTML='<div class="bo-na">schematic view not available</div>'; } } if(boardSlug!==slug)return;- if(pcb){ if(m.has_pcb){ const p=showViewerLoader(pcb); mountSvg(pcb, await fetchTextProgress(`board-svg/${slug}/pcb${q}`,p), pcbOpt); } else pcb.innerHTML='<div class="bo-na">PCB view not available</div>'; }+ if(pcb){+ if(vlist.includes('layout')) mountViewer(pcb, `board-viewer/${slug}/layout${q}`);+ else if(m.has_pcb){ const p=showViewerLoader(pcb); mountSvg(pcb, await fetchTextProgress(`board-svg/${slug}/pcb${q}`,p), pcbOpt); }+ else pcb.innerHTML='<div class="bo-na">PCB view not available</div>';+ }+}+// Which interactive viewers apply to the version currently selected.+function viewersFor(m){+ if(!boardVersion) return m.viewers||[];+ const ent=(m.versions||[]).find(v=>v.version===boardVersion);+ return (ent&&ent.viewers)||[];+}+// Mount a self-contained interactive viewer (iframe). It ships its own chrome+// (layers/nets panel, hover cards), so no mountSvg post-processing applies.+function mountViewer(el, src){+ el.style.position='relative'; el.style.background='#0d1117';+ el.innerHTML=`<iframe class="bo-viewer" src="${src}" title="interactive board view"></iframe>`++ `<div class="vload ov"><img src="loader.gif" alt=""></div>`;+ const fr=el.querySelector('iframe'), ov=el.querySelector('.vload.ov');+ const done=()=>{ if(ov&&ov.parentNode) ov.remove(); };+ fr.addEventListener('load',done);+ setTimeout(done,15000); // never strand the spinner if load never fires } function selectVersion(v){ const m=curBoardMedia||{};