mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
feat(printers): per-printer Maintenance Mode toggle (#1476)
Operator-flipped out-of-service state per printer for three scenarios: parallel Bambuddy installs (dev + prod where the printer rejects all but one MQTT client), printers under repair, and temporary suspension. The backend Printer.is_active gate has shipped since day one and is already honoured by every consumer — MQTT (printer_manager), queue dispatch (print_scheduler, print_queue), metrics, scheduler, picker, backup, maintenance dashboard. The missing piece was UI exposure. Three entry points to flip is_active: - Three-dot overflow menu (Enter / Exit maintenance mode, wrench icon) - Exit button inside the in-card amber panel - Checkbox in EditPrinterModal Card UI: expanded mode shows an amber panel (Wrench + "In Maintenance" + subtitle + Exit) where the cover/progress container would normally render — same height, no layout shift. Compact mode shows an amber pill in place of the progress bar. Header pill swaps Connected/Offline for "Maintenance" and the diagnostic CTA is suppressed (deliberate state, not involuntary offline). HMS / Queue / Firmware pills fall away naturally via the existing status?.connected gates. Mid-print entry (RUNNING / PAUSE) triggers a confirmation dialog — disconnecting MQTT mid-print stops progress tracking and completion notifications for the in-flight job. Idle / FINISH / FAILED skip the dialog and toggle directly. Scope: no backend change, no new permission, no behaviour change for any other consumer. PrinterCreate.is_active?: boolean added to the TypeScript surface so the field flows through api.updatePrinter.
This commit is contained in:
@@ -19,6 +19,7 @@ const mockPrinters = [
|
||||
access_code: '12345678',
|
||||
model: 'X1C',
|
||||
enabled: true,
|
||||
is_active: true,
|
||||
nozzle_diameter: 0.4,
|
||||
nozzle_type: 'hardened_steel',
|
||||
location: 'Workshop',
|
||||
@@ -34,6 +35,7 @@ const mockPrinters = [
|
||||
access_code: '87654321',
|
||||
model: 'P1S',
|
||||
enabled: false,
|
||||
is_active: true,
|
||||
nozzle_diameter: 0.4,
|
||||
nozzle_type: 'stainless_steel',
|
||||
location: null,
|
||||
@@ -538,6 +540,89 @@ describe('PrintersPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('maintenance mode (#1476)', () => {
|
||||
// Wraps the backend is_active flag — already gates MQTT, queue dispatch,
|
||||
// scheduler, metrics, picker. These tests pin the UI surface: status
|
||||
// panel swap, pill swap, and the PATCH on toggle.
|
||||
const inMaintenancePrinter = { ...mockPrinters[0], is_active: false };
|
||||
|
||||
it('shows the maintenance status panel instead of the print container', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => HttpResponse.json([inMaintenancePrinter])),
|
||||
http.get('/api/v1/printers/:id/status', () =>
|
||||
HttpResponse.json({ ...mockPrinterStatus, connected: false }),
|
||||
),
|
||||
);
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('In Maintenance')).toBeInTheDocument();
|
||||
});
|
||||
// Exit button rendered
|
||||
expect(screen.getByRole('button', { name: /exit maintenance/i })).toBeInTheDocument();
|
||||
// The "No active job" / "Ready to print" copy from the normal status
|
||||
// panel must NOT be present — confirms the swap, not a stacked render.
|
||||
expect(screen.queryByText(/no active job/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/ready to print/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the amber Maintenance pill in the header (no Connected/Offline)', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => HttpResponse.json([inMaintenancePrinter])),
|
||||
http.get('/api/v1/printers/:id/status', () =>
|
||||
HttpResponse.json({ ...mockPrinterStatus, connected: false }),
|
||||
),
|
||||
);
|
||||
render(<PrintersPage />);
|
||||
|
||||
// The header pill row contains "Maintenance" exactly once.
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Maintenance').length).toBeGreaterThan(0);
|
||||
});
|
||||
// No connection diagnostic CTA (that's reserved for involuntary offline).
|
||||
expect(screen.queryByRole('button', { name: /run.*diagnostic/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('PATCHes is_active=true when the Exit button is clicked', async () => {
|
||||
const patchedBodies: unknown[] = [];
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => HttpResponse.json([inMaintenancePrinter])),
|
||||
http.get('/api/v1/printers/:id/status', () =>
|
||||
HttpResponse.json({ ...mockPrinterStatus, connected: false }),
|
||||
),
|
||||
http.patch('/api/v1/printers/:id', async ({ request }) => {
|
||||
const body = await request.json();
|
||||
patchedBodies.push(body);
|
||||
return HttpResponse.json({ ...inMaintenancePrinter, is_active: true });
|
||||
}),
|
||||
);
|
||||
render(<PrintersPage />);
|
||||
|
||||
const exit = await screen.findByRole('button', { name: /exit maintenance/i });
|
||||
fireEvent.click(exit);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(patchedBodies.length).toBeGreaterThan(0);
|
||||
});
|
||||
expect(patchedBodies[0]).toEqual(expect.objectContaining({ is_active: true }));
|
||||
});
|
||||
|
||||
it('renders the regular status panel when is_active=true', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => HttpResponse.json([{ ...mockPrinters[0], is_active: true }])),
|
||||
http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockPrinterStatus)),
|
||||
);
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
// Active printer never shows the maintenance panel.
|
||||
expect(screen.queryByText('In Maintenance')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /exit maintenance/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('nozzle rack card', () => {
|
||||
const h2cStatus = {
|
||||
...mockPrinterStatus,
|
||||
|
||||
Reference in New Issue
Block a user