|
@@ -2615,7 +2615,10 @@ function PrinterCard({
|
|
|
const lastPrint = lastPrints?.[0];
|
|
const lastPrint = lastPrints?.[0];
|
|
|
const isPrintingOrPaused = status?.state === 'RUNNING' || status?.state === 'PAUSE';
|
|
const isPrintingOrPaused = status?.state === 'RUNNING' || status?.state === 'PAUSE';
|
|
|
const needsPlateClear = requirePlateClear && status?.awaiting_plate_clear === true;
|
|
const needsPlateClear = requirePlateClear && status?.awaiting_plate_clear === true;
|
|
|
- const showClearPlateButton = status?.connected && needsPlateClear && !isPrintingOrPaused;
|
|
|
|
|
|
|
+ // Not gated on `connected`: the plate-clear gate is Bambuddy-side state, and with
|
|
|
|
|
+ // Auto Power Off the printer is powered down exactly when the operator clears the
|
|
|
|
|
+ // plate. Hiding the control there left no way to release the gate (#2864).
|
|
|
|
|
+ const showClearPlateButton = needsPlateClear && !isPrintingOrPaused;
|
|
|
const activePrintName = status?.current_print && isPrintingOrPaused
|
|
const activePrintName = status?.current_print && isPrintingOrPaused
|
|
|
? formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t, activePlateLabel)
|
|
? formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t, activePlateLabel)
|
|
|
: null;
|
|
: null;
|
|
@@ -2628,6 +2631,9 @@ function PrinterCard({
|
|
|
}
|
|
}
|
|
|
}, [activePrintName, needsPlateClear, status?.cover_url]);
|
|
}, [activePrintName, needsPlateClear, status?.cover_url]);
|
|
|
const plateStatus = (() => {
|
|
const plateStatus = (() => {
|
|
|
|
|
+ // Connected-only because the pill's only render site sits inside the live-status
|
|
|
|
|
+ // panel. For a powered-down printer the plate-clear button itself carries the
|
|
|
|
|
+ // state — see the standalone slot below the status block (#2864).
|
|
|
if (!requirePlateClear || !status?.connected) return null;
|
|
if (!requirePlateClear || !status?.connected) return null;
|
|
|
if (isPrintingOrPaused) {
|
|
if (isPrintingOrPaused) {
|
|
|
return {
|
|
return {
|
|
@@ -2825,6 +2831,26 @@ function PrinterCard({
|
|
|
onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSendCommand'), 'error'),
|
|
onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSendCommand'), 'error'),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Rendered from two places: inside the live-status block for a connected printer,
|
|
|
|
|
+ // and standalone below it for a powered-down one, whose status block isn't rendered
|
|
|
|
|
+ // at all (#2864). Shared so the two can't drift apart.
|
|
|
|
|
+ const expandedClearPlateButton = (
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => clearPlateMutation.mutate()}
|
|
|
|
|
+ disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
|
|
|
|
+ className="mt-2 w-full inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-400/40 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs font-medium disabled:opacity-50"
|
|
|
|
|
+ title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
|
|
|
|
+ >
|
|
|
|
|
+ {clearPlateMutation.isPending ? (
|
|
|
|
|
+ <Loader2 className="w-[var(--pc-i3,0.75rem)] h-[var(--pc-i3,0.75rem)] animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <PlateClearedIcon className="w-[var(--pc-i4,1rem)] h-[var(--pc-i4,1rem)]" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {t('printers.plateStatus.markCleared')}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
const nozzleTemperatureMutation = useMutation({
|
|
const nozzleTemperatureMutation = useMutation({
|
|
|
mutationFn: ({ target, nozzle }: { target: number; nozzle: number }) =>
|
|
mutationFn: ({ target, nozzle }: { target: number; nozzle: number }) =>
|
|
|
api.setNozzleTemperature(printer.id, target, nozzle),
|
|
api.setNozzleTemperature(printer.id, target, nozzle),
|
|
@@ -4692,22 +4718,7 @@ function PrinterCard({
|
|
|
);
|
|
);
|
|
|
})()}
|
|
})()}
|
|
|
|
|
|
|
|
- {viewMode === 'expanded' && showClearPlateButton && (
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => clearPlateMutation.mutate()}
|
|
|
|
|
- disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
|
|
|
|
- className="mt-2 w-full inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-400/40 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs font-medium disabled:opacity-50"
|
|
|
|
|
- title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
|
|
|
|
- >
|
|
|
|
|
- {clearPlateMutation.isPending ? (
|
|
|
|
|
- <Loader2 className="w-[var(--pc-i3,0.75rem)] h-[var(--pc-i3,0.75rem)] animate-spin" />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <PlateClearedIcon className="w-[var(--pc-i4,1rem)] h-[var(--pc-i4,1rem)]" />
|
|
|
|
|
- )}
|
|
|
|
|
- {t('printers.plateStatus.markCleared')}
|
|
|
|
|
- </button>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {viewMode === 'expanded' && showClearPlateButton && expandedClearPlateButton}
|
|
|
|
|
|
|
|
{/* Controls */}
|
|
{/* Controls */}
|
|
|
{viewMode === 'expanded' && (() => {
|
|
{viewMode === 'expanded' && (() => {
|
|
@@ -6285,6 +6296,14 @@ function PrinterCard({
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {/* Powered-down printer with a dirty plate: the status block above renders
|
|
|
|
|
+ nothing without a live connection, so the plate-clear control gets its own
|
|
|
|
|
+ slot here. Auto Power Off makes this the ordinary end-of-print state, and
|
|
|
|
|
+ the gate is Bambuddy-side — releasing it never touches the printer (#2864). */}
|
|
|
|
|
+ {printer.is_active !== false && !status?.connected && viewMode === 'expanded' && showClearPlateButton && (
|
|
|
|
|
+ expandedClearPlateButton
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{/* Bottom block (power row + action bar). Wrapped together so the
|
|
{/* Bottom block (power row + action bar). Wrapped together so the
|
|
|
power row hugs the action bar at the card bottom instead of
|
|
power row hugs the action bar at the card bottom instead of
|
|
|
floating up when there's less filament content above. */}
|
|
floating up when there's less filament content above. */}
|
|
@@ -8895,12 +8914,15 @@ export function PrintersPage() {
|
|
|
// Filter to only applicable printers based on cached state
|
|
// Filter to only applicable printers based on cached state
|
|
|
const applicableIds = ids.filter(id => {
|
|
const applicableIds = ids.filter(id => {
|
|
|
const status = queryClient.getQueryData<{ connected: boolean; state: string | null; hms_errors?: HMSError[] }>(['printerStatus', id]);
|
|
const status = queryClient.getQueryData<{ connected: boolean; state: string | null; hms_errors?: HMSError[] }>(['printerStatus', id]);
|
|
|
|
|
+ // clearPlate is checked before the connection filter: it only releases a
|
|
|
|
|
+ // Bambuddy-side gate, so it applies to a printer Auto Power Off has shut
|
|
|
|
|
+ // down — every other action here needs to reach the machine (#2864).
|
|
|
|
|
+ if (action === 'clearPlate') return !!(status as { awaiting_plate_clear?: boolean } | undefined)?.awaiting_plate_clear;
|
|
|
if (!status?.connected) return false;
|
|
if (!status?.connected) return false;
|
|
|
switch (action) {
|
|
switch (action) {
|
|
|
case 'stop': return status.state === 'RUNNING' || status.state === 'PAUSE';
|
|
case 'stop': return status.state === 'RUNNING' || status.state === 'PAUSE';
|
|
|
case 'pause': return status.state === 'RUNNING';
|
|
case 'pause': return status.state === 'RUNNING';
|
|
|
case 'resume': return status.state === 'PAUSE';
|
|
case 'resume': return status.state === 'PAUSE';
|
|
|
- case 'clearPlate': return !!(status as { awaiting_plate_clear?: boolean }).awaiting_plate_clear;
|
|
|
|
|
case 'clearHMS': return status.hms_errors && filterKnownHMSErrors(status.hms_errors).length > 0;
|
|
case 'clearHMS': return status.hms_errors && filterKnownHMSErrors(status.hms_errors).length > 0;
|
|
|
default: return false;
|
|
default: return false;
|
|
|
}
|
|
}
|