Explorar el Código

fix(inventory): require brand and subtype in spool form (#417)

The spool add/edit modal allowed saving without brand or subtype,
causing incomplete tray_sub_brands when assigned to AMS slots.
BambuStudio then failed to recognize the filament profile.

Brand and Subtype are now mandatory with validation errors on submit.
maziggy hace 3 meses
padre
commit
11d9c98b55

+ 1 - 0
CHANGELOG.md

@@ -19,6 +19,7 @@ All notable changes to Bambuddy will be documented in this file.
 
 - **Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers** ([#364](https://github.com/maziggy/bambuddy/issues/364)) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT `mapping` field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to `slot_id - 1` as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (`ams_hw_id * 256 + local_slot`) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on `tray_now` disambiguation.
 - **npm audit: suppress moderate ajv ReDoS finding** — Added `audit-level=high` to `frontend/.npmrc` so `npm audit` exits cleanly. The ajv@6 ReDoS (GHSA-2g4f-4pwh-qvx6) is a transitive dependency of eslint@9 with no patched v6 release; ajv@8 override breaks eslint. The vulnerability requires crafted `$data` schema input — not an attack vector in a linting config.
+- **Spool Form Allows Empty Brand & Subtype** ([#417](https://github.com/maziggy/bambuddy/issues/417)) — The spool add/edit modal did not require Brand or Subtype fields, allowing spools to be saved without them. When such a spool was assigned to an AMS slot, the `tray_sub_brands` sent to the printer was incomplete (e.g., just "PETG" instead of "PETG Basic"), causing BambuStudio to not recognize the filament profile. Brand and Subtype are now mandatory fields with validation errors shown on submit.
 
 ### Changed
 - **Filament Catalog API Renamed** ([#427](https://github.com/maziggy/bambuddy/issues/427)) — Renamed `/api/v1/filaments/` to `/api/v1/filament-catalog/` to avoid confusion with the inventory spools page (labeled "Filament" in the UI). The old endpoint managed material type definitions (cost, temperature, density), not physical spools — the shared name caused users to expect the API to return their spool inventory.

+ 7 - 1
frontend/src/components/SpoolFormModal.tsx

@@ -320,7 +320,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
     if (!validation.isValid) {
       setErrors(validation.errors);
       // Switch to filament tab if there are errors there
-      if (validation.errors.slicer_filament || validation.errors.material) {
+      if (validation.errors.slicer_filament || validation.errors.material || validation.errors.brand || validation.errors.subtype) {
         setActiveTab('filament');
       }
       return;
@@ -438,6 +438,12 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
                 {errors.material && (
                   <p className="mt-1 text-xs text-red-400">{errors.material}</p>
                 )}
+                {errors.brand && (
+                  <p className="mt-1 text-xs text-red-400">{errors.brand}</p>
+                )}
+                {errors.subtype && (
+                  <p className="mt-1 text-xs text-red-400">{errors.subtype}</p>
+                )}
               </div>
 
               {/* Color Section */}

+ 2 - 2
frontend/src/components/spool-form/FilamentSection.tsx

@@ -161,7 +161,7 @@ export function FilamentSection({
 
       {/* Brand (dropdown with search) */}
       <div>
-        <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.brand')}</label>
+        <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.brand')} *</label>
         <div className="relative" ref={brandRef}>
           <input
             type="text"
@@ -221,7 +221,7 @@ export function FilamentSection({
 
       {/* Variant / Subtype */}
       <div>
-        <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.subtype')}</label>
+        <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.subtype')} *</label>
         <div className="relative" ref={subtypeRef}>
           <input
             type="text"

+ 8 - 0
frontend/src/components/spool-form/types.ts

@@ -117,6 +117,14 @@ export function validateForm(formData: SpoolFormData): ValidationResult {
     errors.material = 'Material is required';
   }
 
+  if (!formData.brand) {
+    errors.brand = 'Brand is required';
+  }
+
+  if (!formData.subtype) {
+    errors.subtype = 'Subtype is required';
+  }
+
   return {
     isValid: Object.keys(errors).length === 0,
     errors,

+ 0 - 1
frontend/src/pages/QueuePage.tsx

@@ -586,7 +586,6 @@ function SortableQueueItem({
         </div>
 
         {/* Status badge + Actions */}
-        {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
         <div className="flex flex-col sm:flex-row items-end sm:items-center gap-2 sm:gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
           <StatusBadge status={item.status} waitingReason={item.waiting_reason} printerState={printerState} t={t} />
 

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


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


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


+ 2 - 2
static/index.html

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

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