Procházet zdrojové kódy

Auto-create Spoolman 'tag' extra field on connect (Issue #123)

The 'tag' extra field is required in Spoolman for storing RFID/UUID
identifiers that link Bambu Lab spools to Spoolman entries. Previously,
users had to manually create this field in Spoolman, causing sync
failures for fresh installations.

Added ensure_tag_extra_field() method to SpoolmanClient that:
- Checks if the 'tag' field exists via GET /api/v1/field/spool/tag
- Creates it via POST if missing

The method is called automatically:
- On app startup when auto-connecting to Spoolman
- When user clicks "Connect" in Spoolman settings
maziggy před 4 měsíci
rodič
revize
0caa68b77f

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@ All notable changes to Bambuddy will be documented in this file.
 
 
 ### Fixed
 ### Fixed
 - **Filament cost using wrong default** - Statistics now correctly uses the "Default filament cost (per kg)" setting instead of hardcoded €25 value (Issue #120)
 - **Filament cost using wrong default** - Statistics now correctly uses the "Default filament cost (per kg)" setting instead of hardcoded €25 value (Issue #120)
+- **Spoolman tag field not auto-created** - The required "tag" extra field is now automatically created in Spoolman on first connect, fixing sync failures for fresh Spoolman installs (Issue #123)
 
 
 ## [0.1.6b10] - 2026-01-21
 ## [0.1.6b10] - 2026-01-21
 
 

+ 3 - 0
backend/app/api/routes/spoolman.py

@@ -109,6 +109,9 @@ async def connect_spoolman(db: AsyncSession = Depends(get_db)):
                 detail=f"Could not connect to Spoolman at {url}",
                 detail=f"Could not connect to Spoolman at {url}",
             )
             )
 
 
+        # Ensure the 'tag' extra field exists for RFID/UUID storage
+        await client.ensure_tag_extra_field()
+
         return {"success": True, "message": f"Connected to Spoolman at {url}"}
         return {"success": True, "message": f"Connected to Spoolman at {url}"}
     except Exception as e:
     except Exception as e:
         logger.error(f"Failed to connect to Spoolman: {e}")
         logger.error(f"Failed to connect to Spoolman: {e}")

+ 2 - 0
backend/app/main.py

@@ -1903,6 +1903,8 @@ async def lifespan(app: FastAPI):
                 client = await init_spoolman_client(spoolman_url)
                 client = await init_spoolman_client(spoolman_url)
                 if await client.health_check():
                 if await client.health_check():
                     logging.info(f"Auto-connected to Spoolman at {spoolman_url}")
                     logging.info(f"Auto-connected to Spoolman at {spoolman_url}")
+                    # Ensure the 'tag' extra field exists for RFID/UUID storage
+                    await client.ensure_tag_extra_field()
                 else:
                 else:
                     logging.warning(f"Spoolman at {spoolman_url} is not reachable")
                     logging.warning(f"Spoolman at {spoolman_url} is not reachable")
             except Exception as e:
             except Exception as e:

+ 36 - 0
backend/app/services/spoolman.py

@@ -488,6 +488,42 @@ class SpoolmanClient:
         vendor = await self.create_vendor("Bambu Lab")
         vendor = await self.create_vendor("Bambu Lab")
         return vendor["id"] if vendor else None
         return vendor["id"] if vendor else None
 
 
+    async def ensure_tag_extra_field(self) -> bool:
+        """Ensure the 'tag' extra field exists for spools.
+
+        Spoolman requires extra fields to be registered before use.
+        This creates the 'tag' field used to store RFID/UUID identifiers.
+
+        Returns:
+            True if field exists or was created, False on failure.
+        """
+        try:
+            client = await self._get_client()
+
+            # Check if field already exists
+            response = await client.get(f"{self.api_url}/field/spool/tag")
+            if response.status_code == 200:
+                logger.debug("Spoolman 'tag' extra field already exists")
+                return True
+
+            # Field doesn't exist - create it
+            field_data = {
+                "name": "tag",
+                "field_type": "text",
+                "default_value": None,
+            }
+            response = await client.post(f"{self.api_url}/field/spool/tag", json=field_data)
+            if response.status_code in (200, 201):
+                logger.info("Created 'tag' extra field in Spoolman")
+                return True
+
+            logger.warning(f"Failed to create 'tag' extra field: {response.status_code} - {response.text}")
+            return False
+
+        except Exception as e:
+            logger.warning(f"Failed to ensure 'tag' extra field exists: {e}")
+            return False
+
     def parse_ams_tray(self, ams_id: int, tray_data: dict) -> AMSTray | None:
     def parse_ams_tray(self, ams_id: int, tray_data: dict) -> AMSTray | None:
         """Parse AMS tray data into AMSTray object.
         """Parse AMS tray data into AMSTray object.
 
 

+ 1 - 0
backend/tests/integration/test_spoolman_api.py

@@ -39,6 +39,7 @@ class TestSpoolmanAPI:
         mock_client.is_connected = True
         mock_client.is_connected = True
         mock_client.base_url = "http://localhost:7912"
         mock_client.base_url = "http://localhost:7912"
         mock_client.health_check = AsyncMock(return_value=True)
         mock_client.health_check = AsyncMock(return_value=True)
+        mock_client.ensure_tag_extra_field = AsyncMock(return_value=True)
         mock_client.get_spools = AsyncMock(return_value=[])
         mock_client.get_spools = AsyncMock(return_value=[])
         mock_client.get_filaments = AsyncMock(return_value=[])
         mock_client.get_filaments = AsyncMock(return_value=[])
         mock_client.create_spool = AsyncMock(return_value={"id": 1})
         mock_client.create_spool = AsyncMock(return_value={"id": 1})