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
This commit is contained in:
maziggy
2026-02-17 12:14:47 +01:00
parent 20e06bcf0c
commit a0abeba394
9 changed files with 35 additions and 6 deletions
+1
View File
@@ -71,6 +71,7 @@ All notable changes to Bambuddy will be documented in this file.
### 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.
- **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
- **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
View File
@@ -466,6 +466,7 @@ async def get_printer_status(
big_fan2_speed=state.big_fan2_speed,
heatbreak_fan_speed=state.heatbreak_fan_speed,
firmware_version=state.firmware_version,
plate_cleared=printer_manager.is_plate_cleared(printer_id),
)
+2
View File
@@ -245,3 +245,5 @@ class PrinterStatus(BaseModel):
heatbreak_fan_speed: int | None = None # Hotend heatbreak fan
# Firmware version (from info.module[name="ota"].sw_ver)
firmware_version: str | None = None
# Queue: user has acknowledged plate is cleared for next queued print
plate_cleared: bool = False
@@ -101,6 +101,28 @@ describe('PrinterQueueWidget - Clear Plate', () => {
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', () => {
+2
View File
@@ -250,6 +250,8 @@ export interface PrinterStatus {
big_fan2_speed: number | null; // Chamber/exhaust fan
heatbreak_fan_speed: number | null; // Hotend heatbreak fan
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 {
@@ -10,6 +10,7 @@ import { parseUTCDate } from '../utils/date';
interface PrinterQueueWidgetProps {
printerId: number;
printerState?: string | null;
plateCleared?: boolean;
}
function formatRelativeTime(dateString: string | null): string {
@@ -26,7 +27,7 @@ function formatRelativeTime(dateString: string | null): string {
return date.toLocaleDateString();
}
export function PrinterQueueWidget({ printerId, printerState }: PrinterQueueWidgetProps) {
export function PrinterQueueWidget({ printerId, printerState, plateCleared }: PrinterQueueWidgetProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
@@ -56,7 +57,7 @@ export function PrinterQueueWidget({ printerId, printerState }: PrinterQueueWidg
return null;
}
const needsClearPlate = printerState === 'FINISH' || printerState === 'FAILED';
const needsClearPlate = (printerState === 'FINISH' || printerState === 'FAILED') && !plateCleared;
if (needsClearPlate) {
return (
+1 -1
View File
@@ -2471,7 +2471,7 @@ function PrinterCard({
</div>
{/* 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} />
</>
)}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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-NiP_88T6.js"></script>
<script type="module" crossorigin src="/assets/index-D8lA4NE5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DF7TfzH1.css">
</head>
<body>