app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
2.12.5: Dock Bar dashboard contract adopted (issue #11)
9 files changed
+264−90
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.12.4"+version = "2.12.5" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.12.4"+version = "2.12.5" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.12.4+2.12.5
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.12.4",+ "version": "2.12.5", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {},
page.json+9−12@@ -1,8 +1,8 @@ { "slug": "adom-lbr",- "version": "2.12.4",+ "version": "2.12.5", "type": "app",- "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",+ "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {}, "tags": [ "eda",@@ -52,24 +52,21 @@ ], "files": [ "bin/**",- "assets/**", "SKILL.md",- "skills/**", "README.md", "install.sh", "uninstall.sh",- "screenshots/**",- "dockbar.json"+ "screenshots/**" ],- "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+ "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.", "sample_prompts": [ { "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",- "label": "KiCad → Altium"+ "label": "KiCad \u2192 Altium" }, { "prompt": "Import this Altium footprint into the adom-lbr JSON",- "label": "Altium → JSON"+ "label": "Altium \u2192 JSON" }, { "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +74,15 @@ }, { "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",- "label": "Altium → KiCad"+ "label": "Altium \u2192 KiCad" }, { "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",- "label": "JSON → KiCad"+ "label": "JSON \u2192 KiCad" }, { "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup", "label": "Browse parts" } ]-}+}
skills/adom-lbr-dockbar/SKILL.md+17@@ -88,6 +88,23 @@ diagnosing anything yourself. Every `/api/*` route (and `/health`) sends cross-origin from the Hydrogen shell; without that header the card reads "not answering" while the server is perfectly healthy. +## The dashboard contract (adom-ui-design 1.9.x, adopted 2.12.5)++The page follows the fleet dashboard contract: dash-header grammar ("Adom+LBR - the EDA library converter", version in the brand hover popover never+the bar, a `target:` chip with the bridge LED, ? recalls the newbie+scaffolding, outline cog), the canonical say-strip (heading "Say it",+curly-quoted phrases with the noun in teal, Copy + Send to AI on every+chip), a status-leds pill row (tap a pill for the prose + fix; busy+escalates to "stuck?" past 15 minutes), and newbie-graduation (localStorage+key `adom-lbr:dash-graduation`, namespaced because the cloud proxy serves+every port under one origin; dismissal / 3 green launches / first+AI-driven action fold the scaffolding, the say strip last at 5).+"Send to AI" goes through `POST /api/say-send`: the backend opens a NEW+thread via the panel's AI-threads API (tag `dock:adom-lbr`) and submits -+new threads only, resume stays parked; failure answers {ok:false, why}+and the page falls back to copying.+ ## Say it, and Try it `dockbar.json` declares up to six phrases in the noun lbr; the dock shows them
src/dock.rs+37@@ -164,6 +164,43 @@ fn curl_json(url: &str, body: Option<&Value>, timeout_s: u32) -> Option<Value> { serde_json::from_slice(&out.stdout).ok() } +/// The workspace control API base: ~/.adom/hydrogen-control-url when present+/// (the canonical pointer per the say-strip contract), else the well-known port.+fn control_api_base() -> String {+ std::fs::read_to_string(home().join(".adom/hydrogen-control-url"))+ .map(|s| s.trim().trim_end_matches('/').to_string())+ .ok()+ .filter(|s| !s.is_empty())+ .unwrap_or_else(|| CONTROL_API.to_string())+}++/// "Send to AI" (say-strip contract, John's ruling 2026-08-24): open a NEW AI+/// thread named for the app and tagged dock:adom-lbr, then submit the phrase.+/// NEW THREADS ONLY - resuming or typing into existing threads stays parked+/// (try_phrase below remains dormant behind TRY_PARKED).+pub fn say_send(phrase: &str) -> Value {+ if phrase.trim().is_empty() {+ return json!({"ok": false, "why": "no phrase given"});+ }+ let base = control_api_base();+ let new_body = json!({"name": "Adom LBR — say it", "tags": ["dock:adom-lbr"]});+ let Some(created) = curl_json(&format!("{base}/ai-threads/new"), Some(&new_body), 4) else {+ return json!({"ok": false, "why": "the AI panel's control API is not reachable from this machine"});+ };+ let sid = created.get("sid").and_then(|s| s.as_str())+ .or_else(|| created.get("id").and_then(|s| s.as_str()))+ .map(String::from);+ let Some(sid) = sid else {+ return json!({"ok": false, "why": format!("the panel did not return a thread id ({})", created.get("error").and_then(|e| e.as_str()).unwrap_or("no sid in reply"))});+ };+ let run = json!({"sid": sid, "prompt": phrase, "submit": true});+ match curl_json(&format!("{base}/ai-threads/run-prompt"), Some(&run), 6) {+ Some(r) if r.get("ok").and_then(|o| o.as_bool()) != Some(false) => json!({"ok": true, "sid": sid}),+ Some(r) => json!({"ok": false, "why": r.get("error").and_then(|e| e.as_str()).unwrap_or("run-prompt refused"), "sid": sid}),+ None => json!({"ok": false, "why": "run-prompt did not answer", "sid": sid}),+ }+}+ /// "Try it": stage the phrase, unsubmitted, into this app's own AI thread via /// the Hydrogen control API. Census first (open threads), history second /// (closed ones, resumed first), else tell the page to copy. Reachable from a
src/manage.html+189−74@@ -22,16 +22,30 @@ #panes{display:flex;flex:1;min-height:0} /* ---- the 44px app header: name left, live LEDs + version + the cog right ---- */ #hdr{height:44px;min-height:44px;display:flex;align-items:center;gap:12px;padding:0 10px 0 14px;background:var(--panel);border-bottom:1px solid var(--edge)}- #hdr .brand{display:flex;align-items:center;gap:9px;font-family:var(--brand-heading);font-weight:700;font-size:15px;letter-spacing:.01em}+ #hdr .brand{position:relative;display:flex;align-items:center;gap:9px;font-family:var(--brand-heading);font-weight:700;font-size:15px;letter-spacing:.01em} #hdr .brand img{width:22px;height:22px;display:block}- #hdr .brand small{color:var(--mut);font-weight:400;font-size:12px}- #hdr .leds{display:flex;align-items:center;gap:10px;margin-left:auto;font-size:11px;color:var(--mut)}- .led{display:inline-flex;align-items:center;gap:5px;cursor:default}- .led i{width:8px;height:8px;border-radius:50%;background:#555;display:inline-block;box-shadow:0 0 0 2px #0003}- .led.ok i{background:var(--green);box-shadow:0 0 6px var(--green)}- .led.warn i{background:var(--yellow);box-shadow:0 0 6px var(--yellow)}- .led.fail i{background:var(--red);box-shadow:0 0 6px var(--red)}- #hdr .ver{font:11px ui-monospace,monospace;color:var(--mut);background:#0d0f14;border:1px solid var(--edge);border-radius:6px;padding:1px 6px}+ #hdr .brand .bs{color:var(--mut);font-weight:400;font-size:12px;font-family:var(--brand-body)}+ #brandpop{position:absolute;top:calc(100% + 8px);left:0;z-index:1500;background:var(--elevated);border:1px solid var(--border);border-radius:9px;padding:10px 13px;font:400 12px/1.6 var(--brand-body);color:var(--text-body);box-shadow:0 12px 32px #000a;white-space:nowrap}+ #brandpop a{color:var(--acc)}+ #hdr .chip{display:inline-flex;align-items:center;gap:6px;margin-left:14px;font:11.5px ui-monospace,monospace;color:var(--mut);background:#0d0f14;border:1px solid var(--edge);border-radius:999px;padding:3px 10px}+ #hdr .chip i{width:7px;height:7px;border-radius:50%;background:#555;display:inline-block}+ #hdr .chip.on i{background:var(--green);box-shadow:0 0 5px var(--green)}+ #hdr .chip .tc{color:var(--text-2)}+ #hdr .chip #tname{color:var(--ink)}+ /* status pill row (status-leds contract) */+ .ledrow{display:flex;flex-wrap:wrap;gap:8px;margin:0 18px 12px}+ .led-pill{display:inline-flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--edge);border-radius:999px;padding:4px 12px;font-size:12.5px;cursor:pointer;color:var(--ink)}+ .led-pill .dot{width:8px;height:8px;border-radius:50%;background:#555}+ .led-pill[data-state=ok] .dot{background:var(--green)}+ .led-pill[data-state=busy] .dot{background:var(--yellow)}+ .led-pill[data-state=bad] .dot{background:var(--red)}+ .led-pill .st{color:var(--text-2)}+ #led-pop{position:fixed;z-index:2500;max-width:420px;background:var(--elevated);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:10px;padding:12px 14px;font:400 12.5px/1.6 var(--brand-body);color:var(--text-body);box-shadow:0 16px 40px #000c;display:none}+ #led-pop b{color:var(--text)}+ #led-pop .fix{color:var(--yellow);display:block;margin-top:6px}+ /* graduation: collapsed scaffolding one-liners */+ .foldline{display:none;margin:10px 18px 2px;padding:7px 14px;background:var(--panel);border:1px solid var(--edge);border-radius:999px;font-size:12px;color:var(--text-2);cursor:pointer;width:fit-content}+ .foldline:hover{border-color:var(--acc);color:var(--ink)} .iconbtn{background:none;border:1px solid transparent;color:var(--ink);width:30px;height:30px;border-radius:8px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .12s,border-color .12s,transform .06s} .iconbtn:hover{background:#1e222b;border-color:var(--edge);color:var(--acc)} .iconbtn:active{transform:translateY(1px) scale(.96)}@@ -47,13 +61,19 @@ #say .say-sub{font-size:12.5px;color:var(--text-2);max-width:80ch} #say .say-sub b{color:var(--acc)} .say-list{display:flex;flex-wrap:wrap;gap:9px}- .phrase{display:inline-flex;align-items:stretch;border:1px solid var(--border);border-radius:999px;background:#0d0f14;overflow:hidden;font-size:13px;transition:border-color .15s ease}- .phrase:hover{border-color:var(--accent)}- .phrase .txt{padding:6px 10px 6px 14px;cursor:copy;color:var(--ink);background:none;border:none;font:500 13px var(--brand-body);transition:background .15s}- .phrase .txt:hover{background:var(--overlay)}- .phrase .try{padding:6px 12px 6px 9px;border-left:1px solid var(--border);color:var(--acc);cursor:pointer;font-weight:600;background:transparent;border-top:none;border-right:none;border-bottom:none;font-size:11px}- .phrase .try:hover{background:#12302f}- .phrase .txt:active,.phrase .try:active{transform:translateY(1px)}+ .say-chip{display:inline-flex;align-items:center;gap:8px;background:#0d0f14;border:1px solid var(--border);border-radius:999px;padding:5px 6px 5px 12px;font:500 13.5px var(--brand-body);color:var(--ink);margin:4px 6px 0 0;cursor:pointer;transition:border-color .15s}+ .say-chip:hover{border-color:var(--accent)}+ .say-chip em{color:var(--acc);font-style:normal}+ .say-chip .copy,.say-chip .send{font:500 11px var(--brand-body);background:none;border:1px solid var(--border);border-radius:999px;padding:2px 9px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}+ .say-chip .copy{color:var(--text-2)}+ .say-chip .send{color:var(--acc);border-color:rgba(0,184,177,.45)}+ .say-chip:hover .copy{color:var(--acc);border-color:var(--acc)}+ .say-chip .send:hover{background:rgba(0,184,177,.12)}+ .say-chip .copy:active,.say-chip .send:active{transform:translateY(1px)}+ #demo-row{margin:10px 18px 2px}+ #demo-row button{display:inline-flex;align-items:center;gap:7px;background:#12302f;border:1px solid var(--acc);color:var(--acc);border-radius:9px;padding:7px 16px;font:700 13px var(--brand-heading);cursor:pointer;transition:filter .12s,transform .06s}+ #demo-row button:hover{filter:brightness(1.2)}+ #demo-row button:active{transform:translateY(1px)} /* ---- Send progress panel: bar + live console + final-frame evidence ---- */ #sendpanel{position:fixed;right:14px;bottom:14px;z-index:1500;width:min(480px,92vw);background:var(--elevated);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;box-shadow:0 16px 48px #000c;display:none} #sendpanel.show{display:block}@@ -274,10 +294,13 @@ </head> <body> <header id="hdr">- <div class="brand"><img src="favicon.svg" alt="">adom-lbr <small>Dashboard</small></div>- <div class="leds" id="leds" title="live status from /api/status"></div>- <span class="ver" id="ver"></span>- <button class="iconbtn" id="btnSettings" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg></button>+ <div class="brand" id="brand"><img src="favicon.svg" alt=""><span class="bn">Adom LBR</span><span class="bs">· the EDA library converter</span>+ <div id="brandpop" hidden><b>Adom LBR</b> <span id="pop-ver"></span><br><a href="https://wiki.adom.inc/adom/adom-lbr" target="_blank" rel="noopener">wiki.adom.inc/adom/adom-lbr</a><div id="pop-extra"></div></div>+ </div>+ <span class="chip" id="targetchip" title="Which machine Adom Bridge answers on for the Send-to buttons"><i id="tdot"></i><span class="tc">target:</span> <span id="tname">looking…</span></span>+ <span style="margin-left:auto"></span>+ <button class="iconbtn" id="btnHelp" title="Bring back the getting-started helpers for this session" aria-label="Help">?</button>+ <button class="iconbtn" id="btnSettings" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,12.97L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"/></svg></button> </header> <nav id="sendbar" aria-label="Send the open part to your EDA tools"> <span class="lab">Send to KiCad</span>@@ -295,15 +318,19 @@ <div class="grp"> <button data-send="fusion-library" title="Stage the .lbr + .step on your PC and have the Fusion bridge build and prove the library">Library</button> </div>- <span class="abdot" id="abdot" title="Adom Bridge on this machine. The buttons stage files and drive KiCad / Fusion through it; the Bridge asks for your permission in its own window. Detection needs the dashboard opened at its 127.0.0.1 address (the dock does this); a browser may block loopback probes from a cloud proxy URL."><i></i><span id="abtext">looking for Adom Bridge…</span></span> </nav>+ <div id="scafwrap"> <section id="say" aria-labelledby="sayTitle" hidden>- <button class="iconbtn say-close" id="sayClose" aria-label="Hide the Say it strip" title="Hide this strip. It stays hidden until you turn it back on in Settings (Say it strip)."><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button>- <div class="say-head"><h2 id="sayTitle">Say it to your AI</h2>- <button id="btnDemo" title="A guided tour: the dashboard drives itself and shows you everything it can do"><svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>Demo</button>- <span class="say-sub">You do not drive this dashboard by hand. You ask your AI, using the word <b>lbr</b>. Click a phrase (or Try it) to copy it, then paste it into any Adom AI thread.</span></div>+ <button class="iconbtn say-close" id="sayClose" aria-label="Collapse the Say it strip" title="Collapse. The ? in the header brings it back."><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button>+ <div class="say-head"><h2 id="sayTitle">Say it</h2>+ <span class="say-sub">You never need this page's buttons: just tell your AI what you want, using the word <b>lbr</b>. Copy a phrase, or send it straight to a new AI thread.</span></div> <div class="say-list" id="phrases"></div> </section>+ <div id="demo-row"><button id="btnDemo" title="A narrated, self-driving tour of everything this dashboard can do"><svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>Play the demo</button></div>+ <button class="foldline" id="say-fold">Say it — <span id="fold-n">6</span> phrases · tap to expand</button>+ </div>+ <div class="ledrow" id="ledrow" title="live status; tap a pill for the detail"></div>+ <div id="led-pop" role="dialog"></div> <div id="panes"> <aside id="list"> <h1>Library <small>boards and parts</small><button id="refresh" title="refresh list (fetch newly-pushed boards)">↻</button></h1>@@ -342,6 +369,7 @@ <button id="tour-play" class="play" title="Pause (space)" aria-label="Play or pause"><svg id="tour-play-ic" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg></button> <button id="tour-next" title="Next step (right arrow)" aria-label="Next"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M16 6h2v12h-2zM6 18l8.5-6L6 6z"/></svg></button> <span class="tn" id="tour-progress">1 / 13</span>+ <button id="tour-mute" title="Mute or unmute the narration" aria-label="Mute"><svg id="tour-mute-ic" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05A4.5 4.5 0 0 0 16.5 12z"/></svg></button> <button id="tour-exit" title="Exit the demo (Esc)" aria-label="Exit"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button> </div> @@ -731,18 +759,88 @@ function toast(msg,err,kind){const t=document.getElementById('toast'); t.className='show'+(k?' '+k:'');clearTimeout(toastT);toastT=setTimeout(()=>t.className='',k==='success'?4000:2600);} async function api(path,opts){const r=await fetch(path,Object.assign({},opts,{signal:AbortSignal.timeout(5000)}));return r.json();} -// ---- live LEDs in the header, from the same /api/status the dock polls ----+// ---- status pill row (status-leds contract): compact pills, prose on tap ---- let lastStatus=null;+const busySince={}; // stuck? escalation: busy longer than 15 min+function shortState(l){+ if(l.state==='ok')return l.label==='Binary'?'v'+(lastStatus?lastStatus.version:''):'ok';+ if(l.state==='warn')return 'attention';+ return 'broken';+}+function paintLeds(st){+ const row=document.getElementById('ledrow');row.innerHTML='';+ (st.leds||[]).forEach(l=>{+ let state=l.state==='ok'?'ok':(l.state==='warn'?'busy':'bad');+ let stext=shortState(l);+ if(state==='busy'){+ busySince[l.label]=busySince[l.label]||Date.now();+ if(Date.now()-busySince[l.label]>15*60*1000)stext='stuck?';+ } else delete busySince[l.label];+ const pill=document.createElement('button');+ pill.className='led-pill';pill.dataset.state=state;+ pill.innerHTML='<span class="dot"></span>'+l.label+' <span class="st">'+stext+'</span>';+ pill.onclick=e=>{+ e.stopPropagation();+ const pop=document.getElementById('led-pop');+ pop.innerHTML='<b>'+l.label+'</b><br>'+l.detail+(l._hint?'<span class="fix">Fix: '+l._hint+'</span>':'');+ const r=pill.getBoundingClientRect();+ pop.style.display='block';+ pop.style.left=Math.min(innerWidth-440,Math.max(8,r.left))+'px';+ pop.style.top=(r.bottom+8)+'px';+ };+ row.appendChild(pill);+ });+}+document.addEventListener('click',()=>{document.getElementById('led-pop').style.display='none';}); async function refreshStatus(force){ try{ const s=await api('api/status'+(force?'?force=1':''));lastStatus=s;- document.getElementById('ver').textContent='v'+s.version;- const el=document.getElementById('leds');el.innerHTML='';- (s.leds||[]).forEach(l=>{const d=document.createElement('span');d.className='led '+l.state;d.title=l.label+': '+l.detail+(l._hint?'\nfix: '+l._hint:'');d.innerHTML='<i></i>'+l.label;el.appendChild(d);});+ document.getElementById('pop-ver').textContent='v'+s.version;+ paintLeds(s); paintSettingsStatus();- }catch(e){ document.getElementById('leds').innerHTML='<span class="led fail" title="dashboard not answering"><i></i>offline</span>'; }+ gradLaunchTick(s);+ }catch(e){ document.getElementById('ledrow').innerHTML='<button class="led-pill" data-state="bad"><span class="dot"></span>dashboard <span class="st">not answering</span></button>'; } } refreshStatus(); setInterval(refreshStatus,15000);+// brand popover: version + links live HERE, never in the bar (dash-header)+(function(){+ const b=document.getElementById('brand'),pop=document.getElementById('brandpop');let t=null;+ b.addEventListener('mouseenter',()=>{t=setTimeout(()=>{pop.hidden=false;},600);});+ b.addEventListener('mouseleave',()=>{clearTimeout(t);pop.hidden=true;});+ b.addEventListener('click',()=>{pop.hidden=!pop.hidden;}); // tap on mobile+})();++// ---- newbie graduation (localStorage; the ? recalls; say strip folds LAST) ----+// Namespaced: behind the cloud proxy every dashboard PORT shares one origin,+// so the fleet key cross-talks between apps there (found live 2026-08-24:+// a sibling app's aiDriven landed in our state). Direct 127.0.0.1:port is+// per-origin as the contract assumes; the namespace makes both safe.+const GRAD_KEY='adom-lbr:dash-graduation';+function gradState(){try{return JSON.parse(localStorage.getItem(GRAD_KEY))??{}}catch(e){return{}}}+function gradSave(patch){try{localStorage.setItem(GRAD_KEY,JSON.stringify(Object.assign(gradState(),patch)))}catch(e){}}+let gradRecall=false; // ? pressed: Fresh for this session+let gradCounted=false; // one launch count per page load+function gradLaunchTick(st){+ if(gradCounted||!st||st.state==='fail')return;+ gradCounted=true;+ gradSave({launches:(gradState().launches||0)+1});+ applyGraduation();+}+// AI-driven graduation: the first Send-to run or Send-to-AI success counts.+function gradAiDriven(){ if(!gradState().aiDriven){gradSave({aiDriven:true});applyGraduation();} }+function applyGraduation(){+ const g=gradState(),n=g.launches||0;+ const dism=(g.dismissed||{});+ const showSettings=settings?settings.show_say_strip!==false:true;+ const ai=!!g.aiDriven;+ const sayFresh=gradRecall||(showSettings&&!dism.say&&!ai&&n<5);+ const demoFresh=gradRecall||(!dism.say&&!ai&&n<3);+ document.getElementById('say').hidden=!sayFresh;+ document.getElementById('demo-row').style.display=demoFresh?'':'none';+ document.getElementById('say-fold').style.display=sayFresh?'none':'inline-block';+}+document.getElementById('btnHelp').onclick=()=>{gradRecall=true;applyGraduation();toast('Helpers are back for this session.',false,'success');};+document.getElementById('say-fold').onclick=()=>{gradRecall=true;applyGraduation();}; loadSettings(); // the Say-it strip's visibility rides settings, so load them at startup // ---- "Say it": phrases come from dockbar.json `say` via the server ----@@ -761,43 +859,39 @@ async function copyText(t){ return true; }catch(e){ return copyViaTextarea(t); } }-function applySayVisibility(){- const el=document.getElementById('say');- if(!settings) return;- el.hidden = settings.show_say_strip===false;-}+function applySayVisibility(){ applyGraduation(); } document.getElementById('sayClose').onclick=()=>{- save({show_say_strip:false});- toast('Say it strip hidden. Bring it back any time in Settings.');+ const g=gradState();gradSave({dismissed:Object.assign(g.dismissed||{}, {say:true})});+ gradRecall=false;applyGraduation();+ toast('Collapsed. The ? in the header brings it back.',false,'success'); }; async function loadSay(){ try{ const r=await api('api/say');const el=document.getElementById('phrases');el.innerHTML='';+ const noun=r.noun||'lbr';+ document.getElementById('fold-n').textContent=(r.say||[]).length; (r.say||[]).forEach(p=>{- const w=document.createElement('span');w.className='phrase';- const t=document.createElement('span');t.className='txt';t.textContent=p;t.title='click to copy';- t.onclick=async()=>{const ok=await copyText(p);if(ok)toast('Copied to clipboard. Paste it into any AI thread.',false,'success');else toast('Could not copy; select it by hand',true);};- // Try it copies the phrase (John's ruling, 2026-08-23): NO live AI-thread- // opening/staging for now — zero /ai-threads calls from page or server- // (the parked /api/try + the staging code stay dormant for the all-clear).- const TRY_STAGING=false;- const b=document.createElement('button');b.className='try';b.textContent='Try it';- b.title='copies this prompt so you can paste it into any AI thread';- b.onclick=async()=>{- b.disabled=true;- if(TRY_STAGING){- try{ const res=await api('api/try',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({phrase:p})});- if(res.staged){toast('Staged into '+(res.where||'your AI thread')+': "'+p+'" (press Enter there to send)');}- else{await copyText(p);toast('Copied instead: '+(res.reason||'could not stage'),true);}- }catch(e){await copyText(p);toast('Copied instead (dashboard could not reach the control API)',true);}- } else {- const ok=await copyText(p);- if(ok)toast('Copied to clipboard. Paste it into any AI thread.',false,'success');- else toast('Could not copy; select the phrase by hand and copy it',true);- }+ const w=document.createElement('span');w.className='say-chip';w.dataset.say=p;+ const quoted=('\u201C'+p+'\u201D').replace(new RegExp('\\b'+noun+'\\b','g'),'<em>'+noun+'</em>');+ const t=document.createElement('span');t.innerHTML=quoted;+ const doCopy=async()=>{const ok=await copyText(p);if(ok)toast('Copied to clipboard. Paste it into any AI thread.',false,'success');else toast('Could not copy; select it by hand',true);};+ w.onclick=doCopy; // largest hit target does the safest thing+ const c=document.createElement('button');c.className='copy';c.textContent='Copy';+ c.onclick=e=>{e.stopPropagation();doCopy();};+ // Send to AI (John's ruling 2026-08-24): a NEW thread via the panel's+ // AI-threads API, through our backend. New threads only; resume stays parked.+ const b=document.createElement('button');b.className='send';b.textContent='Send to AI';+ b.title='opens a new AI thread and sends this phrase';+ b.onclick=async e=>{+ e.stopPropagation();b.disabled=true;+ try{+ const res=await api('api/say-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({phrase:p})});+ if(res&&res.ok){toast('Sent to a new AI thread.',false,'success');gradAiDriven();}+ else{await copyText(p);toast((res&&res.why)||'Could not reach the AI panel; copied instead.',true);}+ }catch(err){await copyText(p);toast('Could not reach the AI panel; copied instead.',true);} b.disabled=false; };- w.appendChild(t);w.appendChild(b);+ w.appendChild(t);w.appendChild(c);w.appendChild(b); el.appendChild(w); }); }catch(e){}@@ -889,8 +983,9 @@ let abUp=false; async function abPing(){ try{const r=await fetch(AB+'/health',{signal:AbortSignal.timeout(1500)});const j=await r.json();abUp=(j.service==='adom-bridge');} catch(e){abUp=false;}- const d=document.getElementById('abdot');d.classList.toggle('on',abUp);- document.getElementById('abtext').textContent=abUp?'Adom Bridge ready':'Adom Bridge not detected on this machine';+ const chip=document.getElementById('targetchip');chip.classList.toggle('on',abUp);+ document.getElementById('tname').textContent=abUp?'this machine':'no bridge';+ chip.title=abUp?'Adom Bridge answers on this machine; the Send-to buttons stage files and drive KiCad / Fusion through it (it asks your permission in its own window)':'Adom Bridge is not detected on this machine, so the Send-to buttons are disabled. Detection needs the dashboard opened at its 127.0.0.1 address, which is how the dock opens it.'; updateSendbar(); } function updateSendbar(){@@ -1141,23 +1236,39 @@ function demoHoverPin(){ return hit; } function demoUnhover(){document.querySelectorAll('#view .pinhi').forEach(x=>x.classList.remove('pinhi'));const tip=document.getElementById('tip');if(tip)tip.style.display='none';}+// dash-demo contract: each step carries action(pre), caption (ab caption at the+// BRIDGE DEFAULT size - never custom), pre-created adom-tts audio, min_ms floor. const TOUR=[- {sel:'#hdr .brand',title:'This is adom-lbr',text:'The <b>EDA library converter</b>: symbols, footprints and 3D chips, moved between <b>KiCad, Fusion and Altium</b>. Everything you see comes live from the wiki, which is the library. This tour drives the app for real; you just read along.'},- {sel:'#say',title:'You ask your AI',text:'You do not drive this dashboard by hand. You ask your AI using the word <b>lbr</b>. These are real prompts: click one (or Try it) and it is copied for you to paste into any Adom AI thread.',pre:async()=>{if(settings&&settings.show_say_strip===false){save({show_say_strip:true});}}},- {sel:'#modes',title:'Projects and Parts',text:'<b>Projects</b> lists your boards from the wiki, each opening into its component list. <b>Parts</b> browses every component page on the wiki directly, nothing needs downloading or owning first.',pre:async()=>{setMode('projects');}},- {sel:'#rows',title:'The whole wiki library',text:'Every part page on the wiki, searchable. Type a part number or keyword to narrow it. Watch: the tour opens an industrial MCU, an STM32 H-line, for you.',pre:async()=>{setMode('parts');}},- {sel:'#view',title:'The part, in full',text:'One click renders everything from the part\'s canonical data: the schematic <b>symbol</b>, the <b>footprint</b>, and the <b>3D model</b>. Drag to pan, scroll to zoom on any of them; double-click resets.',pre:demoOpenSample},- {sel:'#view .card:nth-of-type(1)',title:'Hover any pin',text:'Pins and pads know who they are. Hover one and it lights up in BOTH the symbol and the footprint, with its number and name. The tour is holding one lit for you now.',pre:async()=>{await demoOpenSample();demoHoverPin();},post:demoUnhover},- {sel:'#view .lfilter',title:'Layer filter',text:'The footprint\'s layers are live chips: copper, courtyard, silkscreen, fab. Untick one and that layer hides, so you can inspect exactly the geometry you care about.'},- {sel:'#view model-viewer, #view .card:nth-of-type(3)',title:'The 3D chip',text:'The part\'s real STEP model, rendered in the page. Orbit it with a drag; it auto-rotates when idle. The same model is what gets sent to your desktop tools.'},- {sel:'#view table',title:'The layer stackup',text:'The honest map of what each footprint element becomes <b>per EDA tool</b>: courtyard to F.CrtYd in KiCad and Mechanical 15 in Altium, and so on. This is the translator showing its work.',pre:async()=>{const t=demoEl('#view table');if(t)t.scrollIntoView({behavior:'smooth',block:'center'});}},- {sel:'#sendbar',title:'Send it to your tools',text:'First-class buttons: install this part into <b>KiCad</b> (symbol, footprint, 3D wired) and pop it open in the editor, or have the <b>Fusion</b> bridge build and prove the library. Adom Bridge asks your permission first, and a live panel shows the <b>progress bar, streaming console and evidence screenshots</b> as it lands, so you are never running blind.'},- {sel:'#leds',title:'Live health',text:'Six LEDs, straight from the dashboard\'s own status: the wiki link, your sign-in, the library cache, the 3D viewer assets, the dock. Hover any LED for the detail, and the fix when one is not green.'},- {sel:'#btnSettings',title:'Settings',text:'The cog holds the rest: the Say it strip switch, whose boards count as My projects, the list cache, and package Update/Uninstall with a live console. Every section explains itself.'},- {sel:'#say',title:'That is the tour',text:'From here: browse <b>Parts</b>, open anything, and when you want real work done, copy a phrase and ask your AI, the word is <b>lbr</b>. Enjoy.'}+ {caption:"Welcome to Adom LBR: the tour drives the app for real.",sel:'#hdr .brand',title:'This is adom-lbr',text:'The <b>EDA library converter</b>: symbols, footprints and 3D chips, moved between <b>KiCad, Fusion and Altium</b>. Everything you see comes live from the wiki, which is the library. This tour drives the app for real; you just read along.'},+ {caption:"These phrases hand the app to your AI. The word is lbr.",sel:'#say',title:'You ask your AI',text:'You do not drive this dashboard by hand. You ask your AI using the word <b>lbr</b>. These are real prompts: click one (or Try it) and it is copied for you to paste into any Adom AI thread.',pre:async()=>{if(settings&&settings.show_say_strip===false){save({show_say_strip:true});}}},+ {caption:"Projects are your boards; Parts is the whole wiki library.",sel:'#modes',title:'Projects and Parts',text:'<b>Projects</b> lists your boards from the wiki, each opening into its component list. <b>Parts</b> browses every component page on the wiki directly, nothing needs downloading or owning first.',pre:async()=>{setMode('projects');}},+ {caption:"Searching the wiki library and opening an industrial MCU.",sel:'#rows',title:'The whole wiki library',text:'Every part page on the wiki, searchable. Type a part number or keyword to narrow it. Watch: the tour opens an industrial MCU, an STM32 H-line, for you.',pre:async()=>{setMode('parts');}},+ {caption:"Symbol, footprint and 3D, rendered live from the wiki.",sel:'#view',title:'The part, in full',text:'One click renders everything from the part\'s canonical data: the schematic <b>symbol</b>, the <b>footprint</b>, and the <b>3D model</b>. Drag to pan, scroll to zoom on any of them; double-click resets.',pre:demoOpenSample},+ {caption:"Hovering a pin: it lights in the symbol AND the footprint.",sel:'#view .card:nth-of-type(1)',title:'Hover any pin',text:'Pins and pads know who they are. Hover one and it lights up in BOTH the symbol and the footprint, with its number and name. The tour is holding one lit for you now.',pre:async()=>{await demoOpenSample();demoHoverPin();},post:demoUnhover},+ {caption:"Layer chips show and hide the footprint layers.",sel:'#view .lfilter',title:'Layer filter',text:'The footprint\'s layers are live chips: copper, courtyard, silkscreen, fab. Untick one and that layer hides, so you can inspect exactly the geometry you care about.'},+ {caption:"The STEP model of the part, orbitable in the page.",sel:'#view model-viewer, #view .card:nth-of-type(3)',title:'The 3D chip',text:'The part\'s real STEP model, rendered in the page. Orbit it with a drag; it auto-rotates when idle. The same model is what gets sent to your desktop tools.'},+ {caption:"What each element becomes in KiCad and in Altium.",sel:'#view table',title:'The layer stackup',text:'The honest map of what each footprint element becomes <b>per EDA tool</b>: courtyard to F.CrtYd in KiCad and Mechanical 15 in Altium, and so on. This is the translator showing its work.',pre:async()=>{const t=demoEl('#view table');if(t)t.scrollIntoView({behavior:'smooth',block:'center'});}},+ {caption:"Send to KiCad or Fusion; a live panel shows the evidence.",sel:'#sendbar',title:'Send it to your tools',text:'First-class buttons: install this part into <b>KiCad</b> (symbol, footprint, 3D wired) and pop it open in the editor, or have the <b>Fusion</b> bridge build and prove the library. Adom Bridge asks your permission first, and a live panel shows the <b>progress bar, streaming console and evidence screenshots</b> as it lands, so you are never running blind.'},+ {caption:"Live health pills; every red one carries its fix.",sel:'#leds',title:'Live health',text:'Six LEDs, straight from the dashboard\'s own status: the wiki link, your sign-in, the library cache, the 3D viewer assets, the dock. Hover any LED for the detail, and the fix when one is not green.'},+ {caption:"The cog opens Settings; every section explains itself.",sel:'#btnSettings',title:'Settings',text:'The cog holds the rest: the Say it strip switch, whose boards count as My projects, the list cache, and package Update/Uninstall with a live console. Every section explains itself.'},+ {caption:"That is the tour. Ask your AI; the word is lbr.",sel:'#say',title:'That is the tour',text:'From here: browse <b>Parts</b>, open anything, and when you want real work done, copy a phrase and ask your AI, the word is <b>lbr</b>. Enjoy.'} ]; let tourI=-1,tourPlaying=false,tourTimer=null;+let tourMuted=false; try{tourMuted=localStorage.getItem('adom-lbr-demo-muted')==='1';}catch(e){} const tourAudio=new Audio();+// ab captions, fired per step at the BRIDGE DEFAULT size/position (fleet+// consistency ruling: never pass a custom size). Best-effort: no bridge, no+// consent, or a refusal all mean the in-page card still tells the story.+function tourCaption(text){+ if(!abUp)return;+ fetch(AB+'/command',{method:'POST',headers:{'Content-Type':'application/json'},+ body:JSON.stringify({command:'desktop_caption',args:{text:text||'',caller:{aiThread:'adom-lbr dashboard demo'}}})}).catch(()=>{});+}+function tourPaintMute(){+ tourAudio.muted=tourMuted;+ const ic=document.getElementById('tour-mute-ic');+ ic.innerHTML=tourMuted?'<path fill="currentColor" d="M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.8 8.8 0 0 0 21 12a9 9 0 0 0-7-8.77v2.06A7 7 0 0 1 19 12zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a9 9 0 0 0 3.69-1.81L19.73 21 21 19.73 4.27 3zM12 4 9.91 6.09 12 8.18V4z"/>':'<path fill="currentColor" d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05A4.5 4.5 0 0 0 16.5 12z"/>';+} tourAudio.preload='auto'; tourAudio.onended=()=>{ if(tourPlaying&&tourI>=0){ if(tourI<TOUR.length-1)tourShow(tourI+1); else tourStop(); } }; tourAudio.onerror=()=>{ if(tourPlaying&&tourI>=0){ clearTimeout(tourTimer); tourTimer=setTimeout(()=>{ if(tourPlaying&&tourI<TOUR.length-1)tourShow(tourI+1); else if(tourPlaying)tourStop(); },7000); } };@@ -1169,6 +1280,7 @@ function tourPaintPlay(){ function tourStop(){ tourI=-1;tourPlaying=false;clearTimeout(tourTimer); try{tourAudio.pause();}catch(e){}+ tourCaption(''); // never let a caption outlive the demo demoUnhover(); document.getElementById('tour-ring').style.display='none'; document.getElementById('tour-card').style.display='none';@@ -1201,6 +1313,8 @@ async function tourShow(i){ // pre-rendered narration (adom-tts, shipped with the app - nothing generated live) clearTimeout(tourTimer); try{tourAudio.pause();}catch(e){}+ tourCaption(st.caption||'');+ tourPaintMute(); tourAudio.src='tour/step-'+String(i+1).padStart(2,'0')+'.mp3'; if(tourPlaying){ tourAudio.play().catch(()=>{ // autoplay refused: keep the tour moving on a timer@@ -1223,6 +1337,7 @@ document.getElementById('tour-play').onclick=()=>{ if(tourPlaying){ (tourAudio.src&&tourAudio.paused)?tourAudio.play().catch(()=>{}):tourShow(tourI); } else { try{tourAudio.pause();}catch(e){} clearTimeout(tourTimer); } };+document.getElementById('tour-mute').onclick=()=>{tourMuted=!tourMuted;try{localStorage.setItem('adom-lbr-demo-muted',tourMuted?'1':'0');}catch(e){}tourPaintMute();}; document.getElementById('tour-exit').onclick=tourStop; document.addEventListener('keydown',e=>{if(tourI>=0){ if(e.key==='Escape')tourStop();
src/manage.rs+8@@ -159,6 +159,14 @@ fn route_inner(method: &Method, url: &str, path: &str, body: &str) -> Response<C json_resp(&dock::cached_status(library_count(), &my_owner(), force).to_string()) } (Method::Get, "/api/say") => json_resp(&json!({"status":"ok","say":dock::say_phrases(),"noun":"lbr"}).to_string()),+ // say-strip contract: Send to AI opens a NEW thread and submits (new+ // threads only; the resume path stays parked in /api/try).+ (Method::Post, "/api/say-send") => {+ let phrase = serde_json::from_str::<Value>(body).ok()+ .and_then(|v| v.get("phrase").and_then(|p| p.as_str()).map(String::from))+ .unwrap_or_default();+ json_resp(&dock::say_send(&phrase).to_string())+ } (Method::Post, "/api/try") => { let phrase = serde_json::from_str::<Value>(body).ok() .and_then(|v| v.get("phrase").and_then(|p| p.as_str()).map(String::from))