← Commit history
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.134"+version = "1.9.138" edition = "2021"  [dependencies]
dashboard/src/main.rs+98−16
@@ -125,7 +125,17 @@ fn gather_status_ex(ai_thread: &str, target: &str, launching: bool) -> serde_jso     // 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();+    // TOP-LEVEL ROLLUP (dock_manifest 1.2.0, issue #37 checklist item 2): our own verdict+    // of which LEDs deserve to change the card, so the dock never falls back to its blunt+    // worst-facet rule. Fusion being CLOSED is a normal state for a control panel - the+    // card stays green. Only two things are worth a yellow: the bridge not answering+    // (transient - busy or relay hiccup), and Fusion up with the add-in neither serving+    // nor busy (self-healable, so warn, not fail). update/aps/mcp never move the card.+    let rollup = if !answered { "warn" }+                 else if running && !ready && last_ready.is_empty() { "warn" }+                 else { "ok" };     serde_json::json!({+        "state": rollup,         "target": target,         "answered": answered,         "busyReason": if answered { serde_json::Value::Null }@@ -998,6 +1008,13 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {             respond_json(&mut stream, &serde_json::json!({"targets": rows,                 "_hint": "One Autodesk seat: start Fusion on whichever box suits the moment - the seat follows automatically (the other box's Fusion suspends resumably). Pass ?target=<name> on /api/status and target in /api/action bodies."}));         }+        ("GET", "/api/ping") => {+            // INSTANT identity probe - /api/status does a live bridge round trip (seconds),+            // which made the self-background launch probe time out and the dock step hang+            // (issue #37). This answers from memory, always, in microseconds.+            respond_json(&mut stream, &serde_json::json!({+                "app": "fusion-dashboard", "dashboardVersion": VERSION}));+        }         ("GET", "/api/status") | ("GET", "/state") => {             let t = qs_target.clone().unwrap_or_else(|| app.tgt());             app.log(format!("status target={}", t));@@ -1141,7 +1158,88 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {     } } +/// The URL a dock webview can ACTUALLY REACH for a given port (issue #37: a webview+/// renders OUTSIDE this container, so loopback is only right for a genuinely local run).+fn url_for(port: u16) -> String {+    std::env::var("VSCODE_PROXY_URI")+        .ok()+        .filter(|u| u.contains("{{port}}"))+        .map(|u| u.replace("{{port}}", &port.to_string()))+        .unwrap_or_else(|| format!("http://127.0.0.1:{}/", port))+}++/// Probe 8930-8940 for an already-serving FUSION dashboard. "dashboardVersion" alone is+/// not enough: the KiCad dashboard also serves it (found live on 8931 - the first probe+/// draft would have handed the Fusion dock card KiCad's URL). Require a facet only OUR+/// payload carries.+fn find_running_instance() -> Option<u16> {+    for p in 8930..=8940u16 {+        let out = Command::new("curl")+            .args(["-s", "-m", "1", &format!("http://127.0.0.1:{}/api/ping", p)])+            .output();+        if let Ok(o) = out {+            if String::from_utf8_lossy(&o.stdout).contains("\"app\":\"fusion-dashboard\"") {+                return Some(p);+            }+        }+    }+    None+}+ fn serve(port: u16, target: String, ai_thread: String, print_url: bool) {+    if print_url {+        // THE DOCK LAUNCH STEP (issue #37, the blocker verified on v1.9.126 and v1.9.133):+        // Hydrogen runs this step SYNCHRONOUSLY, so a serve that stays in the foreground+        // blocks the exec for the whole step budget, gets its process TREE killed on+        // timeout, and the URL we printed is dead before Hydrogen polls it. The contract+        // is: print the URL, put the server in the BACKGROUND, exit 0 within seconds.+        //+        // Warm path first (SDK rule: "detect a healthy instance already on the port,+        // print ITS url, exit 0") - a relaunch then costs nothing and never kills the+        // running instance.+        if let Some(p) = find_running_instance() {+            println!("{}", url_for(p));+            ok(&format!("fusion-dashboard already serving on port {} - reusing it", p));+            return;+        }+        // Cold path: re-exec ourselves detached (log to ~/.adom/fusion-bridge/), wait+        // until the child answers, print its URL, and let THIS process exit 0.+        let exe = std::env::current_exe().unwrap_or_else(|_| "fusion-dashboard".into());+        let log_dir = std::env::var("HOME").map(|h| format!("{}/.adom/fusion-bridge", h))+            .unwrap_or_else(|_| "/tmp".into());+        let _ = std::fs::create_dir_all(&log_dir);+        let log = std::fs::OpenOptions::new().create(true).append(true)+            .open(format!("{}/dashboard.log", log_dir));+        let mut cmd = Command::new(exe);+        cmd.arg("serve").args(["--port", &port.to_string()]);+        if !target.is_empty() { cmd.args(["--target", &target]); }+        cmd.args(["--ai-thread", &ai_thread]);+        cmd.stdin(std::process::Stdio::null());+        match log {+            Ok(f) => {+                let f2 = f.try_clone().ok();+                cmd.stdout(f);+                if let Some(f2) = f2 { cmd.stderr(f2); } else { cmd.stderr(std::process::Stdio::null()); }+            }+            Err(_) => { cmd.stdout(std::process::Stdio::null()); cmd.stderr(std::process::Stdio::null()); }+        }+        if cmd.spawn().is_err() {+            err_hint("could not spawn the background server",+                     "Run `fusion-dashboard serve` by hand and read its output.");+            std::process::exit(1);+        }+        for _ in 0..40 {+            std::thread::sleep(Duration::from_millis(250));+            if let Some(p) = find_running_instance() {+                println!("{}", url_for(p));+                ok(&format!("fusion-dashboard serving on port {} (backgrounded; log: {}/dashboard.log)", p, log_dir));+                return;+            }+        }+        err_hint("background server did not answer within 10s",+                 &format!("Read {}/dashboard.log for its output.", log_dir));+        std::process::exit(1);+    }     let mut port = port;     let listener = loop {         match TcpListener::bind(("0.0.0.0", port)) {@@ -1171,22 +1269,6 @@ fn serve(port: u16, target: String, ai_thread: String, print_url: bool) {         launching: Mutex::new(std::collections::HashMap::new()),         last_launch_note: Mutex::new(std::collections::HashMap::new()),     });-    if print_url {-        // Hydrogen dock await:url contract - the URL a webview can ACTUALLY REACH.-        //-        // The comment here used to say "the proxied URL" while printing loopback (page-        // issue #37). A dock webview renders OUTSIDE this container, so 127.0.0.1 is not-        // its loopback - it resolves to the viewer's own machine and the dock navigates to-        // nothing. code-server publishes the mapping we need as VSCODE_PROXY_URI, e.g.-        // https://<workspace>.adom.cloud/proxy/{{port}}/ - substitute the port and print-        // that. Loopback stays as the fallback for a genuinely local run, where it is right.-        let url = std::env::var("VSCODE_PROXY_URI")-            .ok()-            .filter(|u| u.contains("{{port}}"))-            .map(|u| u.replace("{{port}}", &port.to_string()))-            .unwrap_or_else(|| format!("http://127.0.0.1:{}/", port));-        println!("{}", url);-    }     ok(&format!("fusion-dashboard v{} serving on port {} (target {})", VERSION, port,         if app.target.is_empty() { "auto" } else { &app.target })); 
dock.json+7−2
@@ -1,8 +1,8 @@ {-  "dock_manifest": "1.1.0",+  "dock_manifest": "1.2.0",   "webview": {     "title": "Fusion Dashboard",-    "favicon": "docs/img/dock-fusion.svg"+    "favicon": "docs/img/dock-fusion.png"   },   "launch": {     "mode": "container_command",@@ -29,5 +29,10 @@       "submit": "staged",       "typed": false     }+  },+  "health": {+    "url": "/api/status",+    "poll_seconds": 15,+    "timeout_ms": 2000   } }\ No newline at end of file
docs/img/dock-fusion.pngadded
⋯ 1 unchanged line ⋯
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.134"+DASH_VERSION="1.9.138" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.137",+  "version": "1.9.138",   "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+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.137",+  "version": "1.9.138",   "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": {@@ -11,6 +11,10 @@     "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