Kaynağa Gözat

Spoolman mode: slots only from Spoolman's assignments (issue #3430)

- Since #2812 a switch to Spoolman keeps the built-in slot rows, and
  the Inventory page merged them ahead of Spoolman's. Their spool ids
  are built-in ids, so a kept row put a slot on whichever Spoolman
  spool had the same number, with nothing to clear it: a spool taken
  out of the AMS kept its badge.
- Inventory page: built-in assignments only in built-in mode, also
  when another page put them in the shared query cache.
- Printer card: no built-in fill level while Spoolman is on, and no
  built-in Assign / Unassign while it is on but unreachable (Assign
  wrote a built-in row in Spoolman mode).
- SpoolBuddy AMS page and Assign to AMS: no fill from built-in rows
  in Spoolman mode.
- Tests replace the pre-#2812 "local wins on id collision" case.
maziggy 1 gün önce
ebeveyn
işleme
62fad59fd7
32 değiştirilmiş dosya ile 257 ekleme ve 27 silme
  1. 1 0
      CHANGELOG.md
  2. 41 0
      frontend/src/__tests__/components/AssignToAmsModal.test.tsx
  3. 99 10
      frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx
  4. 52 3
      frontend/src/__tests__/pages/PrintersPageAmsSlotActionsWiring.test.tsx
  5. 23 0
      frontend/src/__tests__/pages/SpoolBuddyAmsPage.test.tsx
  6. 4 3
      frontend/src/components/spoolbuddy/AssignToAmsModal.tsx
  7. 12 6
      frontend/src/pages/InventoryPage.tsx
  8. 19 1
      frontend/src/pages/PrintersPage.tsx
  9. 4 2
      frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx
  10. 0 0
      static/assets/ArchivesPage-CpcbZlCY.js
  11. 0 0
      static/assets/AssignToAmsModal-C421iyiz.js
  12. 0 0
      static/assets/AssignToAmsModal-DH0wi62b.js
  13. 0 0
      static/assets/CamWallPage-Ceuo5cN7.js
  14. 0 0
      static/assets/CameraPage-DBqX6f6U.js
  15. 0 0
      static/assets/FileManagerPage-BhNE8zRU.js
  16. 0 0
      static/assets/InventoryPage-Bmlat63M.js
  17. 0 0
      static/assets/InventoryPage-CaIFj3z6.js
  18. 0 0
      static/assets/ModelSourcesPage-DokANRvL.js
  19. 0 0
      static/assets/PrintersPage-4t5tmqMm.js
  20. 0 0
      static/assets/PrintersPage-DP4udQlz.js
  21. 0 0
      static/assets/SettingsPage-Dec0ACaM.js
  22. 0 0
      static/assets/SliceModal-C9Qeeus7.js
  23. 0 0
      static/assets/SpoolBuddyAmsPage-DJIsj8oR.js
  24. 0 0
      static/assets/SpoolBuddyAmsPage-DpXAS9Dr.js
  25. 0 0
      static/assets/SpoolBuddyDashboard-C1zWfGh5.js
  26. 0 0
      static/assets/SpoolBuddyInventoryPage-0WsbJpYW.js
  27. 1 1
      static/assets/SpoolBuddySettingsPage-CqAAyJj4.js
  28. 0 0
      static/assets/index-DNGt_wr5.js
  29. 0 0
      static/assets/locationSensorDefaults-DM_LlId0.js
  30. 0 0
      static/assets/spoolCost-C-H4uQrM.js
  31. 0 0
      static/assets/usePageFileDrop-DjyMXsMg.js
  32. 1 1
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -217,6 +217,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Spoolman mode no longer shows slots from the built-in inventory (#3430, reported by @sean3318)** — Since switching inventory modes keeps both modes' slot assignments, the Inventory page in Spoolman mode also read the built-in inventory's and gave them priority. Its spool numbers are its own, so a built-in assignment kept from before the switch put a printer slot on whichever Spoolman spool had the same number, and nothing in Spoolman mode could clear it: a spool taken out of the AMS kept its slot. Spoolman mode now only uses Spoolman's slot assignments: on the Inventory page, on the printer card and in SpoolBuddy (a slot's fill level came from the built-in spool too), also while Spoolman is unreachable, when the printer card offered to assign and unassign built-in spools. The built-in assignments are kept for when you switch back.
 - **A spool whose tag is read in a slot leaves its other slots (#3427, reported by @Spegeli)** — When an AMS read a spool's tag, Bambuddy assigned the spool to that slot and left any other slot it was assigned to alone. A spool assigned by hand to an empty slot of one printer and then loaded into another was assigned to both: the first printer's slot was configured from it once a spool went in there, and its prints were charged to a spool sitting in the other printer. A tag read now takes the spool out of its other slots, as moving it by hand does; those slots keep what the printer has set, as after unassigning. A slot whose printer reports the same tag keeps its assignment. Internal inventory and Spoolman alike.
 - **AMS Filament Backup pools spools the printer itself treats as backups (#1824, reported by @lightmaster)** — With Filament Backup on, a print short on its assigned spool is only held when all spools of that filament together are short. Which spools count as the same filament was decided from the spools in the inventory, by their preset as stored: a spool created from its RFID tag stores `GFA10`, one set up from the preset picker `GFSA10_02` or a user preset of its own, so two rolls of the same PLA Tough+ never pooled, and Start reported "Insufficient filament for the assigned spool" for a 194 g print with 142 g in one slot and 1000 g in the other, while the printer listed both for Auto Refill. The pool now follows what the printer reports for each tray, its filament preset and colour, which is what it switches on; the remaining grams still come from the inventory. Internal inventory and Spoolman alike, for the queue's Start, the dispatcher and the print dialog's warning.
 - **One plate of a multi-plate file is measured against that plate (#3375, reported by @sargonas)** — The slicer's estimate on an archive adds all plates of the file up, so printing one plate of a three-plate file showed +200 % on the card, an archive printed more than once showed no badge at all, and statistics only kept time accuracies between 50 % and 200 %. An archive now keeps each plate's estimate from its 3MF (archives made before get theirs in the background after the update), and each print is measured against the plate it printed: the card's badge compares the latest print with its plate's estimate, the archive window shows each print's duration and filament next to its plate's estimate, the statistics' time accuracy goes by each print's plate, and the print start and queue job started notifications give the plate's time (the printer's own remaining time when the plate of a multi-plate file is not known). Moving a queued job to another plate now also updates the time shortest-job-first orders it by. A print of a multi-plate file whose plate is not known gets no estimate rather than the whole file's. Wiki updated.

+ 41 - 0
frontend/src/__tests__/components/AssignToAmsModal.test.tsx

@@ -326,6 +326,47 @@ describe('AssignToAmsModal', () => {
     });
   });
 
+  describe('fill levels by inventory mode (#3430)', () => {
+    const loaded = {
+      ...PRINTER_STATUS_ONLINE,
+      ams: [
+        {
+          ...PRINTER_STATUS_ONLINE.ams[0],
+          tray: [
+            { id: 0, ...BLANK_TRAY, tray_type: 'PLA', tray_color: 'FF0000FF', remain: 100 },
+            { id: 1, ...BLANK_TRAY },
+            { id: 2, ...BLANK_TRAY },
+            { id: 3, ...BLANK_TRAY },
+          ],
+        },
+      ],
+    };
+    // A built-in row for slot 0 at 10 % left; the AMS itself reports 100 %.
+    const keptRow = [
+      { id: 1, spool_id: 42, printer_id: 1, ams_id: 0, tray_id: 0, spool: { label_weight: 1000, weight_used: 900 } },
+    ];
+
+    async function fillWidths(spoolmanMode: boolean) {
+      vi.mocked(api.getPrinterStatus).mockResolvedValue(loaded as never);
+      vi.mocked(api.getAssignments).mockResolvedValue(keptRow as never);
+      const { container } = render(
+        <AssignToAmsModal isOpen onClose={() => {}} spool={SPOOL as never} printerId={1} spoolmanMode={spoolmanMode} />,
+      );
+      await waitFor(() => expect(container.querySelector('[style*="width"]')).not.toBeNull());
+      await waitFor(() => expect(api.getAssignments).toHaveBeenCalled());
+      await new Promise((r) => setTimeout(r, 20));
+      return [...container.querySelectorAll<HTMLElement>('[style*="width"]')].map((el) => el.style.width);
+    }
+
+    it('Spoolman mode ignores a kept built-in row', async () => {
+      expect(await fillWidths(true)).toContain('100%');
+    });
+
+    it('built-in mode uses it', async () => {
+      expect(await fillWidths(false)).toContain('10%');
+    });
+  });
+
   describe('API routing based on spoolmanMode', () => {
     it('calls assignSpool when spoolmanMode is false', async () => {
       const user = userEvent.setup();

+ 99 - 10
frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx

@@ -13,6 +13,8 @@ import { render } from '../utils';
 import InventoryPageRouter from '../../pages/InventoryPage';
 import { http, HttpResponse } from 'msw';
 import { server } from '../mocks/server';
+import { useQuery } from '@tanstack/react-query';
+import { api } from '../../api/client';
 
 // Full settings shape — pattern matches InventoryPageLowStock.test.tsx.
 const mockSettings = {
@@ -202,6 +204,56 @@ describe('InventoryPage - LOCATION column (Spoolman mode)', () => {
     expect(slotEndpointCalled).toBe(false);
   });
 
+  it('built-in rows another page put in the shared cache stay off a Spoolman spool (#3430)', async () => {
+    // The printer page fetches ['spool-assignments'] in either mode; a
+    // disabled query still hands out what is cached under its key.
+    function OtherPage() {
+      useQuery({ queryKey: ['spool-assignments'], queryFn: () => api.getAssignments() });
+      return null;
+    }
+    server.use(
+      http.get('/api/v1/settings/spoolman', () =>
+        HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
+      ),
+      http.get('/api/v1/spoolman/inventory/spools', () => HttpResponse.json([mockSpoolmanSpool])),
+      http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
+      http.get('/api/v1/inventory/assignments', () =>
+        HttpResponse.json([
+          { id: 99, spool_id: 216, printer_id: 7, printer_name: 'LocalPrinter', ams_id: 0, tray_id: 0, ams_label: null, created_at: '2025-01-01T00:00:00Z' },
+        ])
+      ),
+    );
+
+    const { container } = render(
+      <>
+        <OtherPage />
+        <InventoryPageRouter />
+      </>
+    );
+    await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
+    await new Promise((r) => setTimeout(r, 50));
+    expect(container.textContent).not.toContain('LocalPrinter');
+  });
+
+  it('does not fetch built-in assignments in Spoolman mode', async () => {
+    let builtInFetched = false;
+    server.use(
+      http.get('/api/v1/settings/spoolman', () =>
+        HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
+      ),
+      http.get('/api/v1/spoolman/inventory/spools', () => HttpResponse.json([mockSpoolmanSpool])),
+      http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
+      http.get('/api/v1/inventory/assignments', () => {
+        builtInFetched = true;
+        return HttpResponse.json([]);
+      }),
+    );
+
+    const { container } = render(<InventoryPageRouter />);
+    await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
+    expect(builtInFetched).toBe(false);
+  });
+
   it('counts spoolman slot assignments in the IN PRINTER stat card', async () => {
     server.use(
       http.get('/api/v1/settings/spoolman', () =>
@@ -240,11 +292,10 @@ describe('InventoryPage - LOCATION column (Spoolman mode)', () => {
     });
   });
 
-  it('local SpoolAssignment wins over Spoolman slot assignment on id collision', async () => {
-    // Both endpoints return an entry with the same numeric id (216). The local
-    // /inventory/assignments source must win — printer_name "LocalPrinter" and
-    // slot "B1" (formatSlotLabel(1, 0, false, false)) — and the Spoolman entry
-    // ("SpoolmanPrinter" / "C4") must NOT appear.
+  it('a built-in assignment kept from built-in mode is not shown on a Spoolman spool (#3430)', async () => {
+    // Since #2812 built-in rows survive a switch to Spoolman. Their spool_id is
+    // a built-in id, so built-in spool 216 in "LocalPrinter" B1 must not label
+    // Spoolman spool 216, which Spoolman has in "SpoolmanPrinter" C4.
     server.use(
       http.get('/api/v1/settings/spoolman', () =>
         HttpResponse.json({
@@ -286,12 +337,50 @@ describe('InventoryPage - LOCATION column (Spoolman mode)', () => {
     const { container } = render(<InventoryPageRouter />);
 
     await waitFor(() => {
-      // Local printer wins
-      expect(container.textContent).toContain('LocalPrinter');
-      expect(container.textContent).toContain('B1');
+      expect(container.textContent).toContain('SpoolmanPrinter');
+      expect(container.textContent).toContain('C4');
     });
-    expect(container.textContent).not.toContain('SpoolmanPrinter');
-    expect(container.textContent).not.toContain('C4');
+    expect(container.textContent).not.toContain('LocalPrinter');
+  });
+
+  it('a spool Spoolman has in no slot shows no slot, whatever the built-in table holds (#3430)', async () => {
+    // The reporter's case: the spool was taken out and Spoolman's row removed,
+    // but a kept built-in row with the same number still named a slot.
+    server.use(
+      http.get('/api/v1/settings/spoolman', () =>
+        HttpResponse.json({
+          spoolman_enabled: 'true',
+          spoolman_url: 'http://localhost:7912',
+        })
+      ),
+      http.get('/api/v1/spoolman/inventory/spools', () =>
+        HttpResponse.json([mockSpoolmanSpool])
+      ),
+      http.get('/api/v1/inventory/assignments', () =>
+        HttpResponse.json([
+          {
+            id: 99,
+            spool_id: 216,
+            printer_id: 7,
+            printer_name: 'LocalPrinter',
+            ams_id: 0,
+            tray_id: 0,
+            ams_label: null,
+            created_at: '2025-01-01T00:00:00Z',
+          },
+        ])
+      ),
+      http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
+    );
+
+    const { container } = render(<InventoryPageRouter />);
+
+    await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
+    expect(container.textContent).not.toContain('LocalPrinter');
+    // Nor counted as in a printer.
+    const label = screen.getByText(/^in printer$/i);
+    const card = label.closest('div.bg-bambu-dark-secondary');
+    expect(within(card as HTMLElement).getByText('0')).toBeInTheDocument();
   });
 });
 

+ 52 - 3
frontend/src/__tests__/pages/PrintersPageAmsSlotActionsWiring.test.tsx

@@ -139,9 +139,13 @@ interface SetupOptions {
   status: JsonBodyType;
   spoolman?: boolean;
   amsLabels?: Record<number, string>;
+  /** Spoolman enabled but not answering. */
+  spoolmanDown?: boolean;
+  /** A built-in row kept from built-in mode (#2812), served in Spoolman mode too. */
+  keptBuiltIn?: boolean;
 }
 
-function setup({ status, spoolman = false, amsLabels = {} }: SetupOptions) {
+function setup({ status, spoolman = false, amsLabels = {}, spoolmanDown = false, keptBuiltIn = false }: SetupOptions) {
   const sent: Sent[] = [];
   const record =
     (response: JsonBodyType = { success: true, message: 'ok', status: 'ok' }) =>
@@ -157,12 +161,12 @@ function setup({ status, spoolman = false, amsLabels = {} }: SetupOptions) {
     http.get('/api/v1/printers/:id/status', () => HttpResponse.json(status)),
     http.get('/api/v1/printers/:id/ams-labels', () => HttpResponse.json(amsLabels)),
     http.get('/api/v1/spoolman/status', () =>
-      HttpResponse.json({ enabled: spoolman, connected: spoolman, url: spoolman ? 'http://spoolman' : null })
+      HttpResponse.json({ enabled: spoolman, connected: spoolman && !spoolmanDown, url: spoolman ? 'http://spoolman' : null })
     ),
     // Built-in inventory: slot 1 of AMS 0 holds spool 77.
     http.get('/api/v1/inventory/assignments', () =>
       HttpResponse.json(
-        spoolman
+        spoolman && !keptBuiltIn
           ? []
           : [
               {
@@ -307,6 +311,51 @@ describe('AMS slot: Unassign routes by inventory mode', () => {
     );
   });
 
+  it('Spoolman mode with Spoolman down: the built-in table is offered neither way (#3430)', async () => {
+    // Before, the card fell back to the built-in table whenever Spoolman was
+    // not connected: it offered to unassign a row of the other mode, and to
+    // assign, which wrote one.
+    setup({ status: statusWith([amsUnit()]), spoolman: true, spoolmanDown: true, keptBuiltIn: true });
+    for (const slot of [1, 2]) {
+      await hover((await slots())[slot], 'Re-read RFID');
+      await settle();
+      expect(screen.queryByRole('button', { name: 'Unassign' })).toBeNull();
+      expect(screen.queryByRole('button', { name: /Assign Spool/ })).toBeNull();
+      fireEvent.mouseLeave((await slots())[slot]);
+    }
+  });
+
+  it('Spoolman down: no built-in Assign on an AMS-HT slot either (#3430)', async () => {
+    setup({ status: statusWith([htUnit]), spoolman: true, spoolmanDown: true });
+    const [ht] = await slots();
+    await hover(ht, 'Re-read RFID');
+    await settle();
+    expect(screen.queryByRole('button', { name: /Assign Spool/ })).toBeNull();
+  });
+
+  it('Spoolman down: no built-in Assign on the external slot either (#3430)', async () => {
+    setup({
+      status: statusWith([], { vt_tray: [{ id: 254, ...baseTray, tray_color: 'FFFFFFFF' }] }),
+      spoolman: true,
+      spoolmanDown: true,
+    });
+    const [ext] = await slots();
+    await hover(ext, 'Load');
+    await settle();
+    expect(screen.queryByRole('button', { name: /Assign Spool/ })).toBeNull();
+  });
+
+  it('built-in mode offers Assign on the external slot', async () => {
+    setup({ status: statusWith([], { vt_tray: [{ id: 254, ...baseTray, tray_color: 'FFFFFFFF' }] }) });
+    const [ext] = await slots();
+    expect(await hover(ext, 'Assign Spool')).toBeInTheDocument();
+  });
+
+  it('built-in mode still offers Assign on an empty slot', async () => {
+    setup({ status: statusWith([amsUnit()]) });
+    expect(await hover((await slots())[2], 'Assign Spool')).toBeEnabled();
+  });
+
   it('an unassigned slot offers no Unassign', async () => {
     setup({ status: statusWith([amsUnit()]) });
     await hover((await slots())[2], 'Re-read RFID');

+ 23 - 0
frontend/src/__tests__/pages/SpoolBuddyAmsPage.test.tsx

@@ -207,6 +207,29 @@ describe('SpoolBuddyAmsPage Phase 13', () => {
     mockShowToast.mockClear();
   });
 
+  describe('fill levels by inventory mode (#3430)', () => {
+    // Slot 1 (tray 0) reports 80 %; a built-in row for it says 10 % is left.
+    async function slotOneWidths() {
+      apiResponses.getAssignments = [
+        { id: 1, spool_id: 2, printer_id: 1, ams_id: 0, tray_id: 0, spool: { label_weight: 1000, weight_used: 900 } },
+      ];
+      renderPage();
+      const slot = await findAmsSlot(1);
+      await waitFor(() => expect(apiCallCounts.getAssignments).toBeGreaterThan(0));
+      await new Promise((r) => setTimeout(r, 30));
+      return [...slot.querySelectorAll<HTMLElement>('[style*="width"]')].map((el) => el.style.width);
+    }
+
+    it('built-in mode fills the slot from its assignment', async () => {
+      expect(await slotOneWidths()).toContain('10%');
+    });
+
+    it('Spoolman mode, even while Spoolman is down, ignores a kept built-in row', async () => {
+      spoolmanStatusValue = { enabled: true, connected: false };
+      expect(await slotOneWidths()).toContain('80%');
+    });
+  });
+
   describe('P13-4 — AssignSpoolModal receives spoolmanEnabled prop', () => {
     it('passes spoolmanEnabled=false in local mode', async () => {
       spoolmanStatusValue = { enabled: false, connected: false };

+ 4 - 3
frontend/src/components/spoolbuddy/AssignToAmsModal.tsx

@@ -226,10 +226,11 @@ export function AssignToAmsModal({
     ? spoolmanAssignments.find(a => a.spoolman_spool_id === spool.id)
     : undefined;
 
-  // Build fill-level override map from inventory assignments
+  // Build fill-level override map from inventory assignments, in built-in
+  // mode only: kept rows from it would fill Spoolman-mode slots (#3430).
   const fillOverrides = useMemo(() => {
     const map: Record<string, number> = {};
-    if (!assignments) return map;
+    if (!assignments || spoolmanMode) return map;
     for (const a of assignments) {
       const sp = a.spool;
       if (sp && sp.label_weight > 0 && sp.weight_used != null) {
@@ -238,7 +239,7 @@ export function AssignToAmsModal({
       }
     }
     return map;
-  }, [assignments]);
+  }, [assignments, spoolmanMode]);
 
   const amsThresholds: AmsThresholds | undefined = settings ? {
     humidityGood: Number(settings.ams_humidity_good) || 40,

+ 12 - 6
frontend/src/pages/InventoryPage.tsx

@@ -866,11 +866,18 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
     t,
   ]);
 
-  const { data: assignments } = useQuery({
+  // Built-in slot assignments, in built-in mode only. Since #2812 they are kept
+  // through a switch to Spoolman, and their spool ids are built-in ids: merged
+  // into the Spoolman view they put a badge on whichever Spoolman spool shares
+  // the number, with nothing there to clear it (#3430). The cache is shared
+  // with pages that fetch it regardless, so the data is dropped here as well.
+  const { data: builtInAssignments } = useQuery({
     queryKey: ['spool-assignments'],
     queryFn: () => api.getAssignments(),
     refetchInterval: 30000,
+    enabled: spoolmanModeReady && !spoolmanMode,
   });
+  const assignments = spoolmanMode ? undefined : builtInAssignments;
 
   // Spoolman-mode slot assignments. spool.id IS the spoolman_spool_id, so this
   // feeds into the same assignmentMap that the LOCATION column reads.
@@ -1215,11 +1222,10 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
 
   const currencySymbol = getCurrencySymbol(settings?.currency || 'USD');
 
-  // Map spool_id -> location display data for the LOCATION column.
-  // Local SpoolAssignment entries first, then Spoolman SlotAssignment fills in
-  // remaining IDs. Local wins on collision (defensive — modes are exclusive in
-  // practice, but a stray pair with the same numeric id would otherwise be
-  // unpredictable). spool.id IS the spoolman_spool_id in Spoolman mode.
+  // Map spool_id -> location display data for the LOCATION column. Only one
+  // source is ever filled: built-in assignments in built-in mode, Spoolman's
+  // slot assignments in Spoolman mode, where spool.id IS the spoolman_spool_id
+  // (see `assignments` above, #3430).
   const assignmentMap = useMemo<Record<number, LocationDisplay>>(() => {
     const map: Record<number, LocationDisplay> = {};
     for (const a of assignments || []) {

+ 19 - 1
frontend/src/pages/PrintersPage.tsx

@@ -2051,6 +2051,7 @@ function PrinterCard({
   cardSize = 2,
   amsThresholds,
   spoolmanEnabled = false,
+  spoolmanModeOn = false,
   linkedSpools,
   spoolmanUrl,
   onGetAssignment,
@@ -2090,6 +2091,8 @@ function PrinterCard({
     tempFair: number;
   };
   spoolmanEnabled?: boolean;
+  /** Spoolman mode is on, connected or not: the built-in slot rows don't apply (#3430). */
+  spoolmanModeOn?: boolean;
   linkedSpools?: Record<string, LinkedSpoolInfo>;
   spoolmanUrl?: string | null;
   onGetAssignment?: (printerId: number, amsId: number, trayId: number) => SpoolAssignment | undefined;
@@ -5756,6 +5759,9 @@ function PrinterCard({
                                               isAssigned: !!slotAssignment || isBambuLabSpool(tray),
                                             };
                                           }
+                                          // Spoolman mode with Spoolman down: neither table may be offered
+                                          // here, and an assign would write a built-in row (#3430).
+                                          if (spoolmanModeOn) return undefined;
                                           const assignment = onGetAssignment?.(printer.id, ams.id, slotIdx);
                                           return {
                                             assignedSpool: assignment?.spool ? {
@@ -6159,6 +6165,9 @@ function PrinterCard({
                                           isAssigned: !!slotAssignment || isBambuLabSpool(tray),
                                         };
                                       }
+                                      // Spoolman mode with Spoolman down: neither table may be offered
+                                      // here, and an assign would write a built-in row (#3430).
+                                      if (spoolmanModeOn) return undefined;
                                       const assignment = onGetAssignment?.(printer.id, ams.id, htSlotId);
                                       return {
                                         assignedSpool: assignment?.spool ? {
@@ -6438,6 +6447,9 @@ function PrinterCard({
                                             isAssigned: !!slotAssignment || isBambuLabSpool(extTray),
                                           };
                                         }
+                                        // Spoolman mode with Spoolman down: neither table may be offered
+                                        // here, and an assign would write a built-in row (#3430).
+                                        if (spoolmanModeOn) return undefined;
                                         const assignment = onGetAssignment?.(printer.id, 255, slotTrayId);
                                         return {
                                           assignedSpool: assignment?.spool ? {
@@ -9104,8 +9116,12 @@ export function PrintersPage() {
     onError: (error: Error) => showToast(error.message || t('spoolman.unlinkFailed'), 'error'),
   });
 
-  // Helper to find assignment for a specific slot
+  // Helper to find assignment for a specific slot. Built-in rows only count in
+  // built-in mode: kept through a switch to Spoolman (#2812), they would
+  // otherwise supply a slot's fill level there (#3430). Off whenever Spoolman
+  // is enabled, connected or not.
   const getAssignment = (printerId: number, amsId: number | string, trayId: number | string): SpoolAssignment | undefined => {
+    if (spoolmanStatus?.enabled) return undefined;
     return spoolAssignments?.find(
       (a) => a.printer_id === printerId && a.ams_id === Number(amsId) && a.tray_id === Number(trayId)
     );
@@ -10003,6 +10019,7 @@ export function PrintersPage() {
                         tempFair: Number(settings.ams_temp_fair) || 35,
                       } : undefined}
                       spoolmanEnabled={spoolmanEnabled}
+                      spoolmanModeOn={!!spoolmanStatus?.enabled}
                       linkedSpools={linkedSpools}
                       spoolmanUrl={spoolmanStatus?.url}
                       onGetAssignment={getAssignment}
@@ -10048,6 +10065,7 @@ export function PrintersPage() {
               viewMode={viewMode}
               cardSize={cardSize}
               spoolmanEnabled={spoolmanEnabled}
+              spoolmanModeOn={!!spoolmanStatus?.enabled}
               linkedSpools={linkedSpools}
               spoolmanUrl={spoolmanStatus?.url}
               onGetAssignment={getAssignment}

+ 4 - 2
frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx

@@ -123,9 +123,11 @@ export function SpoolBuddyAmsPage() {
 
   // Build fill-level override map from inventory assignments
   // Key: "amsId-trayId", Value: fill percentage (0-100)
+  // Built-in rows only in built-in mode: kept through a switch to Spoolman
+  // (#2812), they would give a slot a fill from the other inventory (#3430).
   const fillOverrides = useMemo(() => {
     const map: Record<string, number> = {};
-    if (!assignments) return map;
+    if (!assignments || spoolmanStatus?.enabled) return map;
     for (const a of assignments) {
       const sp = a.spool;
       if (sp && sp.label_weight > 0 && sp.weight_used != null) {
@@ -134,7 +136,7 @@ export function SpoolBuddyAmsPage() {
       }
     }
     return map;
-  }, [assignments]);
+  }, [assignments, spoolmanStatus?.enabled]);
 
   // Look up Spoolman fill level for a given tray
   const printerSerial = printer?.serial_number ?? '';

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/ArchivesPage-CpcbZlCY.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/AssignToAmsModal-C421iyiz.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/AssignToAmsModal-DH0wi62b.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/CamWallPage-Ceuo5cN7.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/CameraPage-DBqX6f6U.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/FileManagerPage-BhNE8zRU.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/InventoryPage-Bmlat63M.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/InventoryPage-CaIFj3z6.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/ModelSourcesPage-DokANRvL.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/PrintersPage-4t5tmqMm.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/PrintersPage-DP4udQlz.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SettingsPage-Dec0ACaM.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SliceModal-C9Qeeus7.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DJIsj8oR.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DpXAS9Dr.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SpoolBuddyDashboard-C1zWfGh5.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-0WsbJpYW.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CqAAyJj4.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/index-DNGt_wr5.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/locationSensorDefaults-DM_LlId0.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/spoolCost-C-H4uQrM.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/usePageFileDrop-DjyMXsMg.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-B8Yl2nqQ.js"></script>
+    <script type="module" crossorigin src="/assets/index-DNGt_wr5.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-Bw3urp1w.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-Bihwyis7.js">

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor