Procházet zdrojové kódy

fix(printers): hide chamber fan badge on open-frame Bambu models

      The Printers page rendered three fan widgets (part / aux / chamber)
      for every printer unconditionally. Open-frame models (A1, A1 Mini,
      A2L, P1P) have no chamber fan — the firmware reports big_fan2_speed
      as 0, so the badge always rendered greyed-out and let users "set" a
      fan speed on hardware that doesn't exist.

      Adds MODELS_WITH_CHAMBER_FAN allowlist (X1C / X1 / X1E / X2D / P1S /
      P2S / H2D / H2D Pro / H2C / H2S) near mapModelCode, and the chamber
      entry is spread into fanItems only when the printer's model is in the
      set. Open-frame printers now show two badges (part + aux), which
      matches their actual hardware.

      Allowlist not denylist: mirrors the file's existing classification
      pattern (the enclosure-door badge gate uses the same shape), and the
      failure mode is preferable — a missing badge on a real chambered
      printer is obvious; a phantom badge on a future open-frame model
      would look correct and silently lie.
maziggy před 2 měsíci
rodič
revize
568f220a5a

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
CHANGELOG.md


+ 67 - 0
frontend/src/__tests__/pages/PrintersPage.test.tsx

@@ -243,6 +243,73 @@ describe('PrintersPage', () => {
     });
     });
   });
   });
 
 
+  describe('fan badges', () => {
+    // Chamber fan only exists on enclosed Bambu models. Open-frame printers
+    // (A1, A1 Mini, A2L, P1P) have no chamber fan — the firmware reports
+    // big_fan2_speed as 0 there and the widget would be dead UI.
+    const statusWithFans = {
+      ...mockPrinterStatus,
+      cooling_fan_speed: 53,
+      big_fan1_speed: 53,
+      big_fan2_speed: 53,
+    };
+
+    const renderWithPrinter = (printer: typeof mockPrinters[number]) => {
+      server.use(
+        http.get('/api/v1/printers/', () => HttpResponse.json([printer])),
+        http.get('/api/v1/printers/:id/status', () => HttpResponse.json(statusWithFans)),
+      );
+      render(<PrintersPage />);
+    };
+
+    it('hides chamber fan badge on A1 Mini (open-frame, no chamber fan)', async () => {
+      renderWithPrinter({ ...mockPrinters[0], model: 'A1 Mini' });
+
+      await waitFor(() => {
+        // Part-cooling badge confirms the fan row rendered.
+        expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
+      });
+      expect(screen.getByTitle('Auxiliary Fan')).toBeInTheDocument();
+      expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
+    });
+
+    it('hides chamber fan badge on A1 (open-frame)', async () => {
+      renderWithPrinter({ ...mockPrinters[0], model: 'A1' });
+
+      await waitFor(() => {
+        expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
+      });
+      expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
+    });
+
+    it('hides chamber fan badge on P1P (open-frame)', async () => {
+      renderWithPrinter({ ...mockPrinters[0], model: 'P1P' });
+
+      await waitFor(() => {
+        expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
+      });
+      expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
+    });
+
+    it('shows chamber fan badge on X1C (enclosed)', async () => {
+      renderWithPrinter({ ...mockPrinters[0], model: 'X1C' });
+
+      await waitFor(() => {
+        expect(screen.getByTitle('Chamber Fan')).toBeInTheDocument();
+      });
+      expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
+      expect(screen.getByTitle('Auxiliary Fan')).toBeInTheDocument();
+    });
+
+    it('shows chamber fan badge on P1S (enclosed)', async () => {
+      renderWithPrinter({ ...mockPrinters[0], model: 'P1S' });
+
+      await waitFor(() => {
+        expect(screen.getByTitle('Chamber Fan')).toBeInTheDocument();
+      });
+    });
+  });
+
   describe('empty state', () => {
   describe('empty state', () => {
     it('shows empty state when no printers', async () => {
     it('shows empty state when no printers', async () => {
       server.use(
       server.use(

+ 34 - 7
frontend/src/pages/PrintersPage.tsx

@@ -1465,6 +1465,23 @@ function getStatusDisplay(state: string | null | undefined, stg_cur_name: string
   }
   }
 }
 }
 
 
+// Bambu models that ship with an enclosure chamber fan (firmware field
+// `big_fan2_speed`). Open-frame models (A1 / A1 Mini / A2L / P1P) have no
+// chamber fan — `big_fan2_speed` is meaningless / always 0 there, so the
+// widget is hidden in fanItems instead of rendered greyed-out.
+const MODELS_WITH_CHAMBER_FAN: ReadonlySet<string> = new Set([
+  'X1C',
+  'X1',
+  'X1E',
+  'X2D',
+  'P1S',
+  'P2S',
+  'H2D',
+  'H2D Pro',
+  'H2C',
+  'H2S',
+]);
+
 // Map SSDP model codes to display names
 // Map SSDP model codes to display names
 function mapModelCode(ssdpModel: string | null): string {
 function mapModelCode(ssdpModel: string | null): string {
   if (!ssdpModel) return '';
   if (!ssdpModel) return '';
@@ -3576,6 +3593,12 @@ function PrinterCard({
               const statusControlClass = `relative text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center transition-colors ${
               const statusControlClass = `relative text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center transition-colors ${
                 canUseStatusControls ? 'cursor-pointer hover:bg-bambu-dark-tertiary' : 'cursor-default opacity-80'
                 canUseStatusControls ? 'cursor-pointer hover:bg-bambu-dark-tertiary' : 'cursor-default opacity-80'
               }`;
               }`;
+              // Chamber fan only exists on enclosed Bambu models. Open-frame
+              // printers (A1, A1 Mini, A2L, P1P) have no chamber fan — showing
+              // the widget there is at best dead UI and at worst suggests a
+              // control that does nothing. Mirrors the enclosure-door badge
+              // gate above.
+              const hasChamberFan = MODELS_WITH_CHAMBER_FAN.has(printer.model ?? '');
               const fanItems = [
               const fanItems = [
                 {
                 {
                   key: 'part',
                   key: 'part',
@@ -3591,13 +3614,17 @@ function PrinterCard({
                   Icon: Wind,
                   Icon: Wind,
                   activeClass: 'text-blue-400',
                   activeClass: 'text-blue-400',
                 },
                 },
-                {
-                  key: 'chamber',
-                  label: t('printers.fans.chamber'),
-                  value: status.big_fan2_speed ?? 0,
-                  Icon: AirVent,
-                  activeClass: 'text-green-400',
-                },
+                ...(hasChamberFan
+                  ? [
+                      {
+                        key: 'chamber',
+                        label: t('printers.fans.chamber'),
+                        value: status.big_fan2_speed ?? 0,
+                        Icon: AirVent,
+                        activeClass: 'text-green-400',
+                      },
+                    ]
+                  : []),
               ];
               ];
 
 
               return (
               return (

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů