Просмотр исходного кода

H2C nozzle rack: show all 6 nozzles, translate types, add flow & filament (#300)

- Show all 6 nozzles including mounted: backend includes all nozzle_info
  entries (removes id >= 2 filter), frontend filters to non-empty
- Translate nozzle type codes to full names: HS→Hardened Steel, 00/01/05
  mapped to Stainless Steel/Hardened Steel/Tungsten Carbide
- Add flow type to hover card: HH→High Flow, HS→Standard
- Show filament material type in hover card (PLA, PETG etc.) from MQTT
  tray_type/filament_type field
- Add filament_type to NozzleRackSlot schema (backend + frontend)
- Add translations (en, de, ja, it): nozzleTungstenCarbide, nozzleFlow,
  nozzleHighFlow, nozzleStandardFlow
maziggy 3 месяцев назад
Родитель
Сommit
30b8887e79

+ 4 - 1
CHANGELOG.md

@@ -17,13 +17,16 @@ All notable changes to Bambuddy will be documented in this file.
 - **Extended Support Bundle Diagnostics** — Support bundle now collects comprehensive diagnostic data for faster issue resolution: printer connectivity and firmware versions, integration status (Spoolman, MQTT, Home Assistant), network interfaces (subnets only), Python package versions, database health checks, Docker environment details, WebSocket connections, and log file info. All data properly anonymized — no IPs, names, or serials included. Privacy disclosure updated on System Info page.
 - **Extended Support Bundle Diagnostics** — Support bundle now collects comprehensive diagnostic data for faster issue resolution: printer connectivity and firmware versions, integration status (Spoolman, MQTT, Home Assistant), network interfaces (subnets only), Python package versions, database health checks, Docker environment details, WebSocket connections, and log file info. All data properly anonymized — no IPs, names, or serials included. Privacy disclosure updated on System Info page.
 
 
 ### Improved
 ### Improved
+- **H2C Nozzle Rack — Show All 6 Nozzles Including Mounted** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — The nozzle rack now shows all 6 nozzles, including the one currently mounted on the hotend. Backend includes all nozzle_info entries (hotend + rack) instead of filtering to rack-only IDs. Frontend filters to non-empty nozzles so mounted nozzles appear with the existing green border/text indicator.
+- **H2C Nozzle Rack — Translate Type Codes & Add Flow Info** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Raw nozzle type codes (e.g. "HS", "HH01") are now translated to human-readable names: material (Hardened Steel, Stainless Steel, Tungsten Carbide) and flow type (High Flow, Standard). New "Flow" row in the hover card. Translations added in all 4 locales (en, de, ja, it).
+- **H2C Nozzle Rack — Show Filament Material in Hover Card** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Nozzle hover card now shows the loaded filament material type (e.g. "PLA", "PETG") alongside the color swatch, captured from MQTT nozzle info data.
 - **H2C Nozzle Rack Compact Layout** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Redesigned nozzle rack from a 2×3 grid to a compact single-row layout with bottom accent bars (green = mounted, gray = docked). Temperature cards are thinner, rack card is wider (flex-[2]), and all cards vertically centered.
 - **H2C Nozzle Rack Compact Layout** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Redesigned nozzle rack from a 2×3 grid to a compact single-row layout with bottom accent bars (green = mounted, gray = docked). Temperature cards are thinner, rack card is wider (flex-[2]), and all cards vertically centered.
 - **Auto-Detect Subnet for Printer Discovery** — Docker users no longer need to manually enter a subnet in the Add Printer dialog. Bambuddy auto-detects available network subnets and pre-selects the first one. When multiple subnets are available (e.g., eth0 + wlan0), a dropdown lets users choose. Falls back to manual text input if no subnets are detected.
 - **Auto-Detect Subnet for Printer Discovery** — Docker users no longer need to manually enter a subnet in the Add Printer dialog. Bambuddy auto-detects available network subnets and pre-selects the first one. When multiple subnets are available (e.g., eth0 + wlan0), a dropdown lets users choose. Falls back to manual text input if no subnets are detected.
 - **Japanese Locale Complete Overhaul** — Restructured `ja.ts` from a divergent format (different key structure, 12 structural conflicts, 1,366 missing translations) to match the English/German locale structure exactly. Translated all 2,083 keys into Japanese, achieving full parity with EN/DE. Zero structural divergences, zero missing keys.
 - **Japanese Locale Complete Overhaul** — Restructured `ja.ts` from a divergent format (different key structure, 12 structural conflicts, 1,366 missing translations) to match the English/German locale structure exactly. Translated all 2,083 keys into Japanese, achieving full parity with EN/DE. Zero structural divergences, zero missing keys.
 
 
 ### Fixed
 ### Fixed
 - **Nozzle Rack Hides 0% Wear** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — New nozzles with 0% wear showed no wear info in the hover card because the condition treated 0 the same as "not available." Now displays "Wear: 0%" correctly. The field is still hidden when the printer doesn't report wear data.
 - **Nozzle Rack Hides 0% Wear** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — New nozzles with 0% wear showed no wear info in the hover card because the condition treated 0 the same as "not available." Now displays "Wear: 0%" correctly. The field is still hidden when the printer doesn't report wear data.
-- **H2C Nozzle Rack Shows Wrong Nozzles** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — The nozzle rack included L/R nozzle heads (IDs 0, 1) alongside the actual rack slots (IDs 16–21), causing the mounted nozzle to appear docked and the last rack position (e.g., 0.6mm) to be cut off. Backend now filters to rack-only entries (id >= 2) and sorts by ID for consistent ordering.
+- **H2C Nozzle Rack Shows Wrong Nozzle Count** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — The nozzle rack only showed 5 of 6 nozzles because the mounted nozzle was excluded by the id >= 2 filter. Backend now includes all nozzle_info entries sorted by ID; frontend filters to non-empty entries so all 6 nozzles are visible with proper mounted/docked indicators.
 - **H2C Firmware Update Downloads Wrong Firmware** ([#311](https://github.com/maziggy/bambuddy/issues/311)) — H2C printers were mapped to the H2D firmware API key (`h2d`), causing firmware checks to offer H2D firmware instead of H2C firmware. H2C has its own firmware track (01.01.x.x vs H2D's 01.02.x.x). Added separate `h2c` API key mapping. Also added missing H2C/H2S entries to printer model ID and 3MF model maps.
 - **H2C Firmware Update Downloads Wrong Firmware** ([#311](https://github.com/maziggy/bambuddy/issues/311)) — H2C printers were mapped to the H2D firmware API key (`h2d`), causing firmware checks to offer H2D firmware instead of H2C firmware. H2C has its own firmware track (01.01.x.x vs H2D's 01.02.x.x). Added separate `h2c` API key mapping. Also added missing H2C/H2S entries to printer model ID and 3MF model maps.
 - **Sidebar Links Custom Icons Have Inverted Colors** ([#308](https://github.com/maziggy/bambuddy/issues/308)) — Custom uploaded icons in sidebar links had their colors inverted in dark mode due to a CSS `invert()` filter. The filter was intended for monochrome preset icons but was incorrectly applied to user-uploaded images (e.g., full-color logos). Removed the invert filter from custom icon rendering in the sidebar and the add/edit link modal.
 - **Sidebar Links Custom Icons Have Inverted Colors** ([#308](https://github.com/maziggy/bambuddy/issues/308)) — Custom uploaded icons in sidebar links had their colors inverted in dark mode due to a CSS `invert()` filter. The filter was intended for monochrome preset icons but was incorrectly applied to user-uploaded images (e.g., full-color logos). Removed the invert filter from custom icon rendering in the sidebar and the add/edit link modal.
 - **Virtual Printer FTP Transfer Fails With Connection Reset** ([#58](https://github.com/maziggy/bambuddy/issues/58)) — Large 3MF uploads to the virtual printer intermittently failed with `[Errno 104] Connection reset by peer` while the small verify_job always succeeded. The `_handle_data_connection` callback returned immediately, allowing the asyncio server-handler task to complete while the data connection was still in active use. The passive port listener also stayed open during transfers, risking duplicate data connections. Fixed by keeping the callback alive until the transfer completes (`_transfer_done` event), closing the passive listener after accepting the connection, and rejecting duplicate data connections. Also added a 5-second drain timeout to MQTT status pushes to prevent blocking when the slicer is busy uploading.
 - **Virtual Printer FTP Transfer Fails With Connection Reset** ([#58](https://github.com/maziggy/bambuddy/issues/58)) — Large 3MF uploads to the virtual printer intermittently failed with `[Errno 104] Connection reset by peer` while the small verify_job always succeeded. The `_handle_data_connection` callback returned immediately, allowing the asyncio server-handler task to complete while the data connection was still in active use. The passive port listener also stayed open during transfers, risking duplicate data connections. Fixed by keeping the callback alive until the transfer completes (`_transfer_done` event), closing the passive listener after accepting the connection, and rejecting duplicate data connections. Also added a 5-second drain timeout to MQTT status pushes to prevent blocking when the slicer is busy uploading.

+ 1 - 0
backend/app/api/routes/printers.py

@@ -373,6 +373,7 @@ async def get_printer_status(
             serial_number=n.get("serial_number", ""),
             serial_number=n.get("serial_number", ""),
             filament_color=n.get("filament_color", ""),
             filament_color=n.get("filament_color", ""),
             filament_id=n.get("filament_id", ""),
             filament_id=n.get("filament_id", ""),
+            filament_type=n.get("filament_type", ""),
         )
         )
         for n in (state.nozzle_rack or [])
         for n in (state.nozzle_rack or [])
     ]
     ]

+ 1 - 0
backend/app/schemas/printer.py

@@ -157,6 +157,7 @@ class NozzleRackSlot(BaseModel):
     serial_number: str = ""  # Nozzle serial number
     serial_number: str = ""  # Nozzle serial number
     filament_color: str = ""  # RGBA hex ("00000000" = no filament)
     filament_color: str = ""  # RGBA hex ("00000000" = no filament)
     filament_id: str = ""  # Bambu filament ID
     filament_id: str = ""  # Bambu filament ID
+    filament_type: str = ""  # Material type (e.g. "PLA", "PETG")
 
 
 
 
 class PrintOptionsResponse(BaseModel):
 class PrintOptionsResponse(BaseModel):

+ 6 - 6
backend/app/services/bambu_mqtt.py

@@ -1749,10 +1749,10 @@ class BambuMQTTClient:
             nozzle_info = nozzle_data.get("info", [])
             nozzle_info = nozzle_data.get("info", [])
             if isinstance(nozzle_info, list):
             if isinstance(nozzle_info, list):
                 # H2C tool-changer: >2 entries means nozzle rack
                 # H2C tool-changer: >2 entries means nozzle rack
-                # nozzle_info contains L/R nozzle heads (id 0,1) AND rack slots (id >= 16).
-                # Filter out L/R heads — they're already tracked in self.state.nozzles.
+                # nozzle_info contains L/R nozzle heads (id 0,1) AND rack slots.
+                # Include ALL entries so mounted nozzles (on hotend) appear in the rack
+                # display with their full data (wear, max_temp, serial, etc.).
                 if len(nozzle_info) > 2:
                 if len(nozzle_info) > 2:
-                    rack_entries = [n for n in nozzle_info if n.get("id", 0) >= 2]
                     self.state.nozzle_rack = sorted(
                     self.state.nozzle_rack = sorted(
                         [
                         [
                             {
                             {
@@ -1765,19 +1765,19 @@ class BambuMQTTClient:
                                 "serial_number": str(n.get("serial_number", "")),
                                 "serial_number": str(n.get("serial_number", "")),
                                 "filament_color": str(n.get("filament_colour", "")),
                                 "filament_color": str(n.get("filament_colour", "")),
                                 "filament_id": str(n.get("filament_id", "")),
                                 "filament_id": str(n.get("filament_id", "")),
+                                "filament_type": str(n.get("tray_type", "") or n.get("filament_type", "")),
                             }
                             }
-                            for i, n in enumerate(rack_entries)
+                            for i, n in enumerate(nozzle_info)
                         ],
                         ],
                         key=lambda x: x["id"],
                         key=lambda x: x["id"],
                     )
                     )
                     if not hasattr(self, "_nozzle_rack_logged") and nozzle_info:
                     if not hasattr(self, "_nozzle_rack_logged") and nozzle_info:
                         self._nozzle_rack_logged = True
                         self._nozzle_rack_logged = True
                         logger.info(
                         logger.info(
-                            "[%s] Nozzle info: %d entries, IDs: %s, rack IDs: %s",
+                            "[%s] Nozzle info: %d entries, IDs: %s",
                             self.serial_number,
                             self.serial_number,
                             len(nozzle_info),
                             len(nozzle_info),
                             [n.get("id") for n in nozzle_info],
                             [n.get("id") for n in nozzle_info],
-                            [n.get("id") for n in rack_entries],
                         )
                         )
                 for nozzle in nozzle_info:
                 for nozzle in nozzle_info:
                     idx = nozzle.get("id", 0)
                     idx = nozzle.get("id", 0)

+ 1 - 0
frontend/src/api/client.ts

@@ -139,6 +139,7 @@ export interface NozzleRackSlot {
   serial_number: string;
   serial_number: string;
   filament_color: string;  // RGBA hex ("00000000" = no filament)
   filament_color: string;  // RGBA hex ("00000000" = no filament)
   filament_id: string;
   filament_id: string;
+  filament_type: string;  // Material type (e.g. "PLA", "PETG")
 }
 }
 
 
 export interface PrintOptions {
 export interface PrintOptions {

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

@@ -215,6 +215,10 @@ export default {
     nozzleSerial: 'Seriennr.',
     nozzleSerial: 'Seriennr.',
     nozzleHardenedSteel: 'Gehärteter Stahl',
     nozzleHardenedSteel: 'Gehärteter Stahl',
     nozzleStainlessSteel: 'Edelstahl',
     nozzleStainlessSteel: 'Edelstahl',
+    nozzleTungstenCarbide: 'Wolframkarbid',
+    nozzleFlow: 'Durchfluss',
+    nozzleHighFlow: 'High Flow',
+    nozzleStandardFlow: 'Standard',
     // Firmware
     // Firmware
     firmwareUpdate: 'Firmware-Update',
     firmwareUpdate: 'Firmware-Update',
     firmwareInstructions: 'Gehen Sie auf dem Touchscreen des Druckers zu',
     firmwareInstructions: 'Gehen Sie auf dem Touchscreen des Druckers zu',

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

@@ -215,6 +215,10 @@ export default {
     nozzleSerial: 'Serial',
     nozzleSerial: 'Serial',
     nozzleHardenedSteel: 'Hardened Steel',
     nozzleHardenedSteel: 'Hardened Steel',
     nozzleStainlessSteel: 'Stainless Steel',
     nozzleStainlessSteel: 'Stainless Steel',
+    nozzleTungstenCarbide: 'Tungsten Carbide',
+    nozzleFlow: 'Flow',
+    nozzleHighFlow: 'High Flow',
+    nozzleStandardFlow: 'Standard',
     // Firmware
     // Firmware
     firmwareUpdate: 'Firmware Update',
     firmwareUpdate: 'Firmware Update',
     firmwareInstructions: 'On the printer\'s touchscreen, go to',
     firmwareInstructions: 'On the printer\'s touchscreen, go to',

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

@@ -215,6 +215,10 @@ export default {
     nozzleSerial: 'Seriale',
     nozzleSerial: 'Seriale',
     nozzleHardenedSteel: 'Acciaio Temprato',
     nozzleHardenedSteel: 'Acciaio Temprato',
     nozzleStainlessSteel: 'Acciaio Inox',
     nozzleStainlessSteel: 'Acciaio Inox',
+    nozzleTungstenCarbide: 'Carburo di Tungsteno',
+    nozzleFlow: 'Flusso',
+    nozzleHighFlow: 'Alto Flusso',
+    nozzleStandardFlow: 'Standard',
     // Firmware
     // Firmware
     firmwareUpdate: 'Aggiornamento Firmware',
     firmwareUpdate: 'Aggiornamento Firmware',
     firmwareInstructions: 'Sul touchscreen della stampante, vai a',
     firmwareInstructions: 'Sul touchscreen della stampante, vai a',

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

@@ -199,6 +199,10 @@ export default {
     nozzleSerial: 'シリアル',
     nozzleSerial: 'シリアル',
     nozzleHardenedSteel: '焼入れ鋼',
     nozzleHardenedSteel: '焼入れ鋼',
     nozzleStainlessSteel: 'ステンレス鋼',
     nozzleStainlessSteel: 'ステンレス鋼',
+    nozzleTungstenCarbide: 'タングステンカーバイド',
+    nozzleFlow: 'フロー',
+    nozzleHighFlow: 'ハイフロー',
+    nozzleStandardFlow: 'スタンダード',
     toast: {
     toast: {
       printStopped: '印刷を停止しました',
       printStopped: '印刷を停止しました',
       printPaused: '印刷を一時停止しました',
       printPaused: '印刷を一時停止しました',

+ 56 - 26
frontend/src/pages/PrintersPage.tsx

@@ -421,11 +421,38 @@ function parseFilamentColor(rgba: string): string | null {
   return `rgba(${parseInt(r, 16)}, ${parseInt(g, 16)}, ${parseInt(b, 16)}, ${a})`;
   return `rgba(${parseInt(r, 16)}, ${parseInt(g, 16)}, ${parseInt(b, 16)}, ${a})`;
 }
 }
 
 
-// Expand nozzle type codes to full names
+// Expand nozzle type codes to material names
+// Handles full text ("hardened_steel"), 2-char codes ("HS"/"HH"), and 4-char codes ("HS01")
+// Material mapping: 00=stainless steel, 01=hardened steel, 05=tungsten carbide
 function nozzleTypeName(type: string, t: (key: string) => string): string {
 function nozzleTypeName(type: string, t: (key: string) => string): string {
+  if (!type) return '';
+  // Full text names (from main nozzle info)
   if (type.includes('hardened')) return t('printers.nozzleHardenedSteel');
   if (type.includes('hardened')) return t('printers.nozzleHardenedSteel');
   if (type.includes('stainless')) return t('printers.nozzleStainlessSteel');
   if (type.includes('stainless')) return t('printers.nozzleStainlessSteel');
-  return type || '';
+  if (type.includes('tungsten')) return t('printers.nozzleTungstenCarbide');
+  // 4-char codes (e.g. "HS01"): last 2 digits = material
+  if (type.length >= 4) {
+    const material = type.slice(2, 4);
+    if (material === '00') return t('printers.nozzleStainlessSteel');
+    if (material === '01') return t('printers.nozzleHardenedSteel');
+    if (material === '05') return t('printers.nozzleTungstenCarbide');
+  }
+  // 2-digit numeric codes
+  if (type === '00') return t('printers.nozzleStainlessSteel');
+  if (type === '01') return t('printers.nozzleHardenedSteel');
+  if (type === '05') return t('printers.nozzleTungstenCarbide');
+  // 2-char alpha codes: H prefix = hardened steel
+  if (type.startsWith('H')) return t('printers.nozzleHardenedSteel');
+  return type;
+}
+
+// Parse flow type from nozzle type code
+// HH = high flow, HS = standard/normal
+function nozzleFlowName(type: string, t: (key: string) => string): string {
+  if (!type) return '';
+  if (type.startsWith('HH')) return t('printers.nozzleHighFlow');
+  if (type.startsWith('HS')) return t('printers.nozzleStandardFlow');
+  return '';
 }
 }
 
 
 // Per-slot hover card for nozzle rack
 // Per-slot hover card for nozzle rack
@@ -477,6 +504,7 @@ function NozzleSlotHoverCard({ slot, index, children }: {
 
 
   const filamentCss = parseFilamentColor(slot.filament_color);
   const filamentCss = parseFilamentColor(slot.filament_color);
   const typeFull = nozzleTypeName(slot.nozzle_type, t);
   const typeFull = nozzleTypeName(slot.nozzle_type, t);
+  const flowFull = nozzleFlowName(slot.nozzle_type, t);
 
 
   return (
   return (
     <div
     <div
@@ -518,6 +546,14 @@ function NozzleSlotHoverCard({ slot, index, children }: {
                   </div>
                   </div>
                 )}
                 )}
 
 
+                {/* Flow (hide if empty) */}
+                {flowFull && (
+                  <div className="flex items-center justify-between">
+                    <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">{t('printers.nozzleFlow')}</span>
+                    <span className="text-xs text-white font-semibold">{flowFull}</span>
+                  </div>
+                )}
+
                 {/* Status badge */}
                 {/* Status badge */}
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">Status</span>
                   <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">Status</span>
@@ -554,15 +590,15 @@ function NozzleSlotHoverCard({ slot, index, children }: {
                   </div>
                   </div>
                 )}
                 )}
 
 
-                {/* Filament color swatch + ID (hide if empty/00000000) */}
-                {filamentCss && (
+                {/* Filament: material type + color swatch (hide if no color) */}
+                {(filamentCss || slot.filament_type) && (
                   <div className="flex items-center justify-between">
                   <div className="flex items-center justify-between">
                     <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">Filament</span>
                     <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">Filament</span>
                     <div className="flex items-center gap-1">
                     <div className="flex items-center gap-1">
-                      <div className="w-3 h-3 rounded-sm border border-white/20" style={{ backgroundColor: filamentCss }} />
-                      {slot.filament_id && (
-                        <span className="text-[10px] text-white font-mono">{slot.filament_id}</span>
+                      {filamentCss && (
+                        <div className="w-3 h-3 rounded-sm border border-white/20" style={{ backgroundColor: filamentCss }} />
                       )}
                       )}
+                      <span className="text-[10px] text-white font-semibold">{slot.filament_type || slot.filament_id || ''}</span>
                     </div>
                     </div>
                   </div>
                   </div>
                 )}
                 )}
@@ -590,15 +626,15 @@ function NozzleSlotHoverCard({ slot, index, children }: {
 // H2C Nozzle Rack Card — compact single row showing 6-position tool-changer dock
 // H2C Nozzle Rack Card — compact single row showing 6-position tool-changer dock
 function NozzleRackCard({ slots }: { slots: import('../api/client').NozzleRackSlot[] }) {
 function NozzleRackCard({ slots }: { slots: import('../api/client').NozzleRackSlot[] }) {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  // Backend now filters to rack-only slots (excludes L/R nozzle heads)
-  const dockSlots = slots;
+  // Backend sends all nozzle_info entries (hotend + rack).
+  // Filter to non-empty nozzles — these are the actual nozzles in the system.
+  const allNozzles = slots.filter(s => s.nozzle_diameter || s.nozzle_type);
 
 
   return (
   return (
     <div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2] flex flex-col justify-center">
     <div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2] flex flex-col justify-center">
       <p className="text-[9px] text-bambu-gray mb-1">{t('printers.nozzleRack')}</p>
       <p className="text-[9px] text-bambu-gray mb-1">{t('printers.nozzleRack')}</p>
       <div className="flex gap-[3px] justify-center">
       <div className="flex gap-[3px] justify-center">
-        {dockSlots.map((slot, i) => {
-          const isEmpty = !slot.nozzle_diameter && !slot.nozzle_type;
+        {allNozzles.map((slot, i) => {
           const isMounted = slot.stat === 1;
           const isMounted = slot.stat === 1;
           const filamentBg = parseFilamentColor(slot.filament_color);
           const filamentBg = parseFilamentColor(slot.filament_color);
 
 
@@ -606,23 +642,17 @@ function NozzleRackCard({ slots }: { slots: import('../api/client').NozzleRackSl
             <NozzleSlotHoverCard key={slot.id ?? i} slot={slot} index={i}>
             <NozzleSlotHoverCard key={slot.id ?? i} slot={slot} index={i}>
               <div
               <div
                 className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
                 className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
-                  isEmpty
-                    ? 'bg-bambu-dark-tertiary/20 opacity-20 border-transparent'
-                    : isMounted
-                      ? 'bg-green-950/35 border-green-400'
-                      : 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
+                  isMounted
+                    ? 'bg-green-950/35 border-green-400'
+                    : 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
                 }`}
                 }`}
-                style={filamentBg && !isEmpty ? { backgroundColor: filamentBg } : undefined}
+                style={filamentBg ? { backgroundColor: filamentBg } : undefined}
               >
               >
-                {isEmpty ? (
-                  <span className="text-[9px] text-bambu-gray/50">—</span>
-                ) : (
-                  <span className={`text-[10px] font-semibold ${isMounted ? 'text-green-400' : 'text-white'}`}
-                        style={filamentBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
-                  >
-                    {slot.nozzle_diameter || '?'}
-                  </span>
-                )}
+                <span className={`text-[10px] font-semibold ${isMounted ? 'text-green-400' : 'text-white'}`}
+                      style={filamentBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
+                >
+                  {slot.nozzle_diameter || '?'}
+                </span>
               </div>
               </div>
             </NozzleSlotHoverCard>
             </NozzleSlotHoverCard>
           );
           );