Browse Source

Fix "Clear Plate" button shown on printers without matching filament color (#486)

When a print was queued to "any printer" with a filament color override
(e.g., white PETG), the "Clear Plate & Start Next" button appeared on
printers that had the right type but wrong color. The backend scheduler
already rejected color mismatches, but the frontend only checked filament
types. Now passes loaded type+color pairs from AMS status to the widget
and filters against filament_overrides, mirroring the backend's
_count_override_color_matches() logic.
maziggy 3 months ago
parent
commit
ad6108c8a7

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.2.1] - Unreleased
 
 ### Fixed
+- **"Clear Plate & Start Next" Ignores Filament Override Color** ([#486](https://github.com/maziggy/bambuddy/issues/486)) — When a print was queued to "any printer" with a filament color override (e.g., white PETG), the "Clear Plate & Start Next" button appeared on all printers of the matching model that had the correct filament *type*, regardless of *color*. A printer with blue PETG would show the button for a white PETG job. The backend scheduler already correctly rejected color mismatches, but the frontend `PrinterQueueWidget` only checked `required_filament_types` (type only) and ignored `filament_overrides` (type + color). Now passes loaded filament type+color pairs from AMS/vt_tray status to the widget and filters queue items against override colors, mirroring the backend's `_count_override_color_matches()` logic.
 - **Queue Empty After Container Restart Due to Uncheckpointed WAL** ([#523](https://github.com/maziggy/bambuddy/issues/523)) — The print queue appeared empty after a Docker container restart until a filter was applied. SQLite WAL mode keeps uncommitted data in a separate `-wal` file, but the shutdown handler never checkpointed the WAL back into the main database or disposed of engine connections. If the container was stopped or crashed, the WAL could contain partial schema migrations or uncommitted data, causing inconsistent query results on restart. Deleting the `-wal` and `-shm` files was the only workaround. Now runs `PRAGMA wal_checkpoint(TRUNCATE)` and disposes the engine on shutdown, ensuring all data is flushed to the main database file before exit.
 - **Virtual Printer Queue Sends Wrong Plate ID and Ignores AMS Mapping** ([#529](https://github.com/maziggy/bambuddy/issues/529)) — Files sent to a virtual printer in queue mode had two issues. First, `plate_id` was always `1`, generating the wrong MQTT gcode path for multi-plate 3MF files (HMS error 0500_4003). Now extracts the plate index from the 3MF's `slice_info.config`. Second, `ams_mapping` was never computed for printer-specific queue items (VP assigned to a particular printer), so the printer always used the first AMS slot regardless of which filament the 3MF required. The scheduler now computes AMS mapping for all queue items that lack one, not just model-based assignments.
 - **Unnecessary Target Model Selector on "Any" Tab** ([#528](https://github.com/maziggy/bambuddy/issues/528)) — When scheduling a print to "Any {model}", a redundant "Target Model" dropdown appeared even though the G-code is already sliced for a specific printer model. Changing the target model would lead to print failures. The dropdown is now hidden when the sliced model is known (the tab label already shows "Any {model}"). It still appears as a fallback for legacy files without model metadata.

+ 165 - 0
frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx

@@ -363,4 +363,169 @@ describe('PrinterQueueWidget - Clear Plate', () => {
       });
     });
   });
+
+  describe('filament override color filtering', () => {
+    const whitePetgOverrideItem = [
+      {
+        id: 20,
+        printer_id: null,
+        archive_id: 20,
+        position: 1,
+        status: 'pending',
+        archive_name: 'White PETG Print',
+        printer_name: null,
+        print_time_seconds: 3600,
+        scheduled_time: null,
+        required_filament_types: ['PETG'],
+        filament_overrides: [{ slot_id: 1, type: 'PETG', color: '#FFFFFF' }],
+      },
+    ];
+
+    it('hides widget when override color does not match loaded filaments', async () => {
+      server.use(
+        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
+      );
+
+      const { container } = render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilamentTypes={new Set(['PETG'])}
+          loadedFilaments={new Set(['PETG:0000ff'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(container.querySelector('button')).not.toBeInTheDocument();
+      });
+      expect(screen.queryByText('White PETG Print')).not.toBeInTheDocument();
+    });
+
+    it('shows widget when override color matches loaded filaments', async () => {
+      server.use(
+        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
+      );
+
+      render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilamentTypes={new Set(['PETG'])}
+          loadedFilaments={new Set(['PETG:ffffff'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('White PETG Print')).toBeInTheDocument();
+        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
+      });
+    });
+
+    it('normalizes override color format (strips # and lowercases)', async () => {
+      const upperCaseColorItem = [
+        {
+          id: 21,
+          printer_id: null,
+          archive_id: 21,
+          position: 1,
+          status: 'pending',
+          archive_name: 'Red PLA Print',
+          printer_name: null,
+          print_time_seconds: 3600,
+          scheduled_time: null,
+          required_filament_types: ['PLA'],
+          filament_overrides: [{ slot_id: 1, type: 'PLA', color: '#FF0000' }],
+        },
+      ];
+
+      server.use(
+        http.get('/api/v1/queue/', () => HttpResponse.json(upperCaseColorItem))
+      );
+
+      render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilamentTypes={new Set(['PLA'])}
+          loadedFilaments={new Set(['PLA:ff0000'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('Red PLA Print')).toBeInTheDocument();
+      });
+    });
+
+    it('shows widget when no loadedFilaments prop is provided (no color filtering)', async () => {
+      server.use(
+        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
+      );
+
+      render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilamentTypes={new Set(['PETG'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('White PETG Print')).toBeInTheDocument();
+      });
+    });
+
+    it('shows widget when queue item has no filament overrides', async () => {
+      // Default mockQueueItems have no filament_overrides
+      render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilaments={new Set(['PLA:000000'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('First Print')).toBeInTheDocument();
+      });
+    });
+
+    it('matches any override when multiple overrides exist', async () => {
+      const multiOverrideItem = [
+        {
+          id: 22,
+          printer_id: null,
+          archive_id: 22,
+          position: 1,
+          status: 'pending',
+          archive_name: 'Multi Color Print',
+          printer_name: null,
+          print_time_seconds: 3600,
+          scheduled_time: null,
+          required_filament_types: ['PLA'],
+          filament_overrides: [
+            { slot_id: 1, type: 'PLA', color: '#FF0000' },
+            { slot_id: 2, type: 'PLA', color: '#00FF00' },
+          ],
+        },
+      ];
+
+      server.use(
+        http.get('/api/v1/queue/', () => HttpResponse.json(multiOverrideItem))
+      );
+
+      // Printer has green PLA but not red — should still match (at least one override)
+      render(
+        <PrinterQueueWidget
+          printerId={1}
+          printerState="FINISH"
+          loadedFilamentTypes={new Set(['PLA'])}
+          loadedFilaments={new Set(['PLA:00ff00'])}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('Multi Color Print')).toBeInTheDocument();
+      });
+    });
+  });
 });

+ 20 - 5
frontend/src/components/PrinterQueueWidget.tsx

@@ -13,9 +13,10 @@ interface PrinterQueueWidgetProps {
   printerState?: string | null;
   plateCleared?: boolean;
   loadedFilamentTypes?: Set<string>;
+  loadedFilaments?: Set<string>;  // "TYPE:rrggbb" pairs for filament override color matching
 }
 
-export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared, loadedFilamentTypes }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
@@ -38,11 +39,25 @@ export function PrinterQueueWidget({ printerId, printerModel, printerState, plat
     },
   });
 
