Browse Source

Size the H2C nozzle rack card to its contents and number its slots

The rack card shared a row with the nozzle, bed and chamber readings but
was set to flex: 2 1 190px -- a 190px floor plus twice their growth share
-- to draw six fixed 28px chips. On anything wider than a compact card it
claimed several hundred pixels and left most of them empty, and the width
came out of the cards that needed it: the combined dual-nozzle reading was
wrapping "220 / 220" onto two lines beside a mostly blank rack. It is now
flex: 0 1 auto, so it takes its content width and gives the remainder
back. Shrink stays enabled so it still gives way on a narrow card instead
of overflowing.

Each slot also carries its physical rack position 1-6 below the chip, so a
nozzle can be named rather than counted along. The numbering is positional
-- an empty slot keeps its number -- so "the nozzle in slot 4" means the
same thing however many of the six are occupied.

The #943 regression test read every span in the slot row, which the new
number spans would have interleaved with the diameters; it now matches the
diameter spans specifically. A second test pins the 1-6 labelling across a
rack with empty positions.
maziggy 3 weeks ago
parent
commit
d0e217f65a

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The MQTT debug log now records the commands sent to a printer, not only what it reports back** — **Printer → Debug → MQTT** captured one side of the conversation. Bambuddy listens on both of a printer's topics, but the one carrying commands returned before anything was written to the log, so a capture could show every status push the printer made and nothing it was ever told — including the commands Bambu Studio sends over the local network, which is the only place they can be observed at all. Those now appear alongside Bambuddy's own, grouped under the outgoing filter. It is what lets a question like "which value does Studio put in this field?" be answered from a user's capture instead of guessed at, and it is why #2774 could not be taken further. Commands Bambuddy sends appear twice, once as it publishes and once as the broker echoes it back, and the pair is itself evidence the command reached the broker. Logging is off until switched on, as before. Covered by backend tests.
 - **The L and XL printer cards now scale their text and icons, not just their width (#1848, reporter @misterff1)** — Switching a card from M to XL made it wider, enlarged the printer name and the thumbnail, and left everything else exactly as it was: the AMS slot labels, temperatures, filament names, status text and every small button stayed pinned between 8 and 11 pixels, well under the smallest size used anywhere else in Bambuddy. The result was a full-width card carrying the same tiny text as the compact one, which is precisely the opposite of what someone reaching for a bigger card is asking for. Browser zoom is not an answer to this, since it enlarges the entire page and so preserves the very disparity being complained about. The card body now scales along with the card: L draws it 20% larger and XL 40% larger, icons included, so the controls grow with the text rather than staying fiddly to hit. The AMS-HT card needed two adjustments of its own, since its temperature and humidity readings sit beside the slot rather than under it. Its single slot was the only thing on that row able to grow, so it swallowed every spare pixel and pushed the readings hard against the card's edge — it is now capped at roughly two ordinary slots, which keeps them clear at any card width. The card itself also gained a ceiling of one full AMS card's width, so a unit that wraps onto a line of its own no longer stretches that single slot across the whole card. S and M are deliberately untouched — S is the dense fleet view where density is the point, and M is the default, so an existing install looks identical until you reach for a size that is already asking for more room. Wiki updated. Covered by frontend tests.
 - **The "Slicer Bundles (removed)" card is gone from Settings** — Bundle import was withdrawn in 0.2.5, and the panel it lived in was kept behind as a static notice explaining where the feature went and what to use instead. That notice has done its job: it has been visible for several releases, it was shown to everyone running the slicer sidecar whether or not they had ever imported a bundle, and it occupied a card in **Settings → Workflow** that could not be acted on. The card and its translations are removed. Nothing about slicing changes — single-preset import, Bambu Cloud and Orca Cloud sync all work as before, and the slice-time lookup order is still Imported, then Orca Cloud, then Bambu Cloud, then the sidecar's standard presets. In the same pass **G-code Injection** moved to the foot of the right-hand column, which evens out two columns that the removal had left lopsided; the card itself is unchanged, and the settings search still jumps straight to it. Wiki updated.
+- **The H2C nozzle rack no longer crowds the temperatures beside it, and its slots are numbered** — The rack card sat in the same row as the nozzle, bed and chamber readings, but it grew at twice their rate from a 190-pixel floor, so on anything wider than a compact card it claimed several hundred pixels to draw a row of six 28-pixel chips and left the rest empty. The width came out of its neighbours, which are the cards that actually need it: on a dual-nozzle machine the combined nozzle reading is "220° / 220°" and it was wrapping onto two lines beside a mostly blank rack. The card is now sized to what it draws and gives the remainder back; it can still give way and shrink on a narrow card rather than overflowing. Each slot also carries its physical rack position underneath it, 1 to 6, so a nozzle can be named rather than counted along — the numbering is positional, so an empty slot keeps its number and "the nozzle in slot 4" means the same thing however many of the six are occupied. Covered by frontend tests.
 - **Error and warning toasts now stay up twice as long** — Every pop-up notification disappeared after three seconds regardless of what it said. That is about right for "Settings saved", which confirms something you just did and is skimmed rather than read, but errors and warnings are a different kind of message: they carry a reason, often one relayed from the printer or the backend, and they run to a couple of lines. Three seconds was not long enough to finish reading one, and a missed error message is gone for good — there is no notification history to go back to. Errors and warnings now hold for six seconds. Success and informational toasts keep the three-second default, so the common case of clicking something and seeing it confirmed is unchanged, and the close button and the manual dismiss work exactly as before on all of them. The background print-dispatch toast is unaffected: it stays up while it has work in progress and clears itself shortly after the last job settles. Covered by frontend tests.
 
 ### Fixed

+ 25 - 1
frontend/src/__tests__/pages/PrintersPage.test.tsx

@@ -812,10 +812,34 @@ describe('PrintersPage', () => {
       const rackLabel = screen.getAllByText('Nozzle Rack')[0];
       const rackCard = rackLabel.parentElement!;
       const slotRow = rackCard.querySelectorAll('div.flex')[0];
-      const slotTexts = Array.from(slotRow.querySelectorAll('span')).map(s => s.textContent);
+      // Match the diameter spans specifically — each chip also carries a slot
+      // number span, and a bare `span` sweep would interleave the two.
+      const slotTexts = Array.from(slotRow.querySelectorAll('span[data-rack-diameter]')).map(s => s.textContent);
       expect(slotTexts).toEqual(['—', '0.2', '0.6', '0.8', '1.0', '1.2']);
     });
 
+    it('labels every rack position 1..6 regardless of which nozzles are present', async () => {
+      // The numbering is positional, not a count of what is loaded: the empty
+      // position keeps its number so "the nozzle in slot 4" means the same
+      // thing whether or not slots 1..3 are occupied.
+      server.use(
+        http.get('/api/v1/printers/:id/status', () => {
+          return HttpResponse.json(h2cStatus);
+        })
+      );
+
+      render(<PrintersPage />);
+
+      await waitFor(() => {
+        expect(screen.getAllByText('Nozzle Rack').length).toBeGreaterThan(0);
+      });
+
+      const rackLabel = screen.getAllByText('Nozzle Rack')[0];
+      const slotRow = rackLabel.parentElement!.querySelectorAll('div.flex')[0];
+      const numbers = Array.from(slotRow.querySelectorAll('span:not([data-rack-diameter])')).map(s => s.textContent);
+      expect(numbers).toEqual(['1', '2', '3', '4', '5', '6']);
+    });
+
     it('hides nozzle rack when only L/R nozzles present (H2D)', async () => {
       const h2dStatus = {
         ...mockPrinterStatus,

+ 28 - 12
frontend/src/pages/PrintersPage.tsx

@@ -555,7 +555,12 @@ function NozzleRackCard({ slots, filamentInfo }: { slots: import('../api/client'
   );
 
   return (
-    <div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2_1_190px] flex flex-col justify-center">
+    // Sized to its contents rather than growing: the six chips are a fixed
+    // 28px each, so any extra width is dead space taken from the temperature
+    // cards beside it — which are flex-1 with a 0 basis and wrap their values
+    // ("220° / 220°") as soon as they lose it. Shrink stays enabled so the
+    // card still gives way on a narrow printer card instead of overflowing.
+    <div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[0_1_auto] flex flex-col justify-center">
       <p className="text-[length:var(--pc-t9,9px)] text-bambu-gray mb-1">{t('printers.nozzleRack')}</p>
       <div className="flex gap-[3px] justify-center">
         {rackSlots.map((slot, i) => {
@@ -565,18 +570,29 @@ function NozzleRackCard({ slots, filamentInfo }: { slots: import('../api/client'
 
           return (
             <NozzleSlotHoverCard key={slot.id >= 0 ? slot.id : `empty-${i}`} slot={slot} index={i} filamentName={slot.filament_id ? filamentInfo?.[slot.filament_id]?.name : undefined}>
-              <div
-                className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
-                  isEmpty
-                    ? 'bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20'
-                    : 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
-                }`}
-                style={filamentBg ? { backgroundColor: filamentBg } : undefined}
-              >
-                <span className={`text-[length:var(--pc-t10,10px)] font-semibold ${isEmpty ? 'text-bambu-gray/30' : lightBg ? 'text-black/80' : 'text-white'}`}
-                      style={filamentBg && !lightBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
+              <div className="flex flex-col items-center gap-0.5">
+                <div
+                  className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
+                    isEmpty
+                      ? 'bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20'
+                      : 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
+                  }`}
+                  style={filamentBg ? { backgroundColor: filamentBg } : undefined}
                 >
-                  {isEmpty ? '—' : (slot.nozzle_diameter || '?')}
+                  <span
+                    data-rack-diameter
+                    className={`text-[length:var(--pc-t10,10px)] font-semibold ${isEmpty ? 'text-bambu-gray/30' : lightBg ? 'text-black/80' : 'text-white'}`}
+                    style={filamentBg && !lightBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
+                  >
+                    {isEmpty ? '—' : (slot.nozzle_diameter || '?')}
+                  </span>
+                </div>
+                {/* Physical rack position, so "swap the nozzle in slot 4" can be
+                    acted on without counting chips. Sits below the chip rather
+                    than inside it: the chip is 28px and already carries the
+                    diameter over a filament-coloured background. */}
+                <span className="text-[length:var(--pc-t8,8px)] leading-none tabular-nums text-bambu-gray/70">
+                  {i + 1}
                 </span>
               </div>
             </NozzleSlotHoverCard>

File diff suppressed because it is too large
+ 1 - 0
static/assets/index-B3n6Lnj1.css


File diff suppressed because it is too large
+ 0 - 1
static/assets/index-BVhCaZnn.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CiO0qpbC.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-D686R9Ft.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BVhCaZnn.css">
+    <script type="module" crossorigin src="/assets/index-CiO0qpbC.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-B3n6Lnj1.css">
   </head>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff