Browse Source

Fix AMS fill level showing 0% for non-Viewer users (#676)

  When inventory spool assignments had stale weight_used values, the fill
  level fallback chain (Spoolman → Inventory → AMS remain) used nullish
  coalescing (??), which doesn't fall through on 0. A stale inventory
  fill of 0% permanently shadowed the correct real-time AMS remain value.
  Viewer users were unaffected because their group lacked the
  inventory:view_assignments permission, so the query never fired.

  Now when inventory says 0% but AMS hardware reports positive remain,
  the inventory value is bypassed in favor of the live AMS data.
maziggy 2 months ago
parent
commit
adb0500e67

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Prometheus Build Info Metric** ([#633](https://github.com/maziggy/bambuddy/pull/633)) — Added a `bambuddy_build_info` gauge metric to the Prometheus metrics endpoint, exposing the application version, Python version, platform, and architecture as labels. Follows the standard Prometheus `_build_info` convention for dashboards and version-change alerting. Contributed by @sw1nn.
 - **Prometheus Build Info Metric** ([#633](https://github.com/maziggy/bambuddy/pull/633)) — Added a `bambuddy_build_info` gauge metric to the Prometheus metrics endpoint, exposing the application version, Python version, platform, and architecture as labels. Follows the standard Prometheus `_build_info` convention for dashboards and version-change alerting. Contributed by @sw1nn.
 
 
 ### Fixed
 ### Fixed
+- **AMS Fill Level Shows 0% for Non-Viewer Users** ([#676](https://github.com/maziggy/bambuddy/issues/676)) — When authentication was enabled with advanced permissions, users with `inventory:view_assignments` permission saw 0% fill level on AMS slots where inventory spool data had stale `weight_used` values. The fill level fallback chain (Spoolman → Inventory → AMS remain) used nullish coalescing (`??`), which doesn't fall through on `0` — so a stale inventory fill of 0% permanently shadowed the correct real-time AMS remain value from the printer. Now, when inventory says 0% but the AMS hardware reports a positive remain, the inventory value is bypassed in favor of the live AMS data. Viewer users were unaffected because their group lacked `inventory:view_assignments`, so the inventory query never fired and the AMS remain was used directly. Reported by @cadtoolbox.
 - **Virtual Printer Proxy Mode Always Shows X1C Model** — Creating a virtual printer in Proxy mode always set the model to X1C regardless of the destination printer, because the frontend hides the model dropdown in proxy mode and the backend defaulted to X1C. Now auto-inherits the model from the target printer when creating or updating a proxy virtual printer (e.g. a proxy pointing at a P1S correctly presents itself as P1S to the slicer). The model also auto-updates when changing the target printer or switching to proxy mode.
 - **Virtual Printer Proxy Mode Always Shows X1C Model** — Creating a virtual printer in Proxy mode always set the model to X1C regardless of the destination printer, because the frontend hides the model dropdown in proxy mode and the backend defaulted to X1C. Now auto-inherits the model from the target printer when creating or updating a proxy virtual printer (e.g. a proxy pointing at a P1S correctly presents itself as P1S to the slicer). The model also auto-updates when changing the target printer or switching to proxy mode.
 - **Cloud Profiles Shared Across All Users** ([#665](https://github.com/maziggy/bambuddy/issues/665)) — When authentication was enabled, Bambu Cloud credentials were stored globally — one account per Bambuddy instance. If User A logged into Cloud, every other user saw User A's account and profiles. User B logging in would overwrite User A's credentials. Cloud credentials are now stored per-user: each user logs into their own Bambu Cloud account independently. When auth is disabled (single-user mode), behavior is unchanged. Also fixed cloud data endpoints (`/cloud/settings`, `/cloud/fields`, preset CRUD) requiring `settings:read` / `settings:update` permissions instead of `cloud:auth` — users who had "Cloud Auth" enabled but "Settings" disabled couldn't load profiles after logging in. Reported by @cadtoolbox.
 - **Cloud Profiles Shared Across All Users** ([#665](https://github.com/maziggy/bambuddy/issues/665)) — When authentication was enabled, Bambu Cloud credentials were stored globally — one account per Bambuddy instance. If User A logged into Cloud, every other user saw User A's account and profiles. User B logging in would overwrite User A's credentials. Cloud credentials are now stored per-user: each user logs into their own Bambu Cloud account independently. When auth is disabled (single-user mode), behavior is unchanged. Also fixed cloud data endpoints (`/cloud/settings`, `/cloud/fields`, preset CRUD) requiring `settings:read` / `settings:update` permissions instead of `cloud:auth` — users who had "Cloud Auth" enabled but "Settings" disabled couldn't load profiles after logging in. Reported by @cadtoolbox.
 - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer.
 - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer.

+ 3 - 1
frontend/src/components/spoolbuddy/AmsUnitCard.tsx

@@ -147,7 +147,9 @@ function SpoolSlot({ tray, slotIndex, isActive, fillOverride, onClick }: SpoolSl
   const isEmpty = isTrayEmpty(tray);
   const isEmpty = isTrayEmpty(tray);
   const color = trayColorToCSS(tray.tray_color);
   const color = trayColorToCSS(tray.tray_color);
   const amsFill = tray.remain !== null && tray.remain !== undefined && tray.remain >= 0 ? tray.remain : null;
   const amsFill = tray.remain !== null && tray.remain !== undefined && tray.remain >= 0 ? tray.remain : null;
-  const effectiveFill = fillOverride ?? amsFill;
+  // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
+  const resolvedOverride = (fillOverride === 0 && amsFill !== null && amsFill > 0) ? null : fillOverride;
+  const effectiveFill = resolvedOverride ?? amsFill;
 
 
   return (
   return (
     <div
     <div

+ 16 - 6
frontend/src/pages/PrintersPage.tsx

@@ -3182,9 +3182,13 @@ function PrinterCard({
                                   }
                                   }
                                   return null;
                                   return null;
                                 })();
                                 })();
-                                const effectiveFill = spoolmanFill ?? inventoryFill ?? (hasFillLevel ? tray.remain : null);
+                                // If inventory says 0% but AMS reports positive remain, prefer AMS
+                                // (inventory weight_used may be stale or over-counted — #676)
+                                const resolvedInventoryFill = (inventoryFill === 0 && hasFillLevel && tray.remain > 0)
+                                  ? null : inventoryFill;
+                                const effectiveFill = spoolmanFill ?? resolvedInventoryFill ?? (hasFillLevel ? tray.remain : null);
                                 const fillSource = spoolmanFill !== null ? 'spoolman' as const
                                 const fillSource = spoolmanFill !== null ? 'spoolman' as const
-                                  : inventoryFill !== null ? 'inventory' as const
+                                  : resolvedInventoryFill !== null ? 'inventory' as const
                                   : hasFillLevel ? 'ams' as const
                                   : hasFillLevel ? 'ams' as const
                                   : undefined;
                                   : undefined;
 
 
@@ -3406,9 +3410,12 @@ function PrinterCard({
                           }
                           }
                           return null;
                           return null;
                         })();
                         })();
-                        const htEffectiveFill = htSpoolmanFill ?? htInventoryFill ?? (hasFillLevel ? tray.remain : null);
+                        // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
+                        const htResolvedInventoryFill = (htInventoryFill === 0 && hasFillLevel && tray.remain > 0)
+                          ? null : htInventoryFill;
+                        const htEffectiveFill = htSpoolmanFill ?? htResolvedInventoryFill ?? (hasFillLevel ? tray.remain : null);
                         const htFillSource = htSpoolmanFill !== null ? 'spoolman' as const
                         const htFillSource = htSpoolmanFill !== null ? 'spoolman' as const
-                          : htInventoryFill !== null ? 'inventory' as const
+                          : htResolvedInventoryFill !== null ? 'inventory' as const
                           : hasFillLevel ? 'ams' as const
                           : hasFillLevel ? 'ams' as const
                           : undefined;
                           : undefined;
 
 
@@ -3732,9 +3739,12 @@ function PrinterCard({
                                 return null;
                                 return null;
                               })();
                               })();
                               const extHasFillLevel = extTray.tray_type && extTray.remain >= 0;
                               const extHasFillLevel = extTray.tray_type && extTray.remain >= 0;
-                              const extEffectiveFill = extSpoolmanFill ?? extInventoryFill ?? (extHasFillLevel ? extTray.remain : null);
+                              // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
+                              const extResolvedInventoryFill = (extInventoryFill === 0 && extHasFillLevel && extTray.remain > 0)
+                                ? null : extInventoryFill;
+                              const extEffectiveFill = extSpoolmanFill ?? extResolvedInventoryFill ?? (extHasFillLevel ? extTray.remain : null);
                               const extFillSource = extSpoolmanFill !== null ? 'spoolman' as const
                               const extFillSource = extSpoolmanFill !== null ? 'spoolman' as const
-                                : extInventoryFill !== null ? 'inventory' as const
+                                : extResolvedInventoryFill !== null ? 'inventory' as const
                                 : extHasFillLevel ? 'ams' as const
                                 : extHasFillLevel ? 'ams' as const
                                 : undefined;
                                 : undefined;
 
 

+ 6 - 2
frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx

@@ -228,6 +228,8 @@ export function SpoolBuddyAmsPage() {
       };
       };
       const invFill = fillOverrides[`${unit.id}-0`] ?? null;
       const invFill = fillOverrides[`${unit.id}-0`] ?? null;
       const amsFill = tray.remain != null && tray.remain >= 0 ? tray.remain : null;
       const amsFill = tray.remain != null && tray.remain >= 0 ? tray.remain : null;
+      // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
+      const resolvedInvFill = (invFill === 0 && amsFill !== null && amsFill > 0) ? null : invFill;
       items.push({
       items.push({
         key: `ht-${unit.id}`,
         key: `ht-${unit.id}`,
         label: getAmsName(unit.id),
         label: getAmsName(unit.id),
@@ -237,7 +239,7 @@ export function SpoolBuddyAmsPage() {
         temp: unit.temp,
         temp: unit.temp,
         humidity: unit.humidity,
         humidity: unit.humidity,
         nozzleSide: getNozzleSide(unit.id),
         nozzleSide: getNozzleSide(unit.id),
-        effectiveFill: invFill ?? amsFill,
+        effectiveFill: resolvedInvFill ?? amsFill,
         onClick: () => handleAmsSlotClick(unit.id, 0, isTrayEmpty(tray) ? null : tray),
         onClick: () => handleAmsSlotClick(unit.id, 0, isTrayEmpty(tray) ? null : tray),
       });
       });
     }
     }
@@ -253,6 +255,8 @@ export function SpoolBuddyAmsPage() {
       const extSlotTrayId = extTrayId - 254;
       const extSlotTrayId = extTrayId - 254;
       const extInvFill = fillOverrides[`255-${extSlotTrayId}`] ?? null;
       const extInvFill = fillOverrides[`255-${extSlotTrayId}`] ?? null;
       const extAmsFill = extTray.remain != null && extTray.remain >= 0 ? extTray.remain : null;
       const extAmsFill = extTray.remain != null && extTray.remain >= 0 ? extTray.remain : null;
+      // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
+      const extResolvedInvFill = (extInvFill === 0 && extAmsFill !== null && extAmsFill > 0) ? null : extInvFill;
       items.push({
       items.push({
         key: `ext-${extTrayId}`,
         key: `ext-${extTrayId}`,
         label: isDualNozzle
         label: isDualNozzle
@@ -262,7 +266,7 @@ export function SpoolBuddyAmsPage() {
         isEmpty: isTrayEmpty(extTray),
         isEmpty: isTrayEmpty(extTray),
         isActive: isExtActive,
         isActive: isExtActive,
         nozzleSide: null,
         nozzleSide: null,
-        effectiveFill: extInvFill ?? extAmsFill,
+        effectiveFill: extResolvedInvFill ?? extAmsFill,
         onClick: () => handleExtSlotClick(extTray),
         onClick: () => handleExtSlotClick(extTray),
       });
       });
     }
     }

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-C7vUGUA4.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-COExSGYQ.js"></script>
+    <script type="module" crossorigin src="/assets/index-C7vUGUA4.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index--YKaUCwD.css">
     <link rel="stylesheet" crossorigin href="/assets/index--YKaUCwD.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff