From 885c4156ae401d4441e58baacfc08c6923d52063 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 20 Sep 2026 11:21:19 +0200 Subject: [PATCH] fix(inventory): enable Clear RFID Tag for a tray-UUID-only spool (issue #3109) The button gated on tag_uid alone. A spool linked by its 32-character Bambu tray UUID carries none -- Bambuddy splits a stored tag by length, so a 32-char value becomes tray_uuid and tag_uid stays empty. In Spoolman mode that is every Bambu Lab spool synced from the AMS; on the reporter's instance, 35 of 39 tagged spools, none of which could have its tag cleared from the dialog. The documented workaround was to edit extra.tag in Spoolman's own interface. Everything around the button already treated those spools as tagged. The Tag ID column renders whichever identifier is present, and the payload the button sends nulls both fields -- which both inventory modes honour: the built-in PATCH applies them through exclude_unset, and the Spoolman route keys its tag-removal branch off either field being explicitly null. Either identifier now enables it, and clearing still removes both. --- CHANGELOG.md | 1 + .../components/SpoolFormModal.test.tsx | 112 ++++++++++++++++++ frontend/src/components/SpoolFormModal.tsx | 9 +- .../{index-Z-jaTirY.js => index-B9X0fIlJ.js} | 2 +- static/index.html | 2 +- 5 files changed, 123 insertions(+), 3 deletions(-) rename static/assets/{index-Z-jaTirY.js => index-B9X0fIlJ.js} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 988001703..09c2daed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to Bambuddy will be documented in this file. - **Every FTP session Bambuddy opens now records how it closed (#3009, reported by @grengojbo)** — the report traced a print completion that opened two FTP connections to the printer, deleted one file and then, as far as the log showed, did nothing else until the printer was powered off 21 minutes later, and concluded the connections were being left open. They were not: the post-print SD-card cleanup opens one connection per candidate filename and closes each in a `finally`, which a run against a real FTPS server confirms at the server end for both the delete and the 550 not-here case. The trouble is that nothing in the log could have said so. Neither the clean close nor the hard socket drop logged anything at any level, so a session closed properly and a socket genuinely abandoned produced the same output — none — and the only way to tell them apart was to read the source. Both now log one DEBUG line naming the printer, whether QUIT was acknowledged or the socket had to be dropped without it, why, and how long the session was held. Every connect in a debug log is now paired with a close, so the next person suspecting a leaked FTP connection can settle it from a support bundle rather than by inference. Nothing about the connection handling itself changed, and at default log level nothing new is printed. This does not explain the SD-card read/write error in that report or in #645; it only removes one theory from the list by making it checkable. ### Fixed +- **"Clear RFID Tag" was permanently greyed out for a spool linked by its Bambu tray UUID (#3109, reported by @Niko11111)** — the button gated on `tag_uid` alone, and a spool linked by its 32-character tray UUID carries none: Bambuddy splits a stored tag by length, so a 32-char value becomes `tray_uuid` and `tag_uid` stays empty. In Spoolman mode that is every Bambu Lab spool synced from the AMS — on the reporter's instance, 35 of the 39 tagged spools, not one of which could have its tag cleared from the dialog. Everything around the button already treated those spools as tagged: the Tag ID column renders whichever identifier is present, and the payload the button sends clears both fields. Only the one condition disagreed. Either identifier now enables it, and clearing still removes both. The documented workaround — editing `extra.tag` in Spoolman's own interface — is no longer needed. - **Assigning a spool to a slot holding a non-Bambu filament configured nothing, and could delete the assignment afterwards (#3084, reported by @anthonyma94; #3100, reported by @Sawtaytoes)** — swap a Bambu spool for one the AMS cannot read and Assign Spool published no `ams_filament_setting` at all: the printer kept showing `?` on its own screen and in the slicer, and only Configure, which publishes unconditionally, put anything there. Three backend decisions were reading the tray's `state` field to answer "is there a spool in this slot", and `state` cannot carry that. It is firmware-variant — the A1 Mini BMCU and the P1S Standard AMS report 3 for a loaded slot and never emit 11, and an AMS-HT reports a *loaded* tray as 9 because it does not feed into a shared buffer (#2594) — and it is also partly Bambuddy's own writing: the empty-slot cleanup stamps `state = 9` on every slot whose `tray_exist_bits` bit is 0, and when the bit comes back it refreshes only the `exists` annotation beside it. Either way a slot the firmware said was full sat in the cache reading `exists=True, state=9` until something configured it, and the deferred configuration that is supposed to rescue a pre-assigned slot could not fire either, because its own "has a spool appeared" test was the same 9. That is the deadlock removed from the assign path in #1322, still in place one step further along. The same 9 is what deleted the assignments in #3100: the replay never fired, so the row kept the empty fingerprint it was stored with, and the first tray report that named a filament was read as a spool swap and unlinked. Every one of the three now reads `tray_exist_bits` first, which is the bitmask firmware actually answers this question with and the one the printer card has drawn its `?` from since #2527; `state` remains the fallback for the external spool and anything else whose payload carries no presence bit. A blank tray report from a slot the bit calls occupied no longer unlinks anything either — off a print as well as during one — because a filament the AMS cannot identify is not a filament that was removed. - **The Finance page showed euros whatever currency the install was set to (#3123)** — it was the only surface in Bambuddy that read its currency from a data row (`user_wallets.currency`) rather than the `currency` setting, and it fell back to EUR where every other page falls back to USD. One variable drives every amount on that page, so the personal balance, the cost-center budgets and the whole transaction list were all wrong together. The backend was the other half of it: of the four places that settle on a currency, three wrote a hardcoded `"EUR"` — the wallet the API mints on demand, the wallet a print charge mints when none exists, and the balance returned for a user with no wallet row at all. The page now takes the configured currency from `/settings/ui-flags`, which is readable by anyone who can see Finance, and the API reports the setting. The `user_wallets.currency` column is gone altogether — an install has one currency and nothing here converts between them, so a per-wallet copy could only ever drift from the setting; a startup migration drops it from existing databases on both SQLite and PostgreSQL. Saving settings now also refreshes those flags, which nothing invalidated before. - **On Windows and macOS the Virtual Printer's bind dropdown offered one entry per network adapter, not one per IP address (#3121, reported by @SJB-OLVG)** — each enabled virtual printer needs its own address, and the documented way to get several is to add secondary IPs to the adapter you already have. Linux reads those back through `ip -j addr show`, which reports every address; Windows and macOS have no `ip` command and fell through to an enumeration that stopped at the first IPv4 of each adapter. A Windows host with three addresses on one NIC therefore offered exactly one bind target, and creating a second virtual printer could only fail with "Bind IP ... is already in use". Every address is now listed on those platforms too, with the ones after an adapter's first marked as aliases exactly as on Linux. A Linux host without `iproute2` installed gains the same thing, having previously been limited to one address per interface for the same reason. The wiki gained the Windows instructions for adding those addresses, which it had for every other platform but that one, and the creation steps it describes now match the dialog — bind interface, model and access code are set on the printer's card after **Create**, not in the dialog itself. diff --git a/frontend/src/__tests__/components/SpoolFormModal.test.tsx b/frontend/src/__tests__/components/SpoolFormModal.test.tsx index 9537ae693..86dea10d3 100644 --- a/frontend/src/__tests__/components/SpoolFormModal.test.tsx +++ b/frontend/src/__tests__/components/SpoolFormModal.test.tsx @@ -1063,6 +1063,118 @@ describe('SpoolFormModal — Unassign button (#1336)', () => { }); }); +describe('SpoolFormModal — Clear RFID Tag for a tray-UUID-only spool (#3109)', () => { + const trayUuidOnly = (overrides: Partial): InventorySpool => + ({ + ...existingSpool, + id: 42, + tag_uid: null, + tray_uuid: 'A1B2C3D4E5F60718293A4B5C6D7E8F90', + ...overrides, + }) as InventorySpool; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('clears the tag on a Spoolman spool linked only by its tray UUID', async () => { + // _map_spoolman_spool splits extra.tag by length: a 32-char value becomes + // tray_uuid and tag_uid stays None. That is every Bambu Lab spool synced + // from the AMS, and not one of them could have its tag cleared here. + render( + + ); + + const clearBtn = await screen.findByRole('button', { name: /clear rfid tag/i }); + expect(clearBtn).not.toBeDisabled(); + + fireEvent.click(clearBtn); + + await waitFor(() => { + expect(api.updateSpoolmanInventorySpool).toHaveBeenCalledWith( + 42, + expect.objectContaining({ tag_uid: null, tray_uuid: null }) + ); + }); + expect(api.updateSpool).not.toHaveBeenCalled(); + }); + + it('clears the tag on a built-in spool linked only by its tray UUID', async () => { + // PATCH /inventory/spools/{id}/link-tag takes tray_uuid on its own, so the + // built-in inventory reaches the same state without Spoolman involved. + render( + + ); + + const clearBtn = await screen.findByRole('button', { name: /clear rfid tag/i }); + expect(clearBtn).not.toBeDisabled(); + + fireEvent.click(clearBtn); + + await waitFor(() => { + expect(api.updateSpool).toHaveBeenCalledWith( + 42, + expect.objectContaining({ tag_uid: null, tray_uuid: null }) + ); + }); + expect(api.updateSpoolmanInventorySpool).not.toHaveBeenCalled(); + }); + + it('stays disabled for a spool carrying neither identifier', async () => { + // The button still has something to gate on -- it is not simply always on. + render( + + ); + + const clearBtn = await screen.findByRole('button', { name: /clear rfid tag/i }); + expect(clearBtn).toBeDisabled(); + }); + + it('still clears the tag on a spool carrying a tag_uid', async () => { + const clearBtnSpool = trayUuidOnly({ tag_uid: 'DEADBEEF', tray_uuid: null }); + render( + + ); + + const clearBtn = await screen.findByRole('button', { name: /clear rfid tag/i }); + expect(clearBtn).not.toBeDisabled(); + + fireEvent.click(clearBtn); + + await waitFor(() => { + expect(api.updateSpool).toHaveBeenCalledWith( + 42, + expect.objectContaining({ tag_uid: null, tray_uuid: null }) + ); + }); + }); +}); + describe('SpoolFormModal locationIdTouched', () => { /** * Regression tests for the round-trip bug: saving the edit modal without diff --git a/frontend/src/components/SpoolFormModal.tsx b/frontend/src/components/SpoolFormModal.tsx index 170a749f6..d1edd6eef 100644 --- a/frontend/src/components/SpoolFormModal.tsx +++ b/frontend/src/components/SpoolFormModal.tsx @@ -1128,10 +1128,17 @@ export function SpoolFormModal({
{isEditing && (
+ {/* Either identifier counts as "tagged". A Bambu Lab spool is + linked by its 32-char tray UUID and carries no tag_uid at all + -- in Spoolman mode that is every Bambu spool, because + _map_spoolman_spool splits extra.tag by length -- so gating on + tag_uid alone left this permanently greyed out for them, while + the Tag ID column beside it showed the UUID and the payload + below already cleared both fields (#3109). */}