← 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

John Lauer ·be6ccc5a6d ·1mo ago ·parent efd5463
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>&nbsp;<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');