Explorar el Código

● refactor(printers): remove redundant in-widget clear-plate button

  In expanded view, PrinterQueueWidget rendered its own "Clear Plate & Start
  Next" button inside a yellow-bordered card whenever the plate-clear gate
  was up and an auto-dispatch item was queued. PR #939 added the card-level
  "Mark plate as cleared" button that already covers that state — and every
  other state (staged-only queue, empty queue, etc.) — so both buttons hit
  the same /clear-plate endpoint with identical optimistic-update semantics.
  Two controls, one action, visible together in one specific state.

  Remove the widget's button and its entire needsClearPlate render branch.
  The widget becomes a passive "Next in queue" preview linking to /queue;
  the card-level button remains the single plate-clear entry point.

  Also drop:
  - now-dead awaitingPlateClear / requirePlateClear / printerState props
    from PrinterQueueWidgetProps and the matching call site
  - orphaned queue.clearPlate / queue.plateReady translations from all eight
    locale files (queue.clearPlateSuccess stays — used by the card button's
    success toast)
  - PrinterQueueWidgetClearPlate.test.tsx (654 lines) — every test asserted
    the behaviour of the now-gone button; PrinterQueueWidget.test.tsx still
    covers the passive-link path

  Deliberately *not* changed: plate-status pill stays inside the Status box
  (lines 2664/2671/2736/2783 of PrintersPage.tsx). Compact-view (Size S)
  pill and icon-only clear button at :2664/:2671/:2673 untouched.
maziggy hace 1 mes
padre
commit
5215ac68e9

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 3 - 0
CHANGELOG.md


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

