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.12.1: newbie-panel breakpoint 760->620 (his dock pane measures 744px, inside the 744-853 band, so two-column never rendered where it was designed to) with the left column now minmax(180px,220px); dash-header horizontal padding 14->16px; status-led pill row wired, server-emitted short state with prose on tap and a next step on red, and gray 'checking' for not-yet-probed so a fresh server never shows a red pill telling a signed-in user to re-auth
6 files changed
+117−14
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-project-manager"-version = "0.12.0"+version = "0.12.1" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-project-manager"-version = "0.12.0"+version = "0.12.1" 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.12.0",+ "version": "0.12.1", "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−8@@ -295,6 +295,62 @@ fn serve_cmd(port_flag: Option<u16>) -> i32 { 1 } ++/// Dock health + the status-led pill row. Answers in MILLISECONDS: it reads only+/// what is ALREADY cached and never starts a wiki fetch or a bridge probe, because+/// the dock polls this every few seconds. Each pill carries a SHORT `state` for the+/// pill itself plus the full prose in `detail` — the page must never slice prose to+/// make a pill, which is what produced mid-word cuts like "in talled and pinned".+/// A `bad` pill always carries its `next` step.+fn api_status() -> Value {+ let boards = LIST_CACHE.lock().unwrap().is_some();+ let machine = MACHINE.lock().unwrap().clone();+ // None = never probed yet (the caches fill on the first board-list fetch), which+ // is NOT the same as "broken" and must never render red with a corrective step.+ let whoami_probed = WHOAMI.lock().unwrap().clone();+ let mut pills = vec![json!({+ "id": "dashboard", "label": "Dashboard",+ "state": "serving", "status": "ok",+ "detail": format!("Adom Project Manager v{} is serving this page.", env!("APP_VERSION"))+ })];+ pills.push(match &machine {+ // The container's own /root is the fallback we synthesize when no shell+ // probe has resolved — it means the bridge is NOT answering, not that the+ // user is on Linux in /root.+ Some((os, home)) if !(os == "linux" && home == "/root") => json!({+ "id": "bridge", "label": "Bridge", "state": "answering", "status": "ok",+ "detail": format!("Your machine answered: {os}, home {home}. Add to Library and Add Project deliver here.")+ }),+ Some(_) => json!({+ "id": "bridge", "label": "Bridge", "state": "not answering", "status": "bad",+ "detail": "The desktop bridge did not answer, so Add to Library and Add Project cannot deliver.",+ "next": "Start Adom Bridge on your machine, then press \u{21bb} to re-probe."+ }),+ None => json!({+ "id": "bridge", "label": "Bridge", "state": "checking", "status": "unknown",+ "detail": "Still working out which machine answers; this resolves on the first delivery action."+ }),+ });+ pills.push(match &whoami_probed {+ None => json!({"id": "wiki", "label": "Wiki", "state": "checking", "status": "unknown",+ "detail": "Reading your wiki identity; this resolves as soon as the board list loads."}),+ Some((me, _)) if me.is_empty() => json!({"id": "wiki", "label": "Wiki", "state": "signed out", "status": "bad",+ "detail": "No signed-in wiki session, so the Personal and Organization tabs stay empty.",+ "next": "Run `adom-wiki auth` in this workspace, then reload."}),+ Some((me, orgs)) => json!({"id": "wiki", "label": "Wiki", "state": format!("as {me}"), "status": "ok",+ "detail": format!("Signed in to wiki.adom.inc as {me}{}.",+ if orgs.is_empty() { String::new() } else { format!(", orgs: {}", orgs.join(", ")) })}),+ });+ pills.push(if boards {+ json!({"id": "boards", "label": "Boards", "state": "loaded", "status": "ok",+ "detail": "The board list is cached, so opening a project is instant."})+ } else {+ json!({"id": "boards", "label": "Boards", "state": "loading", "status": "busy",+ "detail": "Fetching the board list from wiki.adom.inc (the first fetch takes a few seconds)."})+ });+ json!({"state": "ok", "version": env!("APP_VERSION"), "boards_cached": boards, "pills": pills})+}+ 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();@@ -657,14 +713,7 @@ fn route(method: &Method, url: &str, body: &str) -> Response<Cursor<Vec<u8>>> { .with_header(Header::from_bytes(&b"Cache-Control"[..], &b"public, max-age=86400"[..]).unwrap()), // Dock health: answered instantly from in-process state, no wiki calls. The // dock polls this every few seconds, so it must never block.- (Method::Get, "/api/status") => json_resp(- &json!({- "state": "ok",- "version": env!("APP_VERSION"),- "boards_cached": LIST_CACHE.lock().unwrap().is_some(),- })- .to_string(),- ),+ (Method::Get, "/api/status") => json_resp(&api_status().to_string()), (Method::Get, "/pm/version") => Response::from_string( format!("{{\"version\":\"{}\"}}", env!("APP_VERSION"))).with_header(hdr("application/json")), // Update check: compares the running binary's version against the latest
src/manage.html+57−3@@ -12,7 +12,7 @@ body{margin:0;font:14px/1.5 'Satoshi',-apple-system,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--ink);height:100vh;display:flex;flex-direction:column} /* canonical Adom 44px three-zone app header (brand · subject · actions) */ header.app{height:44px;flex:0 0 44px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;- gap:12px;padding:0 14px;background:var(--panel);border-bottom:1px solid var(--edge);position:relative;z-index:30}+ gap:12px;padding:0 16px;background:var(--panel);border-bottom:1px solid var(--edge);position:relative;z-index:30} header.app .brand{display:flex;align-items:center;gap:8px;min-width:0;position:relative;cursor:default} header.app .brand svg{width:16px;height:16px;color:var(--ink);flex:0 0 16px} header.app .app-name{font-family:'Familjen Grotesk',-apple-system,sans-serif;font-weight:600;font-size:13px;color:var(--acc);white-space:nowrap}@@ -482,9 +482,9 @@ #newbie .nb-x{font-size:13.5px;color:var(--muted);max-width:70ch;margin:6px 0 14px} #newbie .nb-x b{color:var(--teal);font-weight:600} #newbie .nb-close{position:absolute;top:14px;right:14px}- #newbie .nb-cols{display:grid;grid-template-columns:220px 1fr;gap:24px;align-items:start}+ #newbie .nb-cols{display:grid;grid-template-columns:minmax(180px,220px) 1fr;gap:24px;align-items:start} #newbie .nb-right{border-left:1px solid var(--line);padding-left:24px}- @media (max-width:760px){+ @media (max-width:620px){ #newbie .nb-cols{grid-template-columns:1fr} #newbie .nb-right{border-left:none;border-top:1px solid var(--line);padding-left:0;padding-top:18px} }@@ -512,6 +512,24 @@ #tip{position:fixed;z-index:900;background:var(--overlay);color:var(--text);border:1px solid var(--line); border-radius:6px;padding:5px 9px;font-size:12px;pointer-events:none;opacity:0;transition:opacity .1s;white-space:nowrap} #tip.on{opacity:1}+ /* status-leds: ONE compact pill row. The SERVER emits each pill's short state;+ this page never slices prose to make one. Prose appears on tap only, and a red+ pill always carries its next step. */+ :root{--led-green:var(--green);--led-amber:var(--yellow);--led-red:var(--red)}+ #leds{display:flex;flex-wrap:wrap;gap:8px;margin:0 22px 16px}+ #leds:empty{display:none}+ .led-pill{display:inline-flex;align-items:center;gap:7px;background:var(--surface);+ border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:12.5px;cursor:pointer;color:var(--text)}+ .led-pill:hover{border-color:var(--teal)}+ .led-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--muted);flex:0 0 8px}+ .led-pill[data-state=ok] .dot{background:var(--led-green)}+ .led-pill[data-state=busy] .dot{background:var(--led-amber)}+ .led-pill[data-state=bad] .dot{background:var(--led-red)}+ .led-pill .lbl{color:var(--muted)}+ .led-detail{margin:0 22px 16px;background:var(--surface);border:1px solid var(--line);+ border-left:3px solid var(--teal);border-radius:10px;padding:12px 14px;font-size:13px;color:var(--text);max-width:70ch}+ .led-detail[hidden]{display:none}+ .led-detail .nx{display:block;margin-top:7px;color:var(--muted)} </style> </head> <body>@@ -600,6 +618,9 @@ </div> </section> + <div id="leds" role="status" aria-label="System status"></div>+ <div class="led-detail" id="led-detail" hidden></div>+ <div id="workspace"> <!-- HOME: full-screen gallery of boards/molecules/projects/circuits. The left list only appears once you click into a project (the interior below). -->@@ -691,6 +712,38 @@ function setSubjectMeta(txt){ const m=document.getElementById('hdr-meta'); if(m& document.addEventListener('click',hide,true); })(); +// ══ status-leds ══════════════════════════════════════════════════════════════+// Pills come from GET /api/status, which answers from cache in milliseconds and+// emits its OWN short state per pill. Tapping one reveals the prose; a bad pill+// shows its next step.+async function ledsRefresh(){+ let d; try{ d=await fetch('api/status').then(r=>r.json()); }catch(e){ return; }+ const row=document.getElementById('leds'), det=document.getElementById('led-detail');+ if(!row||!Array.isArray(d.pills)) return;+ const openId=row.getAttribute('data-open');+ row.innerHTML='';+ d.pills.forEach(p=>{+ const b=document.createElement('button');+ b.className='led-pill'; b.setAttribute('data-state',p.status||'ok');+ b.innerHTML='<span class="dot"></span><span class="lbl"></span> <span class="st"></span>';+ b.querySelector('.lbl').textContent=p.label;+ b.querySelector('.st').textContent=p.state; // the SERVER's short state, never sliced here+ b.setAttribute('aria-label',p.label+': '+p.state);+ b.onclick=()=>{+ const same=row.getAttribute('data-open')===p.id;+ if(same){ row.removeAttribute('data-open'); det.hidden=true; return; }+ row.setAttribute('data-open',p.id);+ det.textContent=p.detail||'';+ if(p.next){ const n=document.createElement('span'); n.className='nx'; n.textContent='Next: '+p.next; det.appendChild(n); }+ det.hidden=false;+ };+ row.appendChild(b);+ if(openId===p.id){ det.textContent=p.detail||'';+ if(p.next){ const n=document.createElement('span'); n.className='nx'; n.textContent='Next: '+p.next; det.appendChild(n); }+ det.hidden=false; row.setAttribute('data-open',p.id); }+ });+}+ // ══ newbie-panel + dash-header controls (adom-ui-design 1.22.1) ═══════════════ // The phrases are dockbar.json's say[] — the same nouns the dock card teaches. const SAY_PHRASES=[@@ -748,6 +801,7 @@ function nbRender(){ } function nbInit(){ nbRender();+ ledsRefresh(); setInterval(ledsRefresh,15000); let saved=null; try{ saved=localStorage.getItem(NB_KEY); }catch(e){} nbSetOpen(saved!=='closed'); // fresh dashboards start expanded const close=document.getElementById('nb-close'), tog=document.getElementById('nb-toggle');