diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa15e6b4f..103bc468d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@ All notable changes to Bambuddy will be documented in this file.
### Improved
- **Settings Search Finds More Cards** — The cross-tab search field at the top of Settings now finds Sidebar Links, Spoolman, Spool Catalog, Color Catalog, all four Failure Detection sections, Advanced Email Authentication, SMTP Test, Authenticator App (TOTP), Email OTP, 2FA Linked Accounts, Single Sign-On (OIDC), LDAP Server Configuration, and the four Backup sub-cards (GitHub, History, Local, Scheduled). Powered by a new module-level registry (`frontend/src/lib/settingsSearch.ts`) so future settings register themselves next to their component instead of being forgotten in a central array.
+### Changed
+- **Plate-Clear Confirmation Disabled by Default** — New installs ship with Settings → Workflow → "Require Plate-Clear Confirmation" off. Multiple new users reported queued prints appearing to not start because the prompt was waiting for acknowledgement; opt in from Workflow if you want the confirmation gate.
+
### Fixed
- **Clear Plate Confirmation Bypassed on Power Cycle** ([#961](https://github.com/maziggy/bambuddy/issues/961)) — With Auto Off enabled and another job queued, the smart plug would cut power when a print finished and immediately re-power when the scheduler saw the queue, at which point the printer booted fresh into `IDLE` and the next job auto-dispatched without the "Clear Plate & Start Next" confirmation. Root cause: the plate-cleared gate lived only in the in-memory `PrinterManager._plate_cleared` set, and the scheduler's idle check treated `IDLE` as always-idle regardless of whether a previous finish had been acknowledged — so the gate was lost across both Bambuddy restarts and the IDLE-on-boot state transition. The gate is now an `awaiting_plate_clear` column on the `printers` table, set by `on_print_complete` when a print finishes or fails, cleared by the `/printers/{id}/clear-plate` endpoint and by the scheduler when it dispatches the next job, and rehydrated from the DB into `PrinterManager` on startup. `_is_printer_idle` now short-circuits to not-idle whenever `require_plate_clear` is on and the printer is awaiting ack, regardless of the currently reported state — so the prompt survives Auto Off cycles, Bambuddy restarts, and the printer booting back into `IDLE`. The clear-plate endpoint no longer requires the printer to currently report `FINISH`/`FAILED` (it accepts the ack whenever the awaiting flag is set), and the Printers page widget prompts based on the flag rather than the reported state. Thanks to @miaopas for reporting.
- **Insecure Temp File Creation in Backup Export** — The manual backup download endpoint used `tempfile.mktemp()`, which is vulnerable to a symlink race condition (CWE-377). Replaced with `tempfile.mkstemp()` which atomically creates the file, eliminating the TOCTOU window.
diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py
index 8dee7c1ec..5aa1b7bbf 100644
--- a/backend/app/schemas/settings.py
+++ b/backend/app/schemas/settings.py
@@ -224,7 +224,7 @@ class AppSettings(BaseModel):
# Plate-clear confirmation for queue scheduling
require_plate_clear: bool = Field(
- default=True,
+ default=False,
description="Require per-printer plate-clear confirmation before starting queued prints on finished printers",
)
queue_shortest_first: bool = Field(
diff --git a/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx b/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx
index d0cbd93af..466052d08 100644
--- a/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx
+++ b/frontend/src/__tests__/components/PrinterQueueWidgetClearPlate.test.tsx
@@ -58,7 +58,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
describe('clear plate button visibility', () => {
it('shows clear plate button when printer state is FINISH', async () => {
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -66,7 +66,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
});
it('shows clear plate button when printer state is FAILED', async () => {
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -128,7 +128,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
// still awaiting plate-clear ack. The prompt must still show — the ack state, not
// the reported printer state, is the authoritative signal.
it('shows clear plate button in IDLE state when awaitingPlateClear is true (#961)', async () => {
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -137,7 +137,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
it('shows clear plate button with no printerState when awaitingPlateClear is true', async () => {
// State may be null briefly after a reconnect; the widget must still gate on the flag.
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -166,7 +166,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
describe('clear plate action', () => {
it('shows confirmation state after clicking clear plate', async () => {
const user = userEvent.setup();
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -192,7 +192,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
);
const user = userEvent.setup();
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
@@ -264,6 +264,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
printerId={1}
printerState="FINISH"
awaitingPlateClear={true}
+ requirePlateClear={true}
loadedFilamentTypes={new Set(['PLA', 'PETG'])}
/>
);
@@ -281,6 +282,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
printerId={1}
printerState="FINISH"
awaitingPlateClear={true}
+ requirePlateClear={true}
loadedFilamentTypes={new Set(['PLA'])}
/>
);
@@ -297,7 +299,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
);
render(
-
+
);
await waitFor(() => {
@@ -378,6 +380,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
printerId={1}
printerState="FINISH"
awaitingPlateClear={true}
+ requirePlateClear={true}
loadedFilamentTypes={new Set(['PETG'])}
/>
);
@@ -437,6 +440,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
printerId={1}
printerState="FINISH"
awaitingPlateClear={true}
+ requirePlateClear={true}
loadedFilamentTypes={new Set(['PETG'])}
loadedFilaments={new Set(['PETG:ffffff'])}
/>
@@ -591,12 +595,15 @@ describe('PrinterQueueWidget - Clear Plate', () => {
});
});
- it('shows clear plate button when requirePlateClear is not provided (defaults to true)', async () => {
+ it('shows passive link when requirePlateClear is not provided (defaults to false)', async () => {
render();
await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '/queue');
});
+
+ expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
});
it('still shows next item info in passive link when requirePlateClear is false', async () => {
@@ -637,7 +644,7 @@ describe('PrinterQueueWidget - Clear Plate', () => {
http.get('/api/v1/queue/', () => HttpResponse.json(mixedItems)),
);
- render();
+ render();
await waitFor(() => {
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
diff --git a/frontend/src/components/PrinterQueueWidget.tsx b/frontend/src/components/PrinterQueueWidget.tsx
index b62ae002e..76b592d44 100644
--- a/frontend/src/components/PrinterQueueWidget.tsx
+++ b/frontend/src/components/PrinterQueueWidget.tsx
@@ -20,7 +20,7 @@ interface PrinterQueueWidgetProps {
loadedFilaments?: Set; // "TYPE:rrggbb" pairs for filament override color matching
}
-export function PrinterQueueWidget({ printerId, printerModel, awaitingPlateClear, requirePlateClear = true, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerModel, awaitingPlateClear, requirePlateClear = false, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx
index fa18d7eff..5537801f1 100644
--- a/frontend/src/pages/PrintersPage.tsx
+++ b/frontend/src/pages/PrintersPage.tsx
@@ -1279,7 +1279,7 @@ function PrinterCard({
onOpenEmbeddedCamera,
checkPrinterFirmware = true,
dryingPresets = DRYING_PRESETS,
- requirePlateClear = true,
+ requirePlateClear = false,
selectionMode = false,
isSelected = false,
onToggleSelect,
@@ -6360,7 +6360,7 @@ export function PrintersPage() {
onOpenEmbeddedCamera={(id, name) => setEmbeddedCameraPrinters(prev => new Map(prev).set(id, { id, name }))}
checkPrinterFirmware={settings?.check_printer_firmware !== false}
dryingPresets={effectiveDryingPresets}
- requirePlateClear={settings?.require_plate_clear !== false}
+ requirePlateClear={settings?.require_plate_clear === true}
selectionMode={selectionMode}
isSelected={selectedPrinterIds.has(printer.id)}
onToggleSelect={toggleSelect}
@@ -6399,7 +6399,7 @@ export function PrintersPage() {
onOpenEmbeddedCamera={(id, name) => setEmbeddedCameraPrinters(prev => new Map(prev).set(id, { id, name }))}
checkPrinterFirmware={settings?.check_printer_firmware !== false}
dryingPresets={effectiveDryingPresets}
- requirePlateClear={settings?.require_plate_clear !== false}
+ requirePlateClear={settings?.require_plate_clear === true}
selectionMode={selectionMode}
isSelected={selectedPrinterIds.has(printer.id)}
onToggleSelect={toggleSelect}
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
index 8b2b0a593..9ab2e8572 100644
--- a/frontend/src/pages/SettingsPage.tsx
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -889,7 +889,7 @@ export function SettingsPage() {
(settings.default_timelapse ?? false) !== (localSettings.default_timelapse ?? false) ||
(settings.stagger_group_size ?? 2) !== (localSettings.stagger_group_size ?? 2) ||
(settings.stagger_interval_minutes ?? 5) !== (localSettings.stagger_interval_minutes ?? 5) ||
- (settings.require_plate_clear ?? true) !== (localSettings.require_plate_clear ?? true);
+ (settings.require_plate_clear ?? false) !== (localSettings.require_plate_clear ?? false);
if (!hasChanges) {
return;
@@ -3730,7 +3730,7 @@ export function SettingsPage() {