@@ -1,654 +0,0 @@
-/**
- * Tests for the PrinterQueueWidget clear plate behavior.
- *
- * When the printer is in FINISH or FAILED state and has pending queue items,
- * the widget shows a "Clear Plate & Start Next" button instead of the
- * passive queue link. After clicking, it shows a confirmation state.
- */
-
-import { describe, it, expect, beforeEach } from 'vitest';
-import { screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { render } from '../utils';
-import { PrinterQueueWidget } from '../../components/PrinterQueueWidget';
-import { http, HttpResponse } from 'msw';
-import { server } from '../mocks/server';
-
-const mockQueueItems = [
-  {
-    id: 1,
-    printer_id: 1,
-    archive_id: 1,
-    position: 1,
-    status: 'pending',
-    archive_name: 'First Print',
-    printer_name: 'X1 Carbon',
-    print_time_seconds: 3600,
-    scheduled_time: null,
-  },
-  {
-    id: 2,
-    printer_id: 1,
-    archive_id: 2,
-    position: 2,
-    status: 'pending',
-    archive_name: 'Second Print',
-    printer_name: 'X1 Carbon',
-    print_time_seconds: 7200,
-    scheduled_time: null,
-  },
-];
-
-describe('PrinterQueueWidget - Clear Plate', () => {
-  beforeEach(() => {
-    server.use(
-      http.get('/api/v1/queue/', ({ request }) => {
-        const url = new URL(request.url);
-        const printerId = url.searchParams.get('printer_id');
-        if (printerId === '1') {
-          return HttpResponse.json(mockQueueItems);
-        }
-        return HttpResponse.json([]);
-      }),
-      http.post('/api/v1/printers/:id/clear-plate', () => {
-        return HttpResponse.json({ success: true, message: 'Plate cleared' });
-      })
-    );
-  });
-
-  describe('clear plate button visibility', () => {
-    it('shows clear plate button when printer state is FINISH', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('shows clear plate button when printer state is FAILED', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('shows passive link when printer state is IDLE', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="IDLE" />);
-
-      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 printer state is RUNNING', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="RUNNING" />);
-
-      await waitFor(() => {
-        const link = screen.getByRole('link');
-        expect(link).toHaveAttribute('href', '/queue');
-      });
-    });
-
-    it('shows passive link when printerState is not provided', async () => {
-      render(<PrinterQueueWidget printerId={1} />);
-
-      await waitFor(() => {
-        const link = screen.getByRole('link');
-        expect(link).toHaveAttribute('href', '/queue');
-      });
-    });
-
-    it('shows passive link when FINISH but plateCleared is true', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={false} />);
-
-      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" awaitingPlateClear={false} />);
-
-      await waitFor(() => {
-        const link = screen.getByRole('link');
-        expect(link).toHaveAttribute('href', '/queue');
-      });
-
-      expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
-    });
-
-    // Regression for #961: after Auto Off cycles the printer it boots into IDLE while
-    // 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(<PrinterQueueWidget printerId={1} printerState="IDLE" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    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(<PrinterQueueWidget printerId={1} awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('clear plate button shows queue info', () => {
-    it('shows next item name in clear plate mode', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('First Print')).toBeInTheDocument();
-      });
-    });
-
-    it('shows additional items badge in clear plate mode', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('+1')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('clear plate action', () => {
-    it('shows confirmation state after clicking clear plate', async () => {
-      const user = userEvent.setup();
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-
-      await user.click(screen.getByText('Clear Plate & Start Next'));
-
-      await waitFor(() => {
-        // Both the widget confirmation and the toast show this text
-        const elements = screen.getAllByText('Plate cleared — ready for next print');
-        expect(elements.length).toBeGreaterThanOrEqual(1);
-      });
-    });
-
-    it('shows error toast on API failure', async () => {
-      server.use(
-        http.post('/api/v1/printers/:id/clear-plate', () => {
-          return HttpResponse.json(
-            { detail: 'Printer not connected' },
-            { status: 400 }
-          );
-        })
-      );
-
-      const user = userEvent.setup();
-      render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-
-      await user.click(screen.getByText('Clear Plate & Start Next'));
-
-      // Button should remain visible (not transition to success state)
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('empty queue', () => {
-    it('renders nothing in FINISH state with no queue items', async () => {
-      const { container } = render(<PrinterQueueWidget printerId={999} printerState="FINISH" awaitingPlateClear={true} />);
-
-      await waitFor(() => {
-        expect(container.querySelector('button')).not.toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('filament compatibility filtering', () => {
-    const petgQueueItems = [
-      {
-        id: 10,
-        printer_id: 1,
-        archive_id: 10,
-        position: 1,
-        status: 'pending',
-        archive_name: 'PETG Print',
-        printer_name: 'H2S',
-        print_time_seconds: 3600,
-        scheduled_time: null,
-        required_filament_types: ['PETG'],
-      },
-    ];
-
-    it('hides widget when queue item requires filament not loaded on printer', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
-      );
-
-      const { container } = render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PLA'])}
-        />
-      );
-
-      // Wait for query to settle, then confirm widget is not rendered
-      await waitFor(() => {
-        expect(container.querySelector('button')).not.toBeInTheDocument();
-      });
-      expect(screen.queryByText('PETG Print')).not.toBeInTheDocument();
-    });
-
-    it('shows widget when queue item required filaments match loaded', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          requirePlateClear={true}
-          loadedFilamentTypes={new Set(['PLA', 'PETG'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('PETG Print')).toBeInTheDocument();
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('shows widget when queue item has no required_filament_types', async () => {
-      // Default mockQueueItems have no required_filament_types
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          requirePlateClear={true}
-          loadedFilamentTypes={new Set(['PLA'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('First Print')).toBeInTheDocument();
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('shows widget when loadedFilamentTypes prop is not provided', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
-      );
-
-      render(
-        <PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('PETG Print')).toBeInTheDocument();
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('skips incompatible first item and shows compatible second item', async () => {
-      const mixedQueue = [
-        {
-          id: 10,
-          printer_id: 1,
-          archive_id: 10,
-          position: 1,
-          status: 'pending',
-          archive_name: 'PETG Print',
-          printer_name: 'H2S',
-          print_time_seconds: 3600,
-          scheduled_time: null,
-          required_filament_types: ['PETG'],
-        },
-        {
-          id: 11,
-          printer_id: 1,
-          archive_id: 11,
-          position: 2,
-          status: 'pending',
-          archive_name: 'PLA Print',
-          printer_name: 'H2S',
-          print_time_seconds: 1800,
-          scheduled_time: null,
-          required_filament_types: ['PLA'],
-        },
-      ];
-
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(mixedQueue))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PLA'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('PLA Print')).toBeInTheDocument();
-      });
-      expect(screen.queryByText('PETG Print')).not.toBeInTheDocument();
-    });
-
-    it('matches filament types case-insensitively', async () => {
-      const lowercaseQueue = [
-        {
-          id: 10,
-          printer_id: 1,
-          archive_id: 10,
-          position: 1,
-          status: 'pending',
-          archive_name: 'Petg Print',
-          printer_name: 'H2S',
-          print_time_seconds: 3600,
-          scheduled_time: null,
-          required_filament_types: ['petg'],
-        },
-      ];
-
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(lowercaseQueue))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          requirePlateClear={true}
-          loadedFilamentTypes={new Set(['PETG'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('Petg Print')).toBeInTheDocument();
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('filament override color filtering', () => {
-    const whitePetgOverrideItem = [
-      {
-        id: 20,
-        printer_id: null,
-        archive_id: 20,
-        position: 1,
-        status: 'pending',
-        archive_name: 'White PETG Print',
-        printer_name: null,
-        print_time_seconds: 3600,
-        scheduled_time: null,
-        required_filament_types: ['PETG'],
-        filament_overrides: [{ slot_id: 1, type: 'PETG', color: '#FFFFFF' }],
-      },
-    ];
-
-    it('hides widget when override color does not match loaded filaments', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
-      );
-
-      const { container } = render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PETG'])}
-          loadedFilaments={new Set(['PETG:0000ff'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(container.querySelector('button')).not.toBeInTheDocument();
-      });
-      expect(screen.queryByText('White PETG Print')).not.toBeInTheDocument();
-    });
-
-    it('shows widget when override color matches loaded filaments', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          requirePlateClear={true}
-          loadedFilamentTypes={new Set(['PETG'])}
-          loadedFilaments={new Set(['PETG:ffffff'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('White PETG Print')).toBeInTheDocument();
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('normalizes override color format (strips # and lowercases)', async () => {
-      const upperCaseColorItem = [
-        {
-          id: 21,
-          printer_id: null,
-          archive_id: 21,
-          position: 1,
-          status: 'pending',
-          archive_name: 'Red PLA Print',
-          printer_name: null,
-          print_time_seconds: 3600,
-          scheduled_time: null,
-          required_filament_types: ['PLA'],
-          filament_overrides: [{ slot_id: 1, type: 'PLA', color: '#FF0000' }],
-        },
-      ];
-
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(upperCaseColorItem))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PLA'])}
-          loadedFilaments={new Set(['PLA:ff0000'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('Red PLA Print')).toBeInTheDocument();
-      });
-    });
-
-    it('shows widget when no loadedFilaments prop is provided (no color filtering)', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
-      );
-
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PETG'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('White PETG Print')).toBeInTheDocument();
-      });
-    });
-
-    it('shows widget when queue item has no filament overrides', async () => {
-      // Default mockQueueItems have no filament_overrides
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilaments={new Set(['PLA:000000'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('First Print')).toBeInTheDocument();
-      });
-    });
-
-    it('matches any override when multiple overrides exist', async () => {
-      const multiOverrideItem = [
-        {
-          id: 22,
-          printer_id: null,
-          archive_id: 22,
-          position: 1,
-          status: 'pending',
-          archive_name: 'Multi Color Print',
-          printer_name: null,
-          print_time_seconds: 3600,
-          scheduled_time: null,
-          required_filament_types: ['PLA'],
-          filament_overrides: [
-            { slot_id: 1, type: 'PLA', color: '#FF0000' },
-            { slot_id: 2, type: 'PLA', color: '#00FF00' },
-          ],
-        },
-      ];
-
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(multiOverrideItem))
-      );
-
-      // Printer has green PLA but not red — should still match (at least one override)
-      render(
-        <PrinterQueueWidget
-          printerId={1}
-          printerState="FINISH"
-          awaitingPlateClear={true}
-          loadedFilamentTypes={new Set(['PLA'])}
-          loadedFilaments={new Set(['PLA:00ff00'])}
-        />
-      );
-
-      await waitFor(() => {
-        expect(screen.getByText('Multi Color Print')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('requirePlateClear setting', () => {
-    it('shows passive link when requirePlateClear is false even in FINISH state', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={false} />);
-
-      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 requirePlateClear is false even in FAILED state', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} requirePlateClear={false} />);
-
-      await waitFor(() => {
-        const link = screen.getByRole('link');
-        expect(link).toHaveAttribute('href', '/queue');
-      });
-
-      expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
-    });
-
-    it('shows clear plate button when requirePlateClear is true (explicit)', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-
-    it('shows passive link when requirePlateClear is not provided (defaults to false)', async () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
-
-      await waitFor(() => {
-        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 () => {
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={false} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('First Print')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('staged (manual_start) items', () => {
-    const stagedItems = [
-      { id: 10, printer_id: 1, archive_id: 1, position: 1, status: 'pending', archive_name: 'Staged Print 1', manual_start: true, scheduled_time: null },
-      { id: 11, printer_id: 1, archive_id: 2, position: 2, status: 'pending', archive_name: 'Staged Print 2', manual_start: true, scheduled_time: null },
-    ];
-
-    it('does not show clear plate button when all items are staged', async () => {
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(stagedItems)),
-      );
-
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
-
-      // Should show the passive link (not the clear plate button)
-      await waitFor(() => {
-        expect(screen.getByText('Staged Print 1')).toBeInTheDocument();
-      });
-      expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
-    });
-
-    it('shows clear plate button when mix of staged and auto-dispatch items', async () => {
-      const mixedItems = [
-        { id: 10, printer_id: 1, archive_id: 1, position: 1, status: 'pending', archive_name: 'Staged Print', manual_start: true, scheduled_time: null },
-        { id: 11, printer_id: 1, archive_id: 2, position: 2, status: 'pending', archive_name: 'Auto Print', manual_start: false, scheduled_time: null },
-      ];
-      server.use(
-        http.get('/api/v1/queue/', () => HttpResponse.json(mixedItems)),
-      );
-
-      render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
-      });
-    });
-  });
-});

+ 8 - 84
frontend/src/components/PrinterQueueWidget.tsx

@@ -1,117 +1,41 @@
-import { useEffect } from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Clock, Calendar, ChevronRight, Loader2, CircleCheck } from 'lucide-react';
+import { useQuery } from '@tanstack/react-query';
+import { Clock, Calendar, ChevronRight } from 'lucide-react';
 import { Link } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
-import { useAuth } from '../contexts/AuthContext';
-import { useToast } from '../contexts/ToastContext';
 import { formatRelativeTime } from '../utils/date';
 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;
-  awaitingPlateClear?: boolean;
-  requirePlateClear?: boolean;
   loadedFilamentTypes?: Set<string>;
   loadedFilaments?: Set<string>;  // "TYPE:rrggbb" pairs for filament override color matching
 }
 
-export function PrinterQueueWidget({ printerId, printerModel, awaitingPlateClear, requirePlateClear = false, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerModel, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
   const { t } = useTranslation();
-  const queryClient = useQueryClient();
-  const { showToast } = useToast();
-  const { hasPermission } = useAuth();
   const { data: queue } = useQuery({
     queryKey: ['queue', printerId, 'pending', printerModel],
     queryFn: () => api.getQueue(printerId, 'pending', printerModel || undefined),
     refetchInterval: 30000,
   });
 
-  const clearPlateMutation = useMutation({
-    mutationFn: () => api.clearPlate(printerId),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['queue', printerId] });
-      queryClient.invalidateQueries({ queryKey: ['printerStatus', printerId] });
-      showToast(t('queue.clearPlateSuccess'), 'success');
-    },
-    onError: (err: Error) => {
-      showToast(err.message, 'error');
-    },
-  });
-
-  // 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 (!awaitingPlateClear) {
-      clearPlateMutation.reset();
-    }
-  }, [awaitingPlateClear, clearPlateMutation]);
-
   // Filter queue to items this printer can actually print (filament type + color check)
   const compatibleQueue = queue ? filterCompatibleQueueItems(queue, loadedFilamentTypes, loadedFilaments) : undefined;
-
-  // Split into auto-dispatchable vs staged (manual_start) items
-  const autoDispatchQueue = compatibleQueue?.filter(item => !item.manual_start) ?? [];
   const totalPending = compatibleQueue?.length || 0;
 
   if (totalPending === 0) {
     return null;
   }
 
-  const nextAutoItem = autoDispatchQueue[0];
   const nextItem = compatibleQueue?.[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;
-    return (
-      <div className="mb-3 p-3 bg-bambu-dark rounded-lg border border-yellow-400/30">
-        <div className="flex items-center gap-3 mb-2">
-          <Calendar className="w-5 h-5 text-yellow-400 flex-shrink-0" />
-          <div className="min-w-0 flex-1">
-            <p className="text-xs text-bambu-gray">{t('queue.nextInQueue')}</p>
-            <p className="text-sm text-white truncate">
-              {displayItem?.archive_name || displayItem?.library_file_name || `File #${displayItem?.archive_id || displayItem?.library_file_id}`}
-            </p>
-          </div>
-          {totalPending > 1 && (
-            <span className="text-xs px-1.5 py-0.5 bg-yellow-400/20 text-yellow-400 rounded flex-shrink-0">
-              +{totalPending - 1}
-            </span>
-          )}
-        </div>
-        {clearPlateMutation.isSuccess ? (
-          <div className="w-full py-2 px-3 rounded-lg bg-bambu-green/10 border border-bambu-green/20 text-bambu-green text-sm flex items-center justify-center gap-2">
-            <CircleCheck className="w-4 h-4" />
-            {t('queue.plateReady')}
-          </div>
-        ) : (
-          <button
-            onClick={() => clearPlateMutation.mutate()}
-            disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
-            className="w-full py-2 px-3 rounded-lg bg-bambu-green/20 border border-bambu-green/40 text-bambu-green hover:bg-bambu-green/30 transition-colors text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50"
-          >
-            {clearPlateMutation.isPending ? (
-              <Loader2 className="w-4 h-4 animate-spin" />
-            ) : (
-              <CircleCheck className="w-4 h-4" />
-            )}
-            {t('queue.clearPlate')}
-          </button>
-        )}
-      </div>
-    );
-  }
 
+  // Passive next-in-queue preview. Plate-clear acknowledgment is handled by the
+  // card-level "Mark plate as cleared" button (PrintersPage.tsx). Having a
+  // second button in this widget caused the two controls to overlap whenever
+  // the plate-clear gate was up with auto-dispatch items queued — both POSTed
+  // to the same /clear-plate endpoint, so the widget button was pure noise.
   return (
     <Link
       to="/queue"

+ 0 - 2
frontend/src/i18n/locales/de.ts

@@ -904,9 +904,7 @@ export default {
     },
     addedBy: 'Hinzugefügt von {{name}}',
     nextInQueue: 'Nächster in der Warteschlange',
-    clearPlate: 'Druckplatte freigeben & Nächsten starten',
     clearPlateSuccess: 'Druckplatte freigegeben — bereit für nächsten Druck',
-    plateReady: 'Druckplatte freigegeben — bereit für nächsten Druck',
     plateNumber: 'Platte {{index}}',
     // Batch / quantity
     quantity: 'Menge',

+ 0 - 2
frontend/src/i18n/locales/en.ts

@@ -904,9 +904,7 @@ export default {
     },
     addedBy: 'Added by {{name}}',
     nextInQueue: 'Next in queue',
-    clearPlate: 'Clear Plate & Start Next',
     clearPlateSuccess: 'Plate cleared — ready for next print',
-    plateReady: 'Plate cleared — ready for next print',
     plateNumber: 'Plate {{index}}',
     // Batch / quantity
     quantity: 'Quantity',

+ 0 - 2
frontend/src/i18n/locales/fr.ts

@@ -897,9 +897,7 @@ export default {
     },
     addedBy: 'Ajouté par {{name}}',
     nextInQueue: 'Prochain en file',
-    clearPlate: 'Vider plateau & lancer suivant',
     clearPlateSuccess: 'Plateau vidé — prêt pour l\'impression suivante',
-    plateReady: 'Plateau vidé — prêt pour l\'impression suivante',
     plateNumber: 'Plateau {{index}}',
     // Batch / quantity
     quantity: 'Quantité',

+ 0 - 2
frontend/src/i18n/locales/it.ts

@@ -897,9 +897,7 @@ export default {
     },
     addedBy: 'Aggiunto da {{name}}',
     nextInQueue: 'Prossimo in coda',
-    clearPlate: 'Libera piatto e avvia il prossimo',
     clearPlateSuccess: 'Piatto liberato — pronto per la prossima stampa',
-    plateReady: 'Piatto liberato — pronto per la prossima stampa',
     plateNumber: 'Piatto {{index}}',
     // Batch / quantity
     quantity: 'Quantità',

+ 0 - 2
frontend/src/i18n/locales/ja.ts

@@ -896,9 +896,7 @@ export default {
     },
     addedBy: '{{username}}が追加',
     nextInQueue: '次のキュー',
-    clearPlate: 'プレートをクリアして次を開始',
     clearPlateSuccess: 'プレートをクリアしました — 次の印刷の準備完了',
-    plateReady: 'プレートをクリアしました — 次の印刷の準備完了',
     plateNumber: 'プレート {{index}}',
     // Batch / quantity
     quantity: '数量',

+ 0 - 2
frontend/src/i18n/locales/pt-BR.ts

@@ -897,9 +897,7 @@ export default {
     },
     addedBy: 'Adicionado por {{name}}',
     nextInQueue: 'Próximo na fila',
-    clearPlate: 'Limpar Placa e Iniciar Próximo',
     clearPlateSuccess: 'Placa limpa — pronta para a próxima impressão',
-    plateReady: 'Placa limpa — pronta para a próxima impressão',
     plateNumber: 'Placa {{index}}',
     // Batch / quantity
     quantity: 'Quantidade',

+ 0 - 2
frontend/src/i18n/locales/zh-CN.ts

@@ -904,9 +904,7 @@ export default {
     },
     addedBy: '由 {{name}} 添加',
     nextInQueue: '队列中的下一个',
-    clearPlate: '清理打印板并开始下一个',
     clearPlateSuccess: '打印板已清理 — 准备进行下一个打印',
-    plateReady: '打印板已清理 — 准备进行下一个打印',
     plateNumber: '板 {{index}}',
     // Batch / quantity
     quantity: '数量',

+ 0 - 2
frontend/src/i18n/locales/zh-TW.ts

@@ -904,9 +904,7 @@ export default {
     },
     addedBy: '由 {{name}} 新增',
     nextInQueue: '佇列中的下一個',
-    clearPlate: '清理列印板並開始下一個',
     clearPlateSuccess: '列印板已清理 — 準備進行下一個列印',
-    plateReady: '列印板已清理 — 準備進行下一個列印',
     plateNumber: '板 {{index}}',
     // Batch / quantity
     quantity: '數量',

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

@@ -2807,7 +2807,7 @@ function PrinterCard({
                 </div>
 
                 {/* Queue Widget - always visible when there are pending items */}
-                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} awaitingPlateClear={status.awaiting_plate_clear} requirePlateClear={requirePlateClear} loadedFilamentTypes={loadedFilamentTypes} loadedFilaments={loadedFilaments} />
+                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} loadedFilamentTypes={loadedFilamentTypes} loadedFilaments={loadedFilaments} />
               </>
             )}
 

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-BoxU3Y8Y.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-CkAOuJaW.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-aHxaU9HU.js


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-B5tW0juP.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CkAOuJaW.css">
+    <script type="module" crossorigin src="/assets/index-aHxaU9HU.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BoxU3Y8Y.css">
   </head>
   <body>
     <div id="root"></div>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio