Browse Source

Keep the tag when a spool is lifted during a Spoolman link or quick-add

In Spoolman mode, Assign Spool and Add to Inventory on the SpoolBuddy
kiosk took the tag UID from the live reader state, which is cleared when
the spool leaves the reader. Lifting it while a dialog was open made the
link fail, and quick-add left an untagged spool behind. Use the tag on
the card, as local inventory already does.
maziggy 2 days ago
parent
commit
f31aa90207

+ 1 - 0
CHANGELOG.md

@@ -77,6 +77,7 @@ All notable changes to Bambuddy will be documented in this file.
   - Add to Inventory and Assign Spool on the kiosk save the spool ID with the tag, also when the spool was lifted off the reader while the dialog was open. In Spoolman mode the spool ID is stored as the spool's tag, as the AMS sync already does.
   - Bambuddy ignores a spool ID that is not a real one, so a SpoolBuddy that has not been updated yet keeps matching by tag UID as before and never saves the filament type as an ID.
   - Spools added on the kiosk before this fix get the spool ID the next time the tag they were added with is scanned. After that, the other tag and the AMS find them too. This only happens when the scanned tag UID matches the spool's exactly, the spool has no spool ID yet, and no other spool carries that ID. A near match on the tag UID, which a different reader can produce, never writes anything. In Spoolman mode the spool ID replaces the tag UID as the spool's tag.
+- **In Spoolman mode, lifting a spool off SpoolBuddy while Assign Spool or Add to Inventory was open lost its tag** — Both took the tag from the reader, which forgets it as soon as the spool is lifted, instead of from the card that stays on screen. Assign Spool then only reported that linking failed, and Add to Inventory created the spool without a tag and showed an error. Both now use the tag shown on the card, as local inventory already did. Bambu Lab spools were covered by the #984 fix above; this fixes it for every other tag.
 - **File names could vanish from the File Manager's list view (#3105, reported and contributed by @akshayy29 in #3119)** — With authentication on, the list has an extra Uploaded By column. When the folder sidebar left less room than the fixed columns needed, the Name column shrank to nothing, and the Uploaded By value sat right next to the thumbnail. The Name column now keeps at least 240px and the Tags column at least 96px. When there isn't room for both, the list scrolls sideways (it already could) instead of hiding them.
 - **PostgreSQL installs no longer warn at every start that the connection pool may exceed the server** — The built-in pool allowed up to 100 connections (`pool_size` 20 + `max_overflow` 80), but a stock PostgreSQL allows 97 (`max_connections` 100 minus 3 reserved for superusers). So every install that hadn't set `DB_POOL_SIZE` / `DB_MAX_OVERFLOW` logged `DB pool may exceed what PostgreSQL allows` at startup, and under heavy load could hit `TooManyConnectionsError`. The default `max_overflow` is now 60, an 80-connection ceiling that fits. Installs that set their own values are unaffected, and large farms can still raise both, along with the server's `max_connections`.
 - **AMS slots that lost their K-profile after a printer restart stayed on the default K, and queued jobs printed with it (#3219)** — A printer that is power-cycled can come back with every slot's K-profile selection cleared (`cali_idx` -1) while the spools, tags and remaining amounts are unchanged. Bambuddy only looked at K-profiles when one of those changed, so it never noticed, and the next queued jobs started on the default K without a warning. Now, while the printer is idle, a loaded slot whose selection is gone gets its spool's stored K-profile back, for built-in and Spoolman inventory and for the external spool. The queue also checks the slots a job uses right before sending it. Only a lost selection is restored: a different profile picked in Bambu Studio is left alone, and so is **Default** picked in Bambuddy's Configure Slot dialog. A slot is retried at most three times, 30 seconds apart; if the profile still doesn't stick (for example because it was deleted on the printer), the log says so and Bambuddy stops until the slot changes. On dual-nozzle printers a slot whose nozzle isn't known yet is skipped rather than guessed.

+ 77 - 0
frontend/src/__tests__/pages/SpoolBuddyDashboard.test.tsx

@@ -846,6 +846,83 @@ describe('SpoolBuddyDashboard', () => {
     });
   });
 
