app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
dashboard 1.9.74: status honesty under load
7 files changed
+60−14
CHANGELOG.md+10@@ -3,6 +3,16 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.9.74 - 2026-08-18 (ralph, dashboard)++- **The board no longer lies while the bridge is busy.** Driving the guided tour from the UI+ exposed two honesty bugs in one screenshot: the header regressed to "bridge ?" and EVERY LED+ flipped red while the tour was visibly running (a status probe that times out under load was+ being read as "everything off"). Now: the server keeps a last-verified payload per target and+ serves it with `stale:true` + `busyReason` when a probe cannot answer, the header never+ regresses a known version, and a running-but-not-answering add-in renders AMBER "busy or still+ starting" with the timestamp it was last verified - never red "not serving".+ ## 1.9.72 - 2026-08-18 (ralph) - **Design surfaces regenerate their project on demand.** Staging cached itself as complete even
bin/fusion-dashboard⋯ 1 unchanged line ⋯
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.72"+version = "1.9.74" edition = "2021" [dependencies]
dashboard/src/main.rs+33−2@@ -111,16 +111,31 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value { let upd_offered = rd["fusionUpdateOffered"].as_bool().unwrap_or(false); let upd_handled = rd["fusionUpdateHandled"].as_bool().unwrap_or(false); + // Did the bridge actually answer? A busy bridge returns an error envelope with no version+ // and no running flag - that is UNKNOWN, not "everything off".+ let answered = rd.get("bridgeVersion").map(|v| !v.is_null()).unwrap_or(false)+ || rd.get("running").is_some() || rd.get("installed").is_some(); serde_json::json!({ "target": target,+ "answered": answered,+ "busyReason": if answered { serde_json::Value::Null }+ else { serde_json::json!(rd["error"].as_str().unwrap_or("the bridge did not answer (busy running a long verb?)")) }, "dashboardVersion": VERSION, "bridgeVersion": rd["bridgeVersion"], "leds": { "fusionRunning": {"on": running, "label": "Fusion running", "_hint": if running {"Fusion is up."} else {"fusion_start launches it in the BACKGROUND (never steals focus)."}}, "addinReady": {"on": ready, "label": "Bridge add-in", "version": addin,- "state": if ready {"on"} else if !running && !last_ready.is_empty() {"dormant"} else {"off"},+ // BUSY is not BROKEN (ralph, 2026-08-18): while a long verb runs - a tour, a 3D+ // build - readiness answers not-ready because the add-in's main thread is+ // occupied. Painting that red said "not serving" while the tour was visibly+ // driving Fusion. Amber + an honest sentence instead.+ "state": if ready {"on"}+ else if running && !last_ready.is_empty() {"busy"}+ else if !running && !last_ready.is_empty() {"dormant"}+ else {"off"}, "_hint": if ready {"Add-in serving - all fusion_* verbs live.".to_string()}+ else if running && !last_ready.is_empty() {format!("Busy or still starting - Fusion is up and the add-in was serving at {}. A long verb (a tour, a 3D build) occupies its main thread, so readiness answers not-ready until it finishes.", last_ready)} else if !running && !last_ready.is_empty() {format!("Dormant - resumes when Fusion starts (last verified {}).", last_ready)} else {"Not serving. If needsSignin, run fusion_signin - it self-drives to the password step.".to_string()}}, "signedIn": {"on": running && !needs_signin, "label": "Autodesk sign-in",@@ -237,6 +252,10 @@ struct App { // (libraryPath, deviceset, designReady) - designReady false means the generated test // project did NOT land, so the four design surfaces must regenerate it before they run. staged: Mutex<std::collections::HashMap<String, (String, String, bool)>>,+ // Last GOOD status per target. While the bridge is busy (a tour, a long verb), readiness+ // calls time out and every field reads false - which used to paint the whole board RED+ // while the tour was visibly running (ralph, 2026-08-18). Unknowable is not broken.+ last_good: Mutex<std::collections::HashMap<String, serde_json::Value>>, } // ── Dashboard-own settings (container-side; bridge prefs stay in the bridge) ──────────────@@ -725,7 +744,18 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { ("GET", "/api/status") | ("GET", "/state") => { let t = qs_target.clone().unwrap_or_else(|| app.tgt()); app.log(format!("status target={}", t));- let st = gather_status(&app.ai_thread, &t);+ let mut st = gather_status(&app.ai_thread, &t);+ if st["answered"].as_bool() == Some(true) {+ app.last_good.lock().unwrap().insert(t.clone(), st.clone());+ } else if let Some(prev) = app.last_good.lock().unwrap().get(&t).cloned() {+ // Keep showing the last VERIFIED state and say it is stale, instead of flipping+ // every LED red just because the bridge was too busy to answer.+ let reason = st["busyReason"].clone();+ st = prev;+ st["stale"] = serde_json::Value::Bool(true);+ st["busyReason"] = reason;+ st["_hint"] = serde_json::json!("Showing the LAST VERIFIED state: the bridge is busy (a tour or long verb is running) so a fresh probe did not answer. Nothing here is broken - it is simply not re-checkable this second.");+ } app.hub.broadcast("state", &st); respond_json(&mut stream, &st); }@@ -847,6 +877,7 @@ fn serve(port: u16, target: String, ai_thread: String, print_url: bool) { auto_green_running: Mutex::new(std::collections::HashSet::new()), auto_green_last: Mutex::new(std::collections::HashMap::new()), staged: Mutex::new(std::collections::HashMap::new()),+ last_good: Mutex::new(std::collections::HashMap::new()), }); if print_url { // Hydrogen dock await:url contract - the proxied URL a webview can reach.
dashboard/ui/index.html+4−2@@ -296,13 +296,15 @@ function renderLeds(st){ const keys=LED_ORDER.filter(k=>leds[k]).concat(Object.keys(leds).filter(k=>!LED_ORDER.includes(k))); for(const key of keys){ const l=leds[key];- const cls=l.pending?'warn':(l.state==='dormant'?'warn':(l.on?'on':'off'));+ const cls=l.pending?'warn':((l.state==='dormant'||l.state==='busy')?'warn':(l.on?'on':'off')); const el=document.createElement('div');el.className='card'; el.innerHTML=`<div class="led-row"><span class="led ${cls}"></span><span class="led-label">${l.label}</span></div><div class="led-sub">${l._hint||''}</div>`; g.appendChild(el); } $('#subject').textContent=st.target?('target: '+st.target):'no bridge target';- $('#meta').textContent='bridge '+(st.bridgeVersion||'?')+' | dashboard '+(st.dashboardVersion||'');+ if(st.bridgeVersion)window.__lastBridgeVer=st.bridgeVersion;+ $('#meta').textContent='bridge '+(st.bridgeVersion||window.__lastBridgeVer||'?')+' | dashboard '+(st.dashboardVersion||'')+ +(st.stale?' (busy - last verified state)':''); } async function refresh(){try{lastStatus=await api(statusUrl());renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}} $('#btnRefresh').onclick=()=>{toast('refreshing status');refresh();};
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.72",+ "version": "1.9.74", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,16 +1,20 @@ { "slug": "fusion-bridge",- "version": "1.9.72",+ "version": "1.9.74", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {- "adom/adom-desktop": "*"+ "adom/adom-desktop": "^1.9.0" }, "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -70,7 +74,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -99,9 +106,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }+ ] }