-  // Filter queue to items this printer can actually print (filament type check)
+  // Filter queue to items this printer can actually print (filament type + color check)
   const compatibleQueue = queue?.filter(item => {
-    if (!item.required_filament_types?.length) return true;
-    if (!loadedFilamentTypes?.size) return true;
-    return item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()));
+    // Type check: all required filament types must be loaded
+    if (item.required_filament_types?.length && loadedFilamentTypes?.size) {
+      if (!item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()))) {
+        return false;
+      }
+    }
+    // Color check: if filament overrides specify colors, at least one must match
+    // Mirrors backend _count_override_color_matches() logic
+    if (item.filament_overrides?.length && loadedFilaments?.size) {
+      const hasColorMatch = item.filament_overrides.some(o => {
+        const oType = (o.type || '').toUpperCase();
+        const oColor = (o.color || '').replace('#', '').toLowerCase().slice(0, 6);
+        return loadedFilaments.has(`${oType}:${oColor}`);
+      });
+      if (!hasColorMatch) return false;
+    }
+    return true;
   });
 
   const nextItem = compatibleQueue?.[0];

+ 24 - 1
frontend/src/pages/PrintersPage.tsx

@@ -1556,6 +1556,29 @@ function PrinterCard({
     return types;
   }, [status?.ams, status?.vt_tray]);
 
+  // Collect loaded filament type+color pairs for queue widget override matching
+  // Format: "TYPE:rrggbb" (e.g., "PETG:ffffff") — mirrors backend _count_override_color_matches()
+  const loadedFilaments = useMemo(() => {
+    const filaments = new Set<string>();
+    if (status?.ams) {
+      for (const ams of status.ams) {
+        for (const tray of ams.tray || []) {
+          if (tray.tray_type && tray.tray_color) {
+            const color = tray.tray_color.replace('#', '').toLowerCase().slice(0, 6);
+            filaments.add(`${tray.tray_type.toUpperCase()}:${color}`);
+          }
+        }
+      }
+    }
+    for (const vt of status?.vt_tray ?? []) {
+      if (vt.tray_type && vt.tray_color) {
+        const color = vt.tray_color.replace('#', '').toLowerCase().slice(0, 6);
+        filaments.add(`${vt.tray_type.toUpperCase()}:${color}`);
+      }
+    }
+    return filaments;
+  }, [status?.ams, status?.vt_tray]);
+
   // Fetch cloud filament info for tooltips (name includes color, also has K value)
   const { data: filamentInfo } = useQuery({
     queryKey: ['filamentInfo', trayInfoIds],
@@ -2489,7 +2512,7 @@ function PrinterCard({
                 </div>
 
                 {/* Queue Widget - always visible when there are pending items */}
-                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} loadedFilamentTypes={loadedFilamentTypes} />
+                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} loadedFilamentTypes={loadedFilamentTypes} loadedFilaments={loadedFilaments} />
               </>
             )}
 

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


+ 1 - 1
static/index.html

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

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