Procházet zdrojové kódy

fix(queue): persist clear plate state across page refresh (#410)

After clicking "Clear Plate & Start Next", refreshing the page showed
the button again because the frontend determined the state purely from
the printer's FINISH/FAILED status. The backend already tracked a
plate_cleared flag in memory but never exposed it to the frontend.

- Add plate_cleared field to PrinterStatus schema and API response
- Pass plateCleared prop to PrinterQueueWidget
- Skip clear plate UI when plate is already acknowledged
- Add 2 tests for plateCleared=true behavior
maziggy před 3 měsíci
rodič
revize
a0abeba394

+ 1 - 0
CHANGELOG.md

@@ -71,6 +71,7 @@ All notable changes to Bambuddy will be documented in this file.
 ### New Features
 ### New Features
 - **External Links: Open in New Tab** ([#338](https://github.com/maziggy/bambuddy/issues/338)) — External sidebar links can now optionally open in a new browser tab instead of an iframe. Sites behind reverse proxies (Traefik, nginx) that send `X-Frame-Options: SAMEORIGIN` or CSP `frame-ancestors` headers block iframe embedding, causing "refused to connect" errors. A new "Open in new tab" toggle in the add/edit link modal lets users choose per-link. Keyboard shortcuts (number keys) also respect the setting. Defaults to iframe (existing behavior) for backward compatibility.
 - **External Links: Open in New Tab** ([#338](https://github.com/maziggy/bambuddy/issues/338)) — External sidebar links can now optionally open in a new browser tab instead of an iframe. Sites behind reverse proxies (Traefik, nginx) that send `X-Frame-Options: SAMEORIGIN` or CSP `frame-ancestors` headers block iframe embedding, causing "refused to connect" errors. A new "Open in new tab" toggle in the add/edit link modal lets users choose per-link. Keyboard shortcuts (number keys) also respect the setting. Defaults to iframe (existing behavior) for backward compatibility.
 - **Print Queue: Clear Plate Confirmation** — When a print finishes or fails and more items are queued, the printer card now shows a "Clear Plate & Start Next" button. The scheduler no longer auto-starts the next print while the printer is in FINISH or FAILED state — the user must confirm the build plate has been cleared first. This prevents prints from starting on a dirty plate. The button respects the `printers:control` permission and is available in all supported languages (en/de/ja).
 - **Print Queue: Clear Plate Confirmation** — When a print finishes or fails and more items are queued, the printer card now shows a "Clear Plate & Start Next" button. The scheduler no longer auto-starts the next print while the printer is in FINISH or FAILED state — the user must confirm the build plate has been cleared first. This prevents prints from starting on a dirty plate. The button respects the `printers:control` permission and is available in all supported languages (en/de/ja).
+- **Clear Plate State Persists Across Page Refresh** ([#410](https://github.com/maziggy/bambuddy/issues/410)) — After clicking "Clear Plate & Start Next", refreshing the page showed the Clear Plate button again because the frontend determined the state purely from the printer's FINISH/FAILED status. The `plate_cleared` flag is now included in the printer status API response, so the widget correctly shows the passive queue link instead of the Clear Plate button after acknowledgment — even after a page refresh.
 
 
 ### Improved
 ### Improved
 - **Skip Objects: Confirmation Dialog** ([#346](https://github.com/maziggy/bambuddy/issues/346)) — Added a warning confirmation modal before skipping an object during a print. Shows the object name and warns the action is irreversible. Prevents accidentally skipping the wrong object. Translated in all 4 locales (en, de, ja, it).
 - **Skip Objects: Confirmation Dialog** ([#346](https://github.com/maziggy/bambuddy/issues/346)) — Added a warning confirmation modal before skipping an object during a print. Shows the object name and warns the action is irreversible. Prevents accidentally skipping the wrong object. Translated in all 4 locales (en, de, ja, it).

+ 1 - 0
backend/app/api/routes/printers.py

@@ -466,6 +466,7 @@ async def get_printer_status(
         big_fan2_speed=state.big_fan2_speed,
         big_fan2_speed=state.big_fan2_speed,
         heatbreak_fan_speed=state.heatbreak_fan_speed,
         heatbreak_fan_speed=state.heatbreak_fan_speed,
         firmware_version=state.firmware_version,
         firmware_version=state.firmware_version,
+        plate_cleared=printer_manager.is_plate_cleared(printer_id),
     )
     )
 
 
 
 

+ 2 - 0
backend/app/schemas/printer.py

@@ -245,3 +245,5 @@ class PrinterStatus(BaseModel):
     heatbreak_fan_speed: int | None = None  # Hotend heatbreak fan
     heatbreak_fan_speed: int | None = None  # Hotend heatbreak fan
     # Firmware version (from info.module[name="ota"].sw_ver)
     # Firmware version (from info.module[name="ota"].sw_ver)
     firmware_version: str | None = None
     firmware_version: str | None = None
+    # Queue: user has acknowledged plate is cleared for next queued print
+    plate_cleared: bool = False

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

@@ -101,6 +101,28 @@ describe('PrinterQueueWidget - Clear Plate', () => {
         expect(link).toHaveAttribute('href', '/queue');
         expect(link).toHaveAttribute('href', '/queue');
       });
       });
     });
     });
+
+    it('shows passive link when FINISH but plateCleared is true', async () => {
+      render(<PrinterQueueWidget printerId={1} printerState="FINISH" plateCleared={true} />);
+
+      await waitFor(() => {
+        const link = screen.getByRole('link');
+        expect(link).toHaveAttribute('href', '/queue');
+      });
+
+      expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
+    });
+
+    it('shows passive link when FAILED but plateCleared is true', async () => {
+      render(<PrinterQueueWidget printerId={1} printerState="FAILED" plateCleared={true} />);
+
+      await waitFor(() => {
+        const link = screen.getByRole('link');
+        expect(link).toHaveAttribute('href', '/queue');
+      });
+
+      expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
+    });
   });
   });
 
 
   describe('clear plate button shows queue info', () => {
   describe('clear plate button shows queue info', () => {

+ 2 - 0
frontend/src/api/client.ts

@@ -250,6 +250,8 @@ export interface PrinterStatus {
   big_fan2_speed: number | null;     // Chamber/exhaust fan
   big_fan2_speed: number | null;     // Chamber/exhaust fan
   heatbreak_fan_speed: number | null; // Hotend heatbreak fan
   heatbreak_fan_speed: number | null; // Hotend heatbreak fan
   firmware_version: string | null;   // Firmware version from MQTT
   firmware_version: string | null;   // Firmware version from MQTT
+  // Queue: user has acknowledged plate is cleared for next queued print
+  plate_cleared: boolean;
 }
 }
 
 
 export interface PrinterCreate {
 export interface PrinterCreate {

+ 3 - 2
frontend/src/components/PrinterQueueWidget.tsx

@@ -10,6 +10,7 @@ import { parseUTCDate } from '../utils/date';
 interface PrinterQueueWidgetProps {
 interface PrinterQueueWidgetProps {
   printerId: number;
   printerId: number;
   printerState?: string | null;
   printerState?: string | null;
+  plateCleared?: boolean;
 }
 }
 
 
 function formatRelativeTime(dateString: string | null): string {
 function formatRelativeTime(dateString: string | null): string {
@@ -26,7 +27,7 @@ function formatRelativeTime(dateString: string | null): string {
   return date.toLocaleDateString();
   return date.toLocaleDateString();
 }
 }
 
 
-export function PrinterQueueWidget({ printerId, printerState }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerState, plateCleared }: PrinterQueueWidgetProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { showToast } = useToast();
@@ -56,7 +57,7 @@ export function PrinterQueueWidget({ printerId, printerState }: PrinterQueueWidg
     return null;
     return null;
   }
   }
 
 
-  const needsClearPlate = printerState === 'FINISH' || printerState === 'FAILED';
+  const needsClearPlate = (printerState === 'FINISH' || printerState === 'FAILED') && !plateCleared;
 
 
   if (needsClearPlate) {
   if (needsClearPlate) {
     return (
     return (

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

@@ -2471,7 +2471,7 @@ function PrinterCard({
                 </div>
                 </div>
 
 
                 {/* Queue Widget - always visible when there are pending items */}
                 {/* Queue Widget - always visible when there are pending items */}
-                <PrinterQueueWidget printerId={printer.id} printerState={status.state} />
+                <PrinterQueueWidget printerId={printer.id} printerState={status.state} plateCleared={status.plate_cleared} />
               </>
               </>
             )}
             )}
 
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-D8lA4NE5.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-NiP_88T6.js"></script>
+    <script type="module" crossorigin src="/assets/index-D8lA4NE5.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DF7TfzH1.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DF7TfzH1.css">
   </head>
   </head>
   <body>
   <body>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů