diff --git a/backend/app/services/spoolman.py b/backend/app/services/spoolman.py index 3e3de5976..e89d884d2 100644 --- a/backend/app/services/spoolman.py +++ b/backend/app/services/spoolman.py @@ -9,6 +9,7 @@ import httpx logger = logging.getLogger(__name__) +BAMBU_RFID_TAG_LENGTH = 32 @dataclass class SpoolmanSpool: @@ -553,6 +554,10 @@ class SpoolmanClient: else: spool_uuid = "" + # Only clear location for Bambu Lab spools (those with a stored 32-character RFID tag). + if len(spool_uuid) != BAMBU_RFID_TAG_LENGTH: + continue + # If this spool's UUID is not in the current AMS, clear its location if spool_uuid not in current_tray_uuids: logger.info( diff --git a/backend/tests/unit/services/test_spoolman_service.py b/backend/tests/unit/services/test_spoolman_service.py index 8697a88c2..ffcc66766 100644 --- a/backend/tests/unit/services/test_spoolman_service.py +++ b/backend/tests/unit/services/test_spoolman_service.py @@ -298,11 +298,15 @@ class TestSpoolmanClient: async def test_clear_location_for_removed_spools_with_cached_spools(self, client): """Verify clear_location_for_removed_spools uses cached spools.""" cached = [ - {"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"TAG1"'}}, - {"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"TAG2"'}}, - {"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"TAG3"'}}, + {"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"A1B2C3D4E5F60718293A4B5C6D7E8F90"'}}, + {"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"B1C2D3E4F5061728394A5B6C7D8E9F01"'}}, + {"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"C1D2E3F40516273849A5B6C7D8E9F012"'}}, ] - current_tags = {"TAG1", "TAG2"} # TAG3 was removed + # Tag 3 was cleared, so only tags 1 and 2 are current + current_tags = { + "A1B2C3D4E5F60718293A4B5C6D7E8F90", + "B1C2D3E4F5061728394A5B6C7D8E9F01", + } with ( patch.object(client, "get_spools", AsyncMock()) as mock_get, diff --git a/frontend/src/__tests__/components/LinkSpoolModal.test.tsx b/frontend/src/__tests__/components/LinkSpoolModal.test.tsx index 8aa3129fe..a7a50bcb1 100644 --- a/frontend/src/__tests__/components/LinkSpoolModal.test.tsx +++ b/frontend/src/__tests__/components/LinkSpoolModal.test.tsx @@ -117,7 +117,7 @@ describe('LinkSpoolModal', () => { }); describe('linking', () => { - it('calls linkSpool on spool click', async () => { + it('uses trayUuid when linking if present (Bambu spool path)', async () => { render(); await waitFor(() => { @@ -126,6 +126,25 @@ describe('LinkSpoolModal', () => { fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!); + await waitFor(() => { + expect(api.linkSpool).toHaveBeenCalledWith(1, { + spoolTag: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4', + printerId: 1, + amsId: 0, + trayId: 0, + }); + }); + }); + + it('falls back to tagUid when trayUuid is missing (generic spool path)', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!); + await waitFor(() => { expect(api.linkSpool).toHaveBeenCalledWith(1, { spoolTag: 'ABCD1234', diff --git a/frontend/src/components/FilamentHoverCard.tsx b/frontend/src/components/FilamentHoverCard.tsx index cb7c501fb..3984ee48f 100644 --- a/frontend/src/components/FilamentHoverCard.tsx +++ b/frontend/src/components/FilamentHoverCard.tsx @@ -21,6 +21,7 @@ interface SpoolmanConfig { onUnlinkSpool?: () => void; linkedSpoolId?: number | null; // Spoolman spool ID if this tray is already linked spoolmanUrl?: string | null; // Base URL for Spoolman (for "Open in Spoolman" link) + syncMode?: string | null; // If auto-sync is enabled, we may want to hide the unlink option for Bambu spools } interface InventoryConfig { @@ -305,7 +306,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp {t('spoolman.openInSpoolman')} - {spoolman.onUnlinkSpool && data.vendor !== 'Bambu Lab' && ( + {spoolman.onUnlinkSpool && (data.vendor !== 'Bambu Lab' || spoolman.syncMode === 'manual') && (