fix: persist plate-clear gate so Auto Off power cycles can't bypass the queue confirmation (#961)

With Auto Off enabled and another job queued, the smart plug cut power when a
  print finished and immediately re-powered the printer because the scheduler
  saw pending items. The printer booted fresh into IDLE and the next job
  auto-dispatched, bypassing the "Clear Plate & Start Next" confirmation.

  Root cause: the plate-clear gate lived only in PrinterManager._plate_cleared
  (in-memory set) and _is_printer_idle treated IDLE as unconditionally idle. On
  power cycle the in-memory flag was lost and the IDLE-on-boot state skipped
  the gate entirely.

  Fix:
  - Replace the in-memory flag with an awaiting_plate_clear column on the
    printers table, rehydrated into the PrinterManager at startup.
  - Set the flag in on_print_complete for completed/failed prints (not user
    cancellations); clear it on ack and on scheduler dispatch.
  - _is_printer_idle now short-circuits to not-idle whenever require_plate_clear
    is on and the flag is set, regardless of the currently reported state —
    so the gate holds through power cycles, Bambuddy restarts, and the printer
    booting back into IDLE.
  - /printers/{id}/clear-plate no longer requires the printer to report
    FINISH/FAILED; it accepts the ack whenever the flag is raised.
  - Frontend widgets (PrinterQueueWidget, Layout, BulkPrinterToolbar) gate on
    the flag rather than reported state.

  Tests: added regression tests for IDLE+awaiting=True (the #961 case) and
  full DB round-trip tests for the persistence layer.
This commit is contained in:
maziggy
2026-04-13 09:12:12 +02:00
parent 1204369dd2
commit de7fff0be4
17 changed files with 436 additions and 216 deletions
+13 -8
View File
@@ -12,14 +12,15 @@ import { filterCompatibleQueueItems } from '../utils/printer';
interface PrinterQueueWidgetProps {
printerId: number;
printerModel?: string | null;
/** @deprecated use awaitingPlateClear — kept so existing callers/tests still compile */
printerState?: string | null;
plateCleared?: boolean;
awaitingPlateClear?: boolean;
requirePlateClear?: boolean;
loadedFilamentTypes?: Set<string>;
loadedFilaments?: Set<string>; // "TYPE:rrggbb" pairs for filament override color matching
}
export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared, requirePlateClear = true, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
export function PrinterQueueWidget({ printerId, printerModel, awaitingPlateClear, requirePlateClear = true, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
@@ -42,13 +43,14 @@ export function PrinterQueueWidget({ printerId, printerModel, printerState, plat
},
});
// Reset mutation state when printer starts a new print cycle so the button
// is clickable again when the next print finishes (fixes #912)
// Reset mutation state when the awaiting flag clears so the button is clickable
// again after the next finished print (fixes #912). The flag is the authoritative
// signal — state alone is not reliable across power cycles (#961).
useEffect(() => {
if (printerState !== 'FINISH' && printerState !== 'FAILED') {
if (!awaitingPlateClear) {
clearPlateMutation.reset();
}
}, [printerState, clearPlateMutation]);
}, [awaitingPlateClear, clearPlateMutation]);
// Filter queue to items this printer can actually print (filament type + color check)
const compatibleQueue = queue ? filterCompatibleQueueItems(queue, loadedFilamentTypes, loadedFilaments) : undefined;
@@ -63,8 +65,11 @@ export function PrinterQueueWidget({ printerId, printerModel, printerState, plat
const nextAutoItem = autoDispatchQueue[0];
const nextItem = compatibleQueue?.[0];
// Only prompt "Clear Plate & Start Next" when there are auto-dispatchable items
const needsClearPlate = requirePlateClear && (printerState === 'FINISH' || printerState === 'FAILED') && !plateCleared && autoDispatchQueue.length > 0;
// Prompt "Clear Plate & Start Next" whenever the backend flags the printer as awaiting
// acknowledgment. Don't gate on reported state: after Auto Off cycles the printer, it
// boots into IDLE while still awaiting — the prompt must survive that (#961). The flag
// is cleared by the backend on ack or when the next print dispatches.
const needsClearPlate = requirePlateClear && !!awaitingPlateClear && autoDispatchQueue.length > 0;
if (needsClearPlate) {
const displayItem = nextAutoItem || nextItem;