← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.52\ No newline at end of file+1.9.53\ No newline at end of file
CHANGELOG.md+18
@@ -3,6 +3,24 @@ 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.53 - 2026-08-17++- **Honest state detection (John: "you aren't detecting my state all that well").** Sign-in,+  add-in, and MCP are UNKNOWABLE while Fusion is closed - the bridge now persists last-verified+  observations (lastReadyAt / lastSignedInAt on readiness, lastEnabledAt on mcp_status) and the+  Dashboard paints those facets AMBER-dormant ("configured, resumes when Fusion starts") instead+  of broken-red. Red now means "actually needs setup".+- **Setup is clean for configured users**: dormant facets are excluded from the pending list, so+  a weeks-old setup shows "Everything is configured - Fusion just is not running" with Start.+- **Multi-box is first-class.** New /api/targets + target pills in the Dashboard header (running+  dot per box, sticky choice); every status read and action carries the chosen box. One-seat copy+  explains the workflow: start Fusion on the conference box, the seat follows automatically, the+  laptop stays undisturbed.+- **Taskbar badge: "Adom is driving Fusion" (John's overlay idea).** New handlers/taskbar_overlay.py+  (ITaskbarList3::SetOverlayIcon via ctypes, no deps) badges Fusion's taskbar button with the Adom+  mark while the background envelope is active - set on first envelope, cleared when the refcount+  drains. resources/adom-overlay.ico ships in the zip.+ ## 1.9.52 - 2026-08-16  - **The catalog now teaches the full foreground contract.** fusion_describe's foreground arg docs
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.52",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.52/adom-bridge-fusion-v1.9.52.zip",-    "sha256": "eaa4e8f6383c10fdd90e007199eb1077d080e8371fdab5ff31b170cd58370c8a",-    "size": 399141,+    "version": "1.9.53",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.53/adom-bridge-fusion-v1.9.53.zip",+    "sha256": "1c42f1d13bee5b915b0885bc53c87e637cf149daef80d6c096a86cd061545381",+    "size": 403956,     "verbPrefixes": [         "fusion_"     ],
bin/fusion-dashboard
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.52",+  "version": "1.9.53",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.46"+version = "1.9.53" edition = "2021"  [dependencies]
dashboard/src/main.rs+44−5
@@ -90,6 +90,12 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value {     let ready = rd["ready"].as_bool().unwrap_or(false);     let needs_signin = rd["needsSignin"].as_bool().unwrap_or(false);     let addin = rd["addinVersion"].as_str().unwrap_or("").to_string();+    // Configured-but-dormant beats broken-red (John, 2026-08-17): while Fusion is closed,+    // sign-in/add-in/MCP are UNKNOWABLE - paint them amber with the last verified time,+    // never red. Red is reserved for "actually needs setup".+    let last_signed = rd["lastSignedInAt"].as_str().unwrap_or("");+    let last_ready = rd["lastReadyAt"].as_str().unwrap_or("");+    let mcp_last = mcp["lastEnabledAt"].as_str().unwrap_or("");     let aps_ok = aps["signedIn"].as_bool().or(aps["configured"].as_bool()).unwrap_or(false)         || aps["ready"].as_bool().unwrap_or(false);     let mcp_on = mcp["enabled"].as_bool().unwrap_or(false);@@ -104,13 +110,22 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value {             "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,-                "_hint": if ready {"Add-in serving - all fusion_* verbs live."} else {"Not serving. If needsSignin, run fusion_signin - it self-drives to the password step."}},+                "state": if ready {"on"} 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!("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",-                "_hint": if needs_signin {"fusion_signin {email} self-drives the whole wall: launches, clicks, types the email, toasts the user ONLY at their password."} else {"Signed in."}},+                "state": if running && !needs_signin {"on"} else if !running && !last_signed.is_empty() {"dormant"} else {"off"},+                "_hint": if needs_signin {"fusion_signin {email} self-drives the whole wall: launches, clicks, types the email, toasts the user ONLY at their password.".to_string()}+                         else if !running && !last_signed.is_empty() {format!("Signed in (verified {}). Unknowable while Fusion is closed - dormant, not broken.", last_signed)}+                         else if running {"Signed in.".to_string()} else {"Unknown until Fusion runs.".to_string()}},             "aps": {"on": aps_ok, "label": "APS cloud search",                 "_hint": if aps_ok {"fusion_aps_search gives ~2s cloud lookups."} else {"Not configured: fusion_aps_set_client_id then fusion_aps_signin (silent when the browser session is warm)."}},             "mcp": {"on": mcp_on, "label": "Fusion MCP server",-                "_hint": if mcp_on {"fusion_mcp_call drives Autodesk's own MCP tools."} else {"fusion_mcp_enable turns it on (~10s, drives the Preferences dialog with a caption)."}},+                "state": if mcp_on {"on"} else if !running && !mcp_last.is_empty() {"dormant"} else {"off"},+                "_hint": if mcp_on {"fusion_mcp_call drives Autodesk's own MCP tools.".to_string()}+                         else if !running && !mcp_last.is_empty() {format!("Enabled in Fusion's preferences (verified {}) - resumes when Fusion starts.", mcp_last)}+                         else {"fusion_mcp_enable turns it on (~10s, drives the Preferences dialog with a caption).".to_string()}},             "datahub": {"on": aps_ok, "label": "Data Hub API",                 "_hint": "Data Hub rides the APS auth: file listings, versions, urn resolution via fusion_aps_get."},             "update": {"on": !upd_offered || upd_handled, "label": "Fusion up to date",@@ -234,6 +249,8 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {     let method = it.next().unwrap_or("").to_uppercase();     let path_full = it.next().unwrap_or("/").to_string();     let path = path_full.split('?').next().unwrap_or("/").to_string();+    let qs_target = path_full.split('?').nth(1).unwrap_or("").split('&')+        .find_map(|kv| kv.strip_prefix("target=").map(|v| v.replace("%20", " ")));     let mut content_len = 0usize;     loop {         let mut h = String::new();@@ -296,8 +313,30 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {                 }             }         }+        ("GET", "/api/targets") => {+            // Multi-box support (John, 2026-08-17): he runs Fusion on a conference box so+            // the laptop stays undisturbed - the dashboard must treat that as first-class.+            let out = Command::new("adom-bridge").arg("targets").output();+            let mut rows = Vec::new();+            if let Ok(o) = out {+                if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&o.stdout) {+                    for t in v["targets"].as_array().unwrap_or(&Vec::new()) {+                        let name = t["name"].as_str().unwrap_or("").to_string();+                        let rd = unwrap_data(&bridge(&app.ai_thread, &name, "fusion_readiness", "{}"));+                        rows.push(serde_json::json!({+                            "name": name,+                            "running": rd["running"].as_bool().unwrap_or(false),+                            "ready": rd["ready"].as_bool().unwrap_or(false),+                            "lastReadyAt": rd["lastReadyAt"],+                        }));+                    }+                }+            }+            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/status") | ("GET", "/state") => {-            let t = app.tgt();+            let t = qs_target.clone().unwrap_or_else(|| app.tgt());             app.log(format!("status target={}", t));             let st = gather_status(&app.ai_thread, &t);             app.hub.broadcast("state", &st);@@ -313,7 +352,7 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {         }         ("POST", "/api/action") => {             let name = body["action"].as_str().unwrap_or("").to_string();-            let t = app.tgt();+            let t = body["target"].as_str().map(|s| s.to_string()).unwrap_or_else(|| app.tgt());             app.log(format!("action {} target={}", name, t));             let out = action(&app.ai_thread, &t, &name, &body["args"]);             respond_json(&mut stream, &out);
dashboard/ui/index.html+34−6
@@ -65,6 +65,12 @@ dialog::backdrop{background:rgba(0,0,0,.55)} #appinfo.show{display:block} #appinfo a{color:var(--accent)} .brand{cursor:default}+#targets{display:flex;gap:8px;margin:0 0 14px}+.tpill{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;border:1px solid var(--border);background:var(--surface);color:var(--text-body);cursor:pointer;font:500 13.5px 'Satoshi',sans-serif}+.tpill.active{border-color:var(--accent);color:var(--text);background:var(--elevated)}+.tdot{width:8px;height:8px;border-radius:999px;background:var(--text-2)}+.tdot.on{background:var(--green)}+.seatnote{font-size:12.5px;color:var(--text-2);margin:-6px 0 12px} </style> </head> <body>@@ -83,6 +89,8 @@ dialog::backdrop{background:rgba(0,0,0,.55)} </header> <main>   <h2>Status</h2>+  <div id="targets"></div>+  <div class="seatnote" id="seatnote" style="display:none">One Autodesk seat, many boxes: start Fusion wherever suits the moment - the seat follows automatically and the other box's Fusion suspends resumably.</div>   <div class="grid" id="ledGrid"></div>   <div class="btnrow">     <button class="btn primary" id="btnSetup">Setup</button>@@ -149,9 +157,28 @@ const consoleBuf=[]; window.onerror=(m)=>{consoleBuf.push("onerror: "+m);}; function toast(msg,kind){const t=document.createElement('div');t.className='toast '+(kind||'');t.textContent=msg;$('#toasts').appendChild(t);setTimeout(()=>t.remove(),kind==='err'?9000:4000);} async function api(path,opts){const r=await fetch(path,opts);return r.json();}-async function act(action,args){return api('api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,args:args||{}})});}+async function act(action,args){return api('api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,target:curTarget||undefined,args:args||{}})});}  let lastStatus=null;+let curTarget=localStorage.getItem('fdTarget')||'';+let targetsCache=[];+function statusUrl(){return 'api/status'+(curTarget?('?target='+encodeURIComponent(curTarget)):'');}+async function loadTargets(){+  try{+    const d=await api('api/targets');+    targetsCache=d.targets||[];+    const el=$('#targets');el.innerHTML='';+    if(targetsCache.length>1){$('#seatnote').style.display='';}+    if(!curTarget&&targetsCache.length){curTarget=(targetsCache.find(t=>t.running)||targetsCache[0]).name;localStorage.setItem('fdTarget',curTarget);}+    for(const t of targetsCache){+      const b=document.createElement('button');b.className='tpill'+(t.name===curTarget?' active':'');+      b.setAttribute('data-tip',(t.running?'Fusion is running here':'Fusion is closed here')+(t.lastReadyAt?(' - last ready '+t.lastReadyAt):''));+      b.innerHTML='<span class="tdot'+(t.running?' on':'')+'"></span>'+t.name;+      b.onclick=()=>{curTarget=t.name;localStorage.setItem('fdTarget',curTarget);loadTargets();refresh();toast('switched to '+t.name)};+      el.appendChild(b);+    }+  }catch(e){}+} function renderLeds(st){   const g=$('#ledGrid');g.innerHTML='';   const leds=st.leds||{};@@ -159,7 +186,7 @@ function renderLeds(st){   const keys=ORDER.filter(k=>leds[k]).concat(Object.keys(leds).filter(k=>!ORDER.includes(k)));   for(const key of keys){     const l=leds[key];-    const cls=l.pending?'warn':(l.on?'on':'off');+    const cls=l.pending?'warn':(l.state==='dormant'?'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);@@ -167,7 +194,7 @@ function renderLeds(st){   $('#subject').textContent=st.target?('target: '+st.target):'no bridge target';   $('#meta').textContent='bridge '+(st.bridgeVersion||'?')+' | dashboard '+(st.dashboardVersion||''); }-async function refresh(){try{lastStatus=await api('api/status');renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}}+async function refresh(){try{lastStatus=await api(statusUrl());renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}} $('#btnRefresh').onclick=()=>{toast('refreshing status');refresh();}; $('#btnStart').onclick=async()=>{toast('starting Fusion in the background');await act('start');setTimeout(refresh,4000);}; $('#btnStop').onclick=async()=>{toast('closing Fusion');await act('stop');setTimeout(refresh,4000);};@@ -214,10 +241,11 @@ const SETUP_ORDER=[   {led:'aps',action:'aps_signin',label:'Set up APS cloud search'},   {led:'mcp',action:'mcp_enable',label:'Enable the Fusion MCP server'}, ];-function setupPending(){if(!lastStatus)return[];return SETUP_ORDER.filter(s=>{const l=lastStatus.leds[s.led];return l&&!l.on;});}+function setupPending(){if(!lastStatus)return[];return SETUP_ORDER.filter(s=>{const l=lastStatus.leds[s.led];return l&&!l.on&&l.state!=='dormant';});} function renderSetup(){   const p=setupPending();-  $('#setupBody').innerHTML=p.length?('Remaining steps:<br>'+p.map((s,i)=>`${i+1}. ${s.label}`).join('<br>')):'Everything is configured. You are ready to go.';+  const dormant=lastStatus&&Object.values(lastStatus.leds).some(l=>l.state==='dormant');+  $('#setupBody').innerHTML=p.length?('Remaining steps:<br>'+p.map((s,i)=>`${i+1}. ${s.label}`).join('<br>')):(dormant?'Everything is configured - Fusion just is not running on '+(curTarget||'this box')+'. Start Fusion (or start it on another box; the Autodesk seat follows automatically).':'Everything is configured. You are ready to go.');   $('#btnSetupNext').style.display=p.length?'':'none'; } $('#btnSetup').onclick=async()=>{await refresh();renderSetup();$('#dlgSetup').showModal();};@@ -296,7 +324,7 @@ brand.addEventListener('mouseleave',()=>{clearTimeout(aiShow);aiHide=setTimeout( ai.addEventListener('mouseenter',()=>clearTimeout(aiHide)); ai.addEventListener('mouseleave',()=>{aiHide=setTimeout(()=>ai.classList.remove('show'),220);}); -refresh();setInterval(refresh,15000);+loadTargets().then(refresh);setInterval(refresh,15000);setInterval(loadTargets,60000); </script> </body> </html>
fusion_detect.py+26
@@ -732,6 +732,17 @@ _ENVELOPE_COUNT = 0 _ENVELOPE_SAVED = {}  +def _fusion_main_hwnd_quick():+    """Largest titled family window's hwnd, or None - cheap, for the taskbar badge."""+    try:+        wins = family_windows() or []+        if not wins:+            return None+        return max(wins, key=lambda w: w.get("width", 0) * w.get("height", 0)).get("hwnd")+    except Exception:+        return None++ def begin_background_envelope(seconds: float, minimize: bool = False):     """Guard the next `seconds` against Fusion-family focus theft. Fire-and-forget."""     global _ENVELOPE_COUNT@@ -752,6 +763,16 @@ def begin_background_envelope(seconds: float, minimize: bool = False):                     _ENVELOPE_SAVED["lsfw"] = bool(ctypes.windll.user32.LockSetForegroundWindow(1))                 except Exception:                     _ENVELOPE_SAVED["lsfw"] = False+                # Taskbar badge (John, 2026-08-17): while Adom drives Fusion, its taskbar+                # button wears the Adom mark - the user can glance down and know WHY the+                # app is busy. Set on first envelope, cleared when the refcount drains.+                try:+                    from handlers.taskbar_overlay import set_driving_badge+                    hw = _fusion_main_hwnd_quick()+                    if hw:+                        set_driving_badge(hw)+                except Exception:+                    pass         threading.Thread(target=_envelope_watch, args=(seconds, minimize), daemon=True).start()     except Exception:         pass@@ -842,6 +863,11 @@ def _envelope_watch(seconds: float, minimize: bool):                         ctypes.windll.user32.LockSetForegroundWindow(2)                     except Exception:                         pass+                try:+                    from handlers.taskbar_overlay import clear_driving_badge+                    clear_driving_badge()+                except Exception:+                    pass   def _focus_guard(seconds: float = 150.0, restore_fg_lock=None, restore_min_anim=None):
handlers/taskbar_overlay.pyadded+119
@@ -0,0 +1,119 @@+"""Taskbar overlay badge: "Adom is driving Fusion" (John, 2026-08-17).++pup badges its own Chrome taskbar buttons to show state; the same signal belongs on+FUSION's taskbar button whenever this bridge is driving it, so the user can glance at+the taskbar and know why Fusion is busy. Windows exposes exactly this as+ITaskbarList3::SetOverlayIcon - callable cross-process with just an hwnd, no window+ownership needed. ctypes COM, no dependencies.++Wired to the background envelope's refcount: first envelope (driving starts) badges the+Fusion main window with resources/adom-overlay.ico ("Adom is driving Fusion"); when the+refcount drains to zero the badge clears. Best-effort everywhere - a missing icon, a+COM failure, or a dead hwnd must never break a verb.+"""++import ctypes+import os+import threading++_LOCK = threading.Lock()+_HICON = None+_BADGED_HWND = None++_CLSID_TaskbarList = "{56FDF344-FD6D-11D0-958A-006097C9A090}"+_IID_ITaskbarList3 = "{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}"+# ITaskbarList3 vtable: IUnknown 0-2 | HrInit 3, AddTab 4, DeleteTab 5, ActivateTab 6,+# SetActiveAlt 7 | MarkFullscreenWindow 8 | SetProgressValue 9, SetProgressState 10,+# RegisterTab 11, UnregisterTab 12, SetTabOrder 13, SetTabActive 14, ThumbBarAddButtons 15,+# ThumbBarUpdateButtons 16, ThumbBarSetImageList 17, SetOverlayIcon 18, ...+_VTBL_HRINIT = 3+_VTBL_SETOVERLAYICON = 18+++class _GUID(ctypes.Structure):+    _fields_ = [("Data1", ctypes.c_ulong), ("Data2", ctypes.c_ushort),+                ("Data3", ctypes.c_ushort), ("Data4", ctypes.c_ubyte * 8)]+++def _guid(s: str) -> "_GUID":+    g = _GUID()+    ctypes.oledll.ole32.CLSIDFromString(ctypes.c_wchar_p(s), ctypes.byref(g))+    return g+++def _icon_path() -> str:+    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),+                        "resources", "adom-overlay.ico")+++def _load_icon():+    global _HICON+    if _HICON:+        return _HICON+    p = _icon_path()+    if not os.path.isfile(p):+        return None+    IMAGE_ICON, LR_LOADFROMFILE = 1, 0x0010+    h = ctypes.windll.user32.LoadImageW(None, p, IMAGE_ICON, 32, 32, LR_LOADFROMFILE)+    _HICON = h or None+    return _HICON+++def _taskbar_call(hwnd, hicon, description):+    """CoCreate ITaskbarList3 on THIS thread and call SetOverlayIcon once."""+    ole32 = ctypes.oledll.ole32+    ole32.CoInitialize(None)+    try:+        clsid = _guid(_CLSID_TaskbarList)+        iid = _guid(_IID_ITaskbarList3)+        ptr = ctypes.c_void_p()+        CLSCTX_INPROC_SERVER = 1+        ole32.CoCreateInstance(ctypes.byref(clsid), None, CLSCTX_INPROC_SERVER,+                               ctypes.byref(iid), ctypes.byref(ptr))+        vtbl = ctypes.cast(ptr, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p))).contents+        proto_init = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p)+        proto_overlay = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p,+                                           ctypes.c_void_p, ctypes.c_void_p, ctypes.c_wchar_p)+        proto_release = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)+        hr_init = proto_init(vtbl[_VTBL_HRINIT])(ptr)+        hr = proto_overlay(vtbl[_VTBL_SETOVERLAYICON])(+            ptr, ctypes.c_void_p(hwnd), ctypes.c_void_p(hicon or 0),+            description if description else None)+        proto_release(vtbl[2])(ptr)+        return hr == 0 and hr_init == 0+    finally:+        ole32.CoUninitialize()+++def set_driving_badge(hwnd) -> bool:+    """Badge the window's taskbar button with the Adom mark ('Adom is driving Fusion')."""+    global _BADGED_HWND+    try:+        if not hwnd:+            return False+        with _LOCK:+            hicon = _load_icon()+            if not hicon:+                return False+            ok = _taskbar_call(int(hwnd), hicon, "Adom is driving Fusion")+            if ok:+                _BADGED_HWND = int(hwnd)+            return bool(ok)+    except Exception:+        return False+++def clear_driving_badge(hwnd=None) -> bool:+    """Remove the badge (from the given hwnd, or whichever window we badged last)."""+    global _BADGED_HWND+    try:+        with _LOCK:+            target = int(hwnd) if hwnd else _BADGED_HWND+            if not target:+                return False+            ok = _taskbar_call(target, None, None)+            if ok and (hwnd is None or _BADGED_HWND == int(hwnd)):+                _BADGED_HWND = None+            return bool(ok)+    except Exception:+        return False
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.46"+DASH_VERSION="1.9.53" 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.46",+  "version": "1.9.53",   "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.46",+  "version": "1.9.53",   "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
resources/adom-overlay.icoadded
⋯ 1 unchanged line ⋯
server.py+26
@@ -1529,6 +1529,22 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     if needs_signin:         result["needsSignin"] = True         result["statusVerb"] = "fusion_readiness"+    # LAST-KNOWN state (John, 2026-08-17: "shouldn't i be fully setup already other than+    # fusion running?"): sign-in/add-in state is UNKNOWABLE while Fusion is closed, and a+    # dashboard that paints "unknown" as broken-red misreads a fully-configured box. So+    # persist the good observations and always report them: a closed Fusion with+    # lastSignedInAt set = "configured, dormant", not "needs setup".+    try:+        import datetime as _dt+        _now_iso = _dt.datetime.now().isoformat(timespec="seconds")+        if ready and running:+            _set_bridge_pref("lastReadyAt", _now_iso)+        if running and not needs_signin and ready:+            _set_bridge_pref("lastSignedInAt", _now_iso)+        result["lastReadyAt"] = _get_bridge_pref("lastReadyAt", None)+        result["lastSignedInAt"] = _get_bridge_pref("lastSignedInAt", None)+    except Exception:+        pass     if licensing:         result["licensingDialog"] = True         result["statusVerb"] = "fusion_readiness"@@ -4627,7 +4643,12 @@ def _handle_mcp_status(fusion_info: dict, args: dict) -> dict:     """Is Autodesk's Fusion MCP server up, and what does it expose?"""     s = _mcp_new_session()     if s.get("error"):+        # Enabled-ness is a persistent Fusion PREFERENCE, but it is only observable while+        # Fusion runs. Report the last GOOD observation so a closed Fusion reads as+        # "configured, dormant" instead of broken (John, 2026-08-17).         return {"success": True, "enabled": False, "url": _MCP_URL, "error": s.get("error"),+                "lastEnabledAt": _get_bridge_pref("mcpLastEnabledAt", None),+                "fusionRunning": _is_fusion_running(),                 "_hint": _mcp_off_hint(), "statusVerb": "fusion_mcp_status"}     tools = []     tl = _mcp_rpc("tools/list")@@ -4635,6 +4656,11 @@ def _handle_mcp_status(fusion_info: dict, args: dict) -> dict:         tools = [{"name": t.get("name"),                   "description": (t.get("description") or "").split("\n")[0][:160]}                  for t in ((tl.get("result") or {}).get("tools") or [])]+    try:+        import datetime as _dt+        _set_bridge_pref("mcpLastEnabledAt", _dt.datetime.now().isoformat(timespec="seconds"))+    except Exception:+        pass     return {"success": True, "enabled": True, "url": _MCP_URL,             "sessionId": s.get("sessionId"), "serverInfo": s.get("serverInfo"),             "toolCount": len(tools), "tools": tools,
tools/release-files.txt+2
@@ -53,3 +53,5 @@ resources/samples/ADS8588SIPM.step server.py start.bat uninstall.sh+handlers/taskbar_overlay.py+resources/adom-overlay.ico