+  describe('Spoolman mode, spool lifted while a dialog is open', () => {
+    // The reader state is cleared when the tag leaves the reader; the card and
+    // its dialogs stay. Linking must use the tag the card shows.
+    function renderLiftable() {
+      const setterRef: { current: React.Dispatch<React.SetStateAction<typeof mockOutletContext.sbState>> | null } = { current: null };
+      function DynWrapper() {
+        const [sbState, setSbState] = React.useState({ ...mockOutletContext.sbState, unknownTagUid: 'AABB1122334455FF' });
+        setterRef.current = setSbState;
+        return <Outlet context={{ ...mockOutletContext, sbState }} />;
+      }
+      const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
+      render(
+        <ToastProvider>
+          <QueryClientProvider client={qc}>
+            <MemoryRouter initialEntries={['/spoolbuddy']}>
+              <Routes>
+                <Route element={<DynWrapper />}>
+                  <Route path="spoolbuddy" element={<SpoolBuddyDashboard />} />
+                </Route>
+              </Routes>
+            </MemoryRouter>
+          </QueryClientProvider>
+        </ToastProvider>
+      );
+      return () => act(() => setterRef.current!((prev) => ({ ...prev, unknownTagUid: null, unknownTrayUuid: null })));
+    }
+
+    beforeEach(async () => {
+      const { api } = await import('../../api/client');
+      (api.getSpoolmanSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
+        spoolman_enabled: 'true',
+        spoolman_url: 'http://localhost:7912',
+        spoolman_sync_mode: 'off',
+        spoolman_disable_weight_sync: 'false',
+        spoolman_report_partial_usage: 'false',
+      });
+      (api.getSpoolmanInventorySpools as ReturnType<typeof vi.fn>).mockResolvedValue([
+        { id: 30, material: 'TPU', brand: 'Polymaker', tag_uid: null, tray_uuid: null, archived_at: null, color_name: 'Orange', rgba: 'FF6600FF', subtype: null, label_weight: 1000, core_weight: 250, weight_used: 0 },
+      ]);
+    });
+
+    it('links the tag shown on the card', async () => {
+      const { api } = await import('../../api/client');
+      const lift = renderLiftable();
+
+      fireEvent.click(await waitFor(() => screen.getByText('Assign Spool')));
+      fireEvent.click(await waitFor(() => screen.getByText('Orange')));
+      await waitFor(() => screen.getByText('Link Tag'));
+      lift();
+      fireEvent.click(screen.getByText('Link Tag'));
+
+      await waitFor(() => {
+        expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(30, {
+          tag_uid: 'AABB1122334455FF',
+          tray_uuid: undefined,
+        });
+      });
+    });
+
+    it('quick-adds the spool with the tag shown on the card', async () => {
+      const { api } = await import('../../api/client');
+      const lift = renderLiftable();
+
+      fireEvent.click(await waitFor(() => screen.getAllByText('Add to Inventory')[0]));
+      await waitFor(() => screen.getByText('Add Anyway'));
+      lift();
+      fireEvent.click(screen.getByText('Add Anyway'));
+
+      await waitFor(() => {
+        expect(api.linkTagToSpoolmanSpool).toHaveBeenCalledWith(4, {
+          tag_uid: 'AABB1122334455FF',
+          tray_uuid: undefined,
+        });
+      });
+    });
+  });
+
   describe('Spoolman mode', () => {
     const SPOOLMAN_SPOOL = {
       id: 42, material: 'PLA', subtype: null, brand: 'Bambu',

+ 7 - 8
frontend/src/pages/spoolbuddy/SpoolBuddyDashboard.tsx

@@ -366,13 +366,12 @@ export function SpoolBuddyDashboard() {
       if (spoolmanMode) {
         // The tray UUID wins when both are sent: it is what the AMS stores in
         // extra.tag, and the same on both tags of a Bambu spool (#984).
-        const tag_uid = sbState.unknownTagUid || undefined;
-        const tray_uuid = displayedTrayUuid || undefined;
-        if (!tag_uid && !tray_uuid) {
-          showToast(t('spoolman.linkFailed'), 'error');
-          return;
-        }
-        const raw = await api.linkTagToSpoolmanSpool(spool.id, { tray_uuid, tag_uid });
+        // Both come from the card, not the reader: the live tag state is
+        // cleared when the spool is lifted while the dialog is open.
+        const raw = await api.linkTagToSpoolmanSpool(spool.id, {
+          tag_uid: displayedTagId,
+          tray_uuid: displayedTrayUuid || undefined,
+        });
         const updated = raw as InventorySpool | undefined;
         if (!updated) {
           showToast(t('spoolman.linkFailed'), 'error');
@@ -463,7 +462,7 @@ export function SpoolBuddyDashboard() {
           low_stock_threshold_pct: null,
         } as Parameters<typeof api.createSpoolmanInventorySpool>[0]);
         await api.linkTagToSpoolmanSpool(created.id, {
-          tag_uid: sbState.unknownTagUid || undefined,
+          tag_uid: displayedTagId,
           tray_uuid: displayedTrayUuid || undefined,
         });
       } else {

File diff suppressed because it is too large
+ 0 - 0
static/assets/ImagePreviewModal-Z1sWEvse.js


File diff suppressed because it is too large
+ 0 - 1
static/assets/PdfPreviewModal-DpcLpTaG.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/SpreadsheetPreviewModal-De739xds.js


File diff suppressed because it is too large
+ 1 - 1
static/assets/index-DEcweL3K.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/pdf-BtWDWzUB.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-xVGFNSn1.js"></script>
+    <script type="module" crossorigin src="/assets/index-DEcweL3K.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
     <link rel="stylesheet" crossorigin href="/assets/index-MteGyJn2.css">
   </head>

Some files were not shown because too many files changed in this diff