← Commit history

dashboard 1.9.74: status honesty under load

John Lauer ·54d0983653 ·1mo ago ·parent fc6c4fa
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]"-  }+  ] }\ No newline at end of file