Browse Source

Fix filament fill level display bugs on printer card (#496)

Three related fixes in PrintersPage.tsx:

1. External spool (vt_tray) fill level was missing the AMS remain
   fallback — only checked Spoolman/inventory, ignoring valid AMS data.
   Now uses the same fallback chain as regular and AMS-HT slots.

2. When fill was unknown (null), slot visual showed a full-width gray
   bar ("full") while the hover card showed "—" ("empty"). Removed the
   misleading gray fallback bar from all three slot types so both views
   consistently show "unknown".

3. Fill source priority always preferred AMS remain over Spoolman and
   inventory, even when those sources were more accurate (e.g. spools
   with usage tracking or migrated from Spoolman). Reversed priority to
   Spoolman → Inventory → AMS remain. Fixed fillSource label to reflect
   the actual data source (was always 'ams' even when using fallback).
maziggy 3 months ago
parent
commit
24c4363620

+ 1 - 0
CHANGELOG.md

@@ -11,6 +11,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **External Spool Mapping Inverted on H2C** ([#492](https://github.com/maziggy/bambuddy/issues/492)) — On H2C dual-nozzle printers, printing from the right nozzle's external spool (Ext-R) incorrectly highlighted the left external spool (Ext-L) as active. The H2C firmware reports `tray_now=254` generically for both external spools, so the frontend's direct ID comparison (`effectiveTrayNow === extTrayId`) always matched Ext-L (id=254). Now uses `active_extruder` on dual-nozzle printers to determine which external spool is active: extruder 1 (left) → Ext-L, extruder 0 (right) → Ext-R.
 - **External Spool Mapping Inverted on H2C** ([#492](https://github.com/maziggy/bambuddy/issues/492)) — On H2C dual-nozzle printers, printing from the right nozzle's external spool (Ext-R) incorrectly highlighted the left external spool (Ext-L) as active. The H2C firmware reports `tray_now=254` generically for both external spools, so the frontend's direct ID comparison (`effectiveTrayNow === extTrayId`) always matched Ext-L (id=254). Now uses `active_extruder` on dual-nozzle printers to determine which external spool is active: extruder 1 (left) → Ext-L, extruder 0 (right) → Ext-R.
 - **External Spool Assignments Lost on Restart** ([#493](https://github.com/maziggy/bambuddy/issues/493)) — Filament spool assignments on external spool holders (Ext-L / Ext-R) were silently deleted every time AMS data changed, including on container restart. The `on_ams_change` stale-assignment cleanup searched only AMS unit data for matching trays, but external spools live in `vt_tray` (a separate MQTT field). Since `_find_tray_in_ams_data` never found them, external assignments were always marked as stale and removed. Now looks up external spool assignments (`ams_id=255`) in the printer's `vt_tray` data instead, and keeps the assignment if `vt_tray` data hasn't arrived yet.
 - **External Spool Assignments Lost on Restart** ([#493](https://github.com/maziggy/bambuddy/issues/493)) — Filament spool assignments on external spool holders (Ext-L / Ext-R) were silently deleted every time AMS data changed, including on container restart. The `on_ams_change` stale-assignment cleanup searched only AMS unit data for matching trays, but external spools live in `vt_tray` (a separate MQTT field). Since `_find_tray_in_ams_data` never found them, external assignments were always marked as stale and removed. Now looks up external spool assignments (`ams_id=255`) in the printer's `vt_tray` data instead, and keeps the assignment if `vt_tray` data hasn't arrived yet.
 - **Developer Mode Detection Always Reports Null** — The MQTT `fun` field is an integer in the JSON payload, but the parser used `int(value, 16)` which requires a string argument. This raised `TypeError` on every message, silently caught by the exception handler, so `developer_mode` was never set. Now handles both integer and hex string formats.
 - **Developer Mode Detection Always Reports Null** — The MQTT `fun` field is an integer in the JSON payload, but the parser used `int(value, 16)` which requires a string argument. This raised `TypeError` on every message, silently caught by the exception handler, so `developer_mode` was never set. Now handles both integer and hex string formats.
+- **Filament Fill Level Wrong in Hover Card / Missing for External Spools** ([#496](https://github.com/maziggy/bambuddy/issues/496)) — Three related fill level display bugs on the printer card. First, external spool slots (vt_tray) were missing the AMS `remain` fallback entirely — `extEffectiveFill` only checked Spoolman and inventory, falling through to `null` even when the printer reported a valid fill percentage. Now includes the same AMS remain fallback as regular and AMS-HT slots. Second, when fill level was unknown (`null`), the AMS slot visual showed a full-width gray bar (appearing "full") while the hover card showed "—" (appearing "empty") — confusing users into thinking the printer card and hover card disagreed. Removed the misleading gray fallback bar from all three slot types; the empty fill bar track now consistently indicates "unknown" in both views. Third, the fill level priority chain always preferred AMS `remain` over Spoolman and inventory data, even when those sources were more accurate (e.g., spools migrated from Spoolman to internal inventory, or spools with accurate usage tracking). Reversed the priority to Spoolman → Inventory → AMS remain, and fixed `fillSource` to correctly reflect the actual data source used (was always reporting `'ams'` even when Spoolman or inventory provided the value via the fallback chain when `remain` was -1).
 - **File Manager Rename Doesn't Update Displayed Name** ([#460](https://github.com/maziggy/bambuddy/issues/460)) — Renaming a file in the File Manager updated the `filename` field but not `file_metadata.print_name`, which the UI uses as the primary display name. Since `print_name` is extracted from inside the 3MF at upload time, it always took precedence over the renamed `filename`. The rename endpoint now also updates `print_name` in the file metadata when present.
 - **File Manager Rename Doesn't Update Displayed Name** ([#460](https://github.com/maziggy/bambuddy/issues/460)) — Renaming a file in the File Manager updated the `filename` field but not `file_metadata.print_name`, which the UI uses as the primary display name. Since `print_name` is extracted from inside the 3MF at upload time, it always took precedence over the renamed `filename`. The rename endpoint now also updates `print_name` in the file metadata when present.
 - **Finish Photo Not Captured When Archive Has No Source 3MF** ([#484](https://github.com/maziggy/bambuddy/issues/484)) — When a print completed but the 3MF source file wasn't downloaded from the printer (e.g. FTP download failure), the archive's `file_path` was null. The finish photo capture silently skipped because it derived the save directory from `file_path`. Now falls back to `archive/{id}/` so the photo is captured regardless.
 - **Finish Photo Not Captured When Archive Has No Source 3MF** ([#484](https://github.com/maziggy/bambuddy/issues/484)) — When a print completed but the 3MF source file wasn't downloaded from the printer (e.g. FTP download failure), the archive's `file_path` was null. The finish photo capture silently skipped because it derived the save directory from `file_path`. Now falls back to `archive/{id}/` so the photo is captured regardless.
 
 

+ 25 - 30
frontend/src/pages/PrintersPage.tsx

@@ -2763,7 +2763,7 @@ function PrinterCard({
                                 // Get saved slot preset mapping (for user-configured slots)
                                 // Get saved slot preset mapping (for user-configured slots)
                                 const slotPreset = slotPresets?.[globalTrayId];
                                 const slotPreset = slotPresets?.[globalTrayId];
 
 
-                                // Fill level fallback chain: AMS remain → Spoolman → Inventory spool
+                                // Fill level fallback chain: Spoolman → Inventory → AMS remain
                                 const trayTag = tray?.tray_uuid?.toUpperCase();
                                 const trayTag = tray?.tray_uuid?.toUpperCase();
                                 const linkedSpool = trayTag ? linkedSpools?.[trayTag] : undefined;
                                 const linkedSpool = trayTag ? linkedSpools?.[trayTag] : undefined;
                                 const spoolmanFill = getSpoolmanFillLevel(linkedSpool);
                                 const spoolmanFill = getSpoolmanFillLevel(linkedSpool);
@@ -2775,12 +2775,11 @@ function PrinterCard({
                                   }
                                   }
                                   return null;
                                   return null;
                                 })();
                                 })();
-                                const effectiveFill = hasFillLevel && tray.remain > 0
-                                  ? tray.remain
-                                  : (spoolmanFill ?? inventoryFill ?? (hasFillLevel ? tray.remain : null));
-                                const fillSource = (hasFillLevel && tray.remain === 0 && (spoolmanFill !== null || inventoryFill !== null))
-                                  ? (spoolmanFill !== null ? 'spoolman' as const : 'inventory' as const)
-                                  : 'ams' as const;
+                                const effectiveFill = spoolmanFill ?? inventoryFill ?? (hasFillLevel ? tray.remain : null);
+                                const fillSource = spoolmanFill !== null ? 'spoolman' as const
+                                  : inventoryFill !== null ? 'inventory' as const
+                                  : hasFillLevel ? 'ams' as const
+                                  : undefined;
 
 
                                 // Build filament data for hover card
                                 // Build filament data for hover card
                                 const filamentData = tray?.tray_type ? {
                                 const filamentData = tray?.tray_type ? {
@@ -2817,7 +2816,7 @@ function PrinterCard({
                                     </div>
                                     </div>
                                     {/* Fill bar */}
                                     {/* Fill bar */}
                                     <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
                                     <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
-                                      {effectiveFill !== null && effectiveFill >= 0 && tray ? (
+                                      {effectiveFill !== null && effectiveFill >= 0 && tray && (
                                         <div
                                         <div
                                           className="h-full rounded-full transition-all"
                                           className="h-full rounded-full transition-all"
                                           style={{
                                           style={{
@@ -2825,9 +2824,7 @@ function PrinterCard({
                                             backgroundColor: getFillBarColor(effectiveFill),
                                             backgroundColor: getFillBarColor(effectiveFill),
                                           }}
                                           }}
                                         />
                                         />
-                                      ) : tray?.tray_type ? (
-                                        <div className="h-full w-full rounded-full bg-white/50 dark:bg-gray-500/40" />
-                                      ) : null}
+                                      )}
                                     </div>
                                     </div>
                                   </div>
                                   </div>
                                 );
                                 );
@@ -2987,7 +2984,7 @@ function PrinterCard({
                         // Get saved slot preset mapping (for user-configured slots)
                         // Get saved slot preset mapping (for user-configured slots)
                         const slotPreset = slotPresets?.[globalTrayId];
                         const slotPreset = slotPresets?.[globalTrayId];
 
 
-                        // Fill level fallback chain: AMS remain → Spoolman → Inventory spool
+                        // Fill level fallback chain: Spoolman → Inventory → AMS remain
                         const htTrayTag = tray?.tray_uuid?.toUpperCase();
                         const htTrayTag = tray?.tray_uuid?.toUpperCase();
                         const htLinkedSpool = htTrayTag ? linkedSpools?.[htTrayTag] : undefined;
                         const htLinkedSpool = htTrayTag ? linkedSpools?.[htTrayTag] : undefined;
                         const htSpoolmanFill = getSpoolmanFillLevel(htLinkedSpool);
                         const htSpoolmanFill = getSpoolmanFillLevel(htLinkedSpool);
@@ -3000,12 +2997,11 @@ function PrinterCard({
                           }
                           }
                           return null;
                           return null;
                         })();
                         })();
-                        const htEffectiveFill = hasFillLevel && tray.remain > 0
-                          ? tray.remain
-                          : (htSpoolmanFill ?? htInventoryFill ?? (hasFillLevel ? tray.remain : null));
-                        const htFillSource = (hasFillLevel && tray.remain === 0 && (htSpoolmanFill !== null || htInventoryFill !== null))
-                          ? (htSpoolmanFill !== null ? 'spoolman' as const : 'inventory' as const)
-                          : 'ams' as const;
+                        const htEffectiveFill = htSpoolmanFill ?? htInventoryFill ?? (hasFillLevel ? tray.remain : null);
+                        const htFillSource = htSpoolmanFill !== null ? 'spoolman' as const
+                          : htInventoryFill !== null ? 'inventory' as const
+                          : hasFillLevel ? 'ams' as const
+                          : undefined;
 
 
                         // Build filament data for hover card
                         // Build filament data for hover card
                         const filamentData = tray?.tray_type ? {
                         const filamentData = tray?.tray_type ? {
@@ -3043,7 +3039,7 @@ function PrinterCard({
                             </div>
                             </div>
                             {/* Fill bar */}
                             {/* Fill bar */}
                             <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
                             <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
-                              {htEffectiveFill !== null && htEffectiveFill >= 0 ? (
+                              {htEffectiveFill !== null && htEffectiveFill >= 0 && (
                                 <div
                                 <div
                                   className="h-full rounded-full transition-all"
                                   className="h-full rounded-full transition-all"
                                   style={{
                                   style={{
@@ -3051,9 +3047,7 @@ function PrinterCard({
                                     backgroundColor: getFillBarColor(htEffectiveFill),
                                     backgroundColor: getFillBarColor(htEffectiveFill),
                                   }}
                                   }}
                                 />
                                 />
-                              ) : tray?.tray_type ? (
-                                <div className="h-full w-full rounded-full bg-white/50 dark:bg-gray-500/40" />
-                              ) : null}
+                              )}
                             </div>
                             </div>
                           </div>
                           </div>
                         );
                         );
@@ -3264,7 +3258,12 @@ function PrinterCard({
                                 }
                                 }
                                 return null;
                                 return null;
                               })();
                               })();
-                              const extEffectiveFill = extSpoolmanFill ?? extInventoryFill ?? null;
+                              const extHasFillLevel = extTray.tray_type && extTray.remain >= 0;
+                              const extEffectiveFill = extSpoolmanFill ?? extInventoryFill ?? (extHasFillLevel ? extTray.remain : null);
+                              const extFillSource = extSpoolmanFill !== null ? 'spoolman' as const
+                                : extInventoryFill !== null ? 'inventory' as const
+                                : extHasFillLevel ? 'ams' as const
+                                : undefined;
 
 
                               const extFilamentData = {
                               const extFilamentData = {
                                 vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
                                 vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
@@ -3275,9 +3274,7 @@ function PrinterCard({
                                 fillLevel: extEffectiveFill,
                                 fillLevel: extEffectiveFill,
                                 trayUuid: extTray.tray_uuid || null,
                                 trayUuid: extTray.tray_uuid || null,
                                 tagUid: extTray.tag_uid || null,
                                 tagUid: extTray.tag_uid || null,
-                                fillSource: extSpoolmanFill !== null ? 'spoolman' as const
-                                  : extInventoryFill !== null ? 'inventory' as const
-                                  : undefined,
+                                fillSource: extFillSource,
                               };
                               };
 
 
                               const isEmpty = !extTray.tray_type;
                               const isEmpty = !extTray.tray_type;
@@ -3295,7 +3292,7 @@ function PrinterCard({
                                     {extTray.tray_type || '—'}
                                     {extTray.tray_type || '—'}
                                   </div>
                                   </div>
                                   <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
                                   <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
-                                    {extEffectiveFill !== null && extEffectiveFill >= 0 && !isEmpty ? (
+                                    {extEffectiveFill !== null && extEffectiveFill >= 0 && !isEmpty && (
                                       <div
                                       <div
                                         className="h-full rounded-full transition-all"
                                         className="h-full rounded-full transition-all"
                                         style={{
                                         style={{
@@ -3303,9 +3300,7 @@ function PrinterCard({
                                           backgroundColor: getFillBarColor(extEffectiveFill),
                                           backgroundColor: getFillBarColor(extEffectiveFill),
                                         }}
                                         }}
                                       />
                                       />
-                                    ) : !isEmpty ? (
-                                      <div className="h-full w-full rounded-full bg-white/50 dark:bg-gray-500/40" />
-                                    ) : null}
+                                    )}
                                   </div>
                                   </div>
                                   {extLabel && <div className="text-[7px] text-white/40 mt-0.5 truncate">{extLabel}</div>}
                                   {extLabel && <div className="text-[7px] text-white/40 mt-0.5 truncate">{extLabel}</div>}
                                 </div>
                                 </div>

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-1Ts9jjQl.css


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


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
 
     <!-- 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-VMGdyBYb.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BU6YJVpb.css">
+    <script type="module" crossorigin src="/assets/index-DHMy9Wxo.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-1Ts9jjQl.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

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