Explorar el Código

fix(inventory): AMS weight sync skips printing printers, scale weights lock, force sync unlocks

maziggy hace 6 días
padre
commit
0b1d6c238b
Se han modificado 100 ficheros con 276 adiciones y 18 borrados
  1. 4 0
      CHANGELOG.md
  2. 26 9
      backend/app/api/routes/inventory.py
  3. 3 0
      backend/app/api/routes/spoolbuddy.py
  4. 24 3
      backend/app/api/routes/spoolman_inventory.py
  5. 135 0
      backend/tests/integration/test_inventory_ams_weight_sync_lock.py
  6. 27 0
      backend/tests/integration/test_spoolman_ams_sync.py
  7. 36 0
      frontend/src/__tests__/components/SpoolmanSettings.test.tsx
  8. 2 2
      frontend/src/api/client.ts
  9. 2 0
      frontend/src/components/SpoolmanSettings.tsx
  10. 1 0
      frontend/src/i18n/locales/de.ts
  11. 1 0
      frontend/src/i18n/locales/en.ts
  12. 1 0
      frontend/src/i18n/locales/es.ts
  13. 1 0
      frontend/src/i18n/locales/fr.ts
  14. 1 0
      frontend/src/i18n/locales/it.ts
  15. 1 0
      frontend/src/i18n/locales/ja.ts
  16. 1 0
      frontend/src/i18n/locales/ko.ts
  17. 1 0
      frontend/src/i18n/locales/nl.ts
  18. 1 0
      frontend/src/i18n/locales/pt-BR.ts
  19. 1 0
      frontend/src/i18n/locales/ru.ts
  20. 1 0
      frontend/src/i18n/locales/sv.ts
  21. 1 0
      frontend/src/i18n/locales/tr.ts
  22. 1 0
      frontend/src/i18n/locales/uk.ts
  23. 1 0
      frontend/src/i18n/locales/zh-CN.ts
  24. 1 0
      frontend/src/i18n/locales/zh-TW.ts
  25. 0 0
      static/assets/AdditionalSection-Cw9KWIyU.js
  26. 0 0
      static/assets/AmsUnitCard-BC8tn_UY.js
  27. 0 0
      static/assets/ArchivesPage-BSjtxefy.js
  28. 0 0
      static/assets/AssignToAmsModal-BxpwPkwW.js
  29. 0 0
      static/assets/CamWallPage-v8I0Gqb-.js
  30. 0 0
      static/assets/CameraDiagnoseModal-0-cRiSxx.js
  31. 0 0
      static/assets/CameraPage-xsaLCFyG.js
  32. 0 0
      static/assets/CameraWall-CeVpwH6L.js
  33. 0 0
      static/assets/ColumnConfigModal-CwMWwpUd.js
  34. 0 0
      static/assets/ConfigureAmsSlotModal-rs7Y8Skg.js
  35. 0 0
      static/assets/ConnectAuthorizePage-KjigJQOw.js
  36. 0 0
      static/assets/ContextMenu-Du5YL36o.js
  37. 0 0
      static/assets/ExternalLinkPage-MjlhCrzH.js
  38. 0 1
      static/assets/FileManagerPage-Di4Mvr--.js
  39. 0 0
      static/assets/FileUploadModal-OWjT06rp.js
  40. 0 0
      static/assets/FinancePage-Ct1FhT7d.js
  41. 0 0
      static/assets/GCodeViewerPage-DUcxo9Oa.js
  42. 0 0
      static/assets/GroupEditPage-BQ5YC2Mn.js
  43. 0 0
      static/assets/ImagePreviewModal-DqEu28FX.js
  44. 0 0
      static/assets/InventoryPage-5oNzlDxv.js
  45. 0 0
      static/assets/LibraryTrashPage-DYQZWjeZ.js
  46. 0 0
      static/assets/LoginPage-CB_Af8aO.js
  47. 0 0
      static/assets/MaintenancePage-BbwyfPgb.js
  48. 0 0
      static/assets/ModelSourcesPage-D9OKl32-.js
  49. 0 0
      static/assets/NotificationsPage-C8VzY4yW.js
  50. 0 1
      static/assets/PdfPreviewModal-BLu569hL.js
  51. 0 0
      static/assets/PhotoGalleryModal-Ex172zPZ.js
  52. 0 0
      static/assets/PrinterLocationsPage-DuF0e4_X.js
  53. 0 0
      static/assets/PrintersPage-BvZoIm26.js
  54. 0 0
      static/assets/ProfilesPage-BXCpTXul.js
  55. 0 0
      static/assets/ProjectDetailPage-cPKsm2eT.js
  56. 0 0
      static/assets/ProjectsPage-BMdO74Cl.js
  57. 0 0
      static/assets/QueuePage-p4xJMdL5.js
  58. 0 0
      static/assets/RichTextEditor-BegmvhWM.js
  59. 0 0
      static/assets/SettingsPage-C0gagyhN.js
  60. 0 0
      static/assets/SetupPage-L4dFtVQU.js
  61. 0 0
      static/assets/SliceModal-DGEganT3.js
  62. 0 0
      static/assets/SpoolBuddyAmsPage-BuEPYNjx.js
  63. 0 0
      static/assets/SpoolBuddyCalibrationPage-BX4vEGrK.js
  64. 0 0
      static/assets/SpoolBuddyDashboard-Bx1sRPV1.js
  65. 0 0
      static/assets/SpoolBuddyInventoryPage-BlJaphbG.js
  66. 1 1
      static/assets/SpoolBuddySettingsPage-CO7t4Yp5.js
  67. 0 0
      static/assets/SpoolBuddyWriteTagPage-DH2QPRri.js
  68. 0 0
      static/assets/SpreadsheetPreviewModal-Bxuio5iS.js
  69. 0 0
      static/assets/StatsPage-B8lecCwL.js
  70. 0 0
      static/assets/StreamOverlayPage-Bk6rcP3W.js
  71. 0 0
      static/assets/SystemInfoPage-NSqIz48P.js
  72. 0 0
      static/assets/de-CA69kxvp.js
  73. 0 0
      static/assets/de-mgb0RyZq.js
  74. 0 0
      static/assets/es--sQjgG5r.js
  75. 0 0
      static/assets/es-rm5cH5pl.js
  76. 0 0
      static/assets/fr-Et2eyvIH.js
  77. 0 0
      static/assets/fr-xnZWydOl.js
  78. 0 0
      static/assets/index-DO5B_8CV.js
  79. 1 1
      static/assets/inventorySearch-p7JXaSAm.js
  80. 0 0
      static/assets/it-CXZoZunK.js
  81. 0 0
      static/assets/it-jJlvgvcv.js
  82. 0 0
      static/assets/ja-D8He1KGK.js
  83. 0 0
      static/assets/ja-DBLnMcoO.js
  84. 0 0
      static/assets/ko-BLGRioLB.js
  85. 0 0
      static/assets/ko-C4ZprGjR.js
  86. 0 0
      static/assets/locationSensorDefaults-Dai6d-Xs.js
  87. 0 0
      static/assets/nl-CStbtRev.js
  88. 0 0
      static/assets/nl-DgNbji2_.js
  89. 0 0
      static/assets/pdf-BPmrYjFp.js
  90. 0 0
      static/assets/pt-BR-D7VRKydp.js
  91. 0 0
      static/assets/pt-BR-Y5qDll9m.js
  92. 0 0
      static/assets/ru-BP4Oxm6d.js
  93. 0 0
      static/assets/ru-Dy8PTljD.js
  94. 0 0
      static/assets/sv-B8l4Qqve.js
  95. 0 0
      static/assets/sv-DcXyMJhL.js
  96. 0 0
      static/assets/tr-CM_aDnky.js
  97. 0 0
      static/assets/tr-CpWorEfj.js
  98. 0 0
      static/assets/uk-Dk-Pyu68.js
  99. 0 0
      static/assets/uk-Dxd00yiD.js
  100. 0 0
      static/assets/utils-CZ1fdcj1.js

+ 4 - 0
CHANGELOG.md

@@ -164,6 +164,10 @@ 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
+- **Spool weights from the AMS** —
+  - **Sync Weights from AMS** and **Sync Spoolman Weights from AMS** leave the spools of a printer that is printing alone, and say how many. Synced halfway through a print, that print's usage was counted twice at completion.
+  - A spool weighed on the SpoolBuddy scale is locked like one whose weight was set by hand, so the automatic AMS remain% sync no longer overwrites the measured weight.
+  - **Sync Weights from AMS** now also updates locked spools and releases their lock. Nothing else could release it, so a spool whose weight was once set by hand was skipped by the sync for good.
 - **SpoolBuddy** —
   - The Write Tag page no longer stays on "Writing tag..." when the tag was written but could not be linked to the spool. It shows why: Spoolman not set up, linking in Spoolman failed, or the spool no longer exists.
   - A scanned tag that cannot be looked up because Spoolman is unavailable, or because the lookup failed, now says so in the kiosk status bar for a few seconds. Before, nothing happened.

+ 26 - 9
backend/app/api/routes/inventory.py

@@ -2760,9 +2760,14 @@ async def sync_weights_from_ams(
 
     Overwrites the database weight_used for every assigned spool using the
     current AMS remain% from connected printers.  This is a manual recovery
-    tool — it bypasses the normal "only increase" guard.
+    tool — it bypasses the normal "only increase" guard, and the weight lock
+    a hand-set or weighed figure carries, which it releases.
+
+    A printer that is printing is left alone: its print's usage is charged
+    when it completes, on top of whatever weight the spool has by then.
     """
-    from backend.app.services.printer_manager import printer_manager
+    from backend.app.services.printer_manager import PrinterManager, printer_manager
+    from backend.app.services.usage_tracker import _active_sessions
 
     result = await db.execute(select(SpoolAssignment).options(selectinload(SpoolAssignment.spool)))
     # Only slots on printers the caller may see (#1727)
@@ -2771,6 +2776,7 @@ async def sync_weights_from_ams(
 
     synced = 0
     skipped = 0
+    printing = 0
 
     for assignment in assignments:
         spool = assignment.spool
@@ -2779,13 +2785,9 @@ async def sync_weights_from_ams(
             skipped += 1
             continue
 
-        if spool.weight_locked:
-            logger.debug("AMS weight sync: spool %d is weight-locked, skipping", spool.id)
-            skipped += 1
-            continue
-
         state = printer_manager.get_status(assignment.printer_id)
-        if not state or not state.raw_data:
+        # A disconnected printer keeps its last report, which is not a reading.
+        if not state or not state.raw_data or getattr(state, "connected", True) is False:
             logger.info(
                 "AMS weight sync: printer %d not connected, skipping spool %d",
                 assignment.printer_id,
@@ -2794,6 +2796,17 @@ async def sync_weights_from_ams(
             skipped += 1
             continue
 
+        # Until completion has charged the print its session stays open, also
+        # after the printer has reported the print finished.
+        if (
+            str(getattr(state, "state", "") or "").upper() in PrinterManager.ACTIVE_PRINT_STATES
+            or assignment.printer_id in _active_sessions
+        ):
+            logger.info("AMS weight sync: printer %d is printing, skipping spool %d", assignment.printer_id, spool.id)
+            skipped += 1
+            printing += 1
+            continue
+
         ams_raw = state.raw_data.get("ams", [])
         if isinstance(ams_raw, dict):
             ams_raw = ams_raw.get("ams", [])
@@ -2839,12 +2852,16 @@ async def sync_weights_from_ams(
                 remain_val,
             )
             spool.weight_used = new_used
+            spool.weight_locked = False
             synced += 1
         else:
+            if spool.weight_locked:
+                # The figure already agrees with the AMS; the override still applies.
+                spool.weight_locked = False
             skipped += 1
 
     await db.commit()
-    return {"synced": synced, "skipped": skipped}
+    return {"synced": synced, "skipped": skipped, "printing": printing}
 
 
 # ── Helpers ──────────────────────────────────────────────────────────────────

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

@@ -1057,6 +1057,9 @@ async def update_spool_weight(
         spool.weight_used = max(0, spool.label_weight - net_filament)
         spool.last_scale_weight = req.weight_grams
         spool.last_weighed_at = datetime.now(timezone.utc)
+        # A measured figure, which the AMS remain% sync must not overwrite
+        # (as a hand-set one, see update_spool).
+        spool.weight_locked = True
         await db.commit()
         logger.info(
             "SpoolBuddy updated spool %d weight: %.1fg on scale, %.1fg used",

+ 24 - 3
backend/app/api/routes/spoolman_inventory.py

@@ -61,7 +61,7 @@ from backend.app.services.location_service import (
     maybe_sync_spoolman_locations,
     resolve_spoolman_location_string,
 )
-from backend.app.services.printer_manager import printer_manager
+from backend.app.services.printer_manager import PrinterManager, printer_manager
 from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
 from backend.app.services.slot_nozzle import resolve_slot_nozzle
 from backend.app.services.spool_filament_preset import resolve_spoolman_preset
@@ -1591,8 +1591,13 @@ async def sync_spoolman_ams_weights(
     # Only slots on printers the caller may see (#1727)
     assignments = [a for a in result.scalars().all() if printer_scope.allows(a.printer_id)]
 
+    from backend.app.services.usage_tracker import _active_sessions
+
     synced = 0
     skipped = 0
+    # A printing printer is left alone: its print's usage is charged when it
+    # completes, on top of whatever remaining weight the spool has by then.
+    printing = 0
 
     def _find_tray(ams_data: list, ams_id: int, tray_id: int) -> dict | None:
         if not ams_data:
@@ -1619,7 +1624,8 @@ async def sync_spoolman_ams_weights(
             continue
 
         state = printer_manager.get_status(assignment.printer_id)
-        if not state or not state.raw_data:
+        # A disconnected printer keeps its last report, which is not a reading.
+        if not state or not state.raw_data or getattr(state, "connected", True) is False:
             logger.info(
                 "Spoolman AMS sync: printer %d not connected, skipping spool %d",
                 assignment.printer_id,
@@ -1628,6 +1634,21 @@ async def sync_spoolman_ams_weights(
             skipped += 1
             continue
 
+        # Until completion has charged the print its session stays open, also
+        # after the printer has reported the print finished.
+        if (
+            str(getattr(state, "state", "") or "").upper() in PrinterManager.ACTIVE_PRINT_STATES
+            or assignment.printer_id in _active_sessions
+        ):
+            logger.info(
+                "Spoolman AMS sync: printer %d is printing, skipping spool %d",
+                assignment.printer_id,
+                assignment.spoolman_spool_id,
+            )
+            skipped += 1
+            printing += 1
+            continue
+
         ams_raw = state.raw_data.get("ams", [])
         if isinstance(ams_raw, dict):
             ams_raw = ams_raw.get("ams", [])
@@ -1695,7 +1716,7 @@ async def sync_spoolman_ams_weights(
                 )
             skipped += 1
 
-    return {"synced": synced, "skipped": skipped}
+    return {"synced": synced, "skipped": skipped, "printing": printing}
 
 
 @router.post("/slot-assignments")

+ 135 - 0
backend/tests/integration/test_inventory_ams_weight_sync_lock.py

@@ -0,0 +1,135 @@
+"""The built-in inventory's AMS weight sync, the weight lock and a printing printer.
+
+A weight set by hand or measured on the SpoolBuddy scale is locked, so the
+automatic remain% sync leaves it alone. "Sync Weights from AMS" is the explicit
+override: it also updates locked spools and releases the lock. It leaves the
+spools of a printing printer alone, whose usage the print's completion charges.
+"""
+
+from datetime import datetime, timezone
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+import pytest
+from httpx import AsyncClient
+from sqlalchemy import select
+
+from backend.app.models.spool import Spool
+from backend.app.models.spool_assignment import SpoolAssignment
+from backend.app.models.spoolbuddy_device import SpoolBuddyDevice
+
+
+async def _assigned_spool(db_session, printer_factory, *, locked: bool) -> int:
+    printer = await printer_factory()
+    spool = Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=100, weight_locked=locked)
+    db_session.add(spool)
+    await db_session.flush()
+    db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=0))
+    await db_session.commit()
+    return spool.id
+
+
+def _printer(state: str, remain: int = 50, connected: bool = True):
+    return SimpleNamespace(
+        state=state, connected=connected, raw_data={"ams": [{"id": 0, "tray": [{"id": 0, "remain": remain}]}]}
+    )
+
+
+async def _sync(async_client, state):
+    with patch("backend.app.services.printer_manager.printer_manager") as pm:
+        pm.get_status = MagicMock(return_value=state)
+        return await async_client.post("/api/v1/inventory/sync-ams-weights")
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_the_sync_updates_a_locked_spool_and_releases_the_lock(
+    async_client: AsyncClient, db_session, printer_factory
+):
+    spool_id = await _assigned_spool(db_session, printer_factory, locked=True)
+
+    response = await _sync(async_client, _printer("IDLE"))
+
+    assert response.status_code == 200
+    assert response.json()["synced"] == 1
+    spool = await db_session.get(Spool, spool_id)
+    await db_session.refresh(spool)
+    assert (spool.weight_used, spool.weight_locked) == (500, False)
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+@pytest.mark.parametrize("gcode_state", ["PREPARE", "RUNNING", "PAUSE"])
+async def test_a_printing_printers_spool_is_left_alone(
+    async_client: AsyncClient, db_session, printer_factory, gcode_state
+):
+    spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
+
+    response = await _sync(async_client, _printer(gcode_state))
+
+    assert (response.json()["synced"], response.json()["printing"]) == (0, 1)
+    spool = await db_session.get(Spool, spool_id)
+    await db_session.refresh(spool)
+    assert spool.weight_used == 100
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_weighing_on_the_scale_locks_the_spool(async_client: AsyncClient, db_session):
+    db_session.add(
+        SpoolBuddyDevice(
+            device_id="sb-1",
+            hostname="spoolbuddy-1",
+            ip_address="10.0.0.1",
+            last_seen=datetime.now(timezone.utc),
+        )
+    )
+    spool = Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=100)
+    db_session.add(spool)
+    await db_session.commit()
+
+    response = await async_client.post(
+        "/api/v1/spoolbuddy/scale/update-spool-weight", json={"spool_id": spool.id, "weight_grams": 600}
+    )
+
+    assert response.status_code == 200
+    await db_session.refresh(spool)
+    assert (spool.weight_used, spool.weight_locked) == (650, True)
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_print_whose_completion_is_still_being_charged_counts_as_printing(
+    async_client: AsyncClient, db_session, printer_factory
+):
+    """The printer already reports FINISH; the usage session is still open
+    until completion has charged the print."""
+    from backend.app.services.usage_tracker import PrintSession, _active_sessions
+
+    spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
+    printer_id = await db_session.scalar(select(SpoolAssignment.printer_id).where(SpoolAssignment.spool_id == spool_id))
+    _active_sessions[printer_id] = PrintSession(
+        printer_id=printer_id, print_name="x", started_at=datetime.now(timezone.utc)
+    )
+    try:
+        response = await _sync(async_client, _printer("FINISH"))
+    finally:
+        _active_sessions.pop(printer_id, None)
+
+    assert (response.json()["synced"], response.json()["printing"]) == (0, 1)
+    spool = await db_session.get(Spool, spool_id)
+    await db_session.refresh(spool)
+    assert spool.weight_used == 100
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_disconnected_printers_last_report_is_not_used(async_client: AsyncClient, db_session, printer_factory):
+    spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
+
+    response = await _sync(async_client, _printer("unknown", connected=False))
+
+    assert response.json()["synced"] == 0
+    spool = await db_session.get(Spool, spool_id)
+    await db_session.refresh(spool)
+    assert spool.weight_used == 100

+ 27 - 0
backend/tests/integration/test_spoolman_ams_sync.py

@@ -98,6 +98,33 @@ def _make_printer_state(remain=75):
 
 
 class TestSyncSpoolmanAmsWeights:
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize("gcode_state", ["PREPARE", "RUNNING", "PAUSE"])
+    async def test_a_printing_printers_spool_is_left_alone(
+        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment, gcode_state
+    ):
+        """Its usage is charged when the print completes; a remaining weight
+        set from remain% halfway through would be charged again on top."""
+        spoolman_client = _make_spoolman_client()
+        printer_state = _make_printer_state(remain=75)
+        printer_state.state = gcode_state
+
+        with (
+            patch(
+                "backend.app.api.routes.spoolman_inventory._get_client",
+                AsyncMock(return_value=spoolman_client),
+            ),
+            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
+        ):
+            pm_mock.get_status = MagicMock(return_value=printer_state)
+
+            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")
+
+        assert response.status_code == 200
+        assert (response.json()["synced"], response.json()["printing"]) == (0, 1)
+        spoolman_client.update_spool_full.assert_not_called()
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_happy_path_synced_count(

+ 36 - 0
frontend/src/__tests__/components/SpoolmanSettings.test.tsx

@@ -390,6 +390,42 @@ describe('SpoolmanSettings', () => {
     });
   });
 
+  describe('AMS weight sync with a printer printing', () => {
+    beforeEach(() => {
+      vi.mocked(api.getSpoolmanSettings).mockResolvedValue({
+        spoolman_enabled: 'true',
+        spoolman_url: 'http://localhost:7912',
+        spoolman_sync_mode: 'auto',
+        spoolman_disable_weight_sync: 'false',
+        spoolman_report_partial_usage: 'true',
+      });
+      vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
+        enabled: true,
+        connected: true,
+        url: 'http://localhost:7912',
+      });
+      vi.mocked(api.syncSpoolmanAmsWeights).mockResolvedValue({ synced: 1, skipped: 2, printing: 2 });
+    });
+
+    it('says which spools were left alone because their printer is printing', async () => {
+      const user = userEvent.setup();
+      render(<SpoolmanSettings />);
+
+      const syncButtons = await screen.findAllByRole('button', { name: /Sync Spoolman Weights from AMS/i });
+      await user.click(syncButtons[0]);
+      const dialogTitle = await screen.findByText('Sync Spoolman Spool Weights from AMS');
+      const dialog = dialogTitle.closest('div[class*="fixed"]') ?? document.body;
+      const confirm = Array.from(dialog.querySelectorAll('button')).filter((b) =>
+        /Sync Spoolman Weights from AMS/i.test(b.textContent ?? ''),
+      );
+      await user.click(confirm[confirm.length - 1]);
+
+      expect(
+        await screen.findByText('2 spool(s) on a printer that is printing left alone. Sync again once the print has finished.'),
+      ).toBeInTheDocument();
+    });
+  });
+
   describe('native tag migration (Spoolman 0.27+, #3168)', () => {
     const report = {
       dry_run: true,

+ 2 - 2
frontend/src/api/client.ts

@@ -7189,7 +7189,7 @@ export const api = {
   clearSpoolUsageHistory: (spoolId: number) =>
     request<{ status: string }>(`/inventory/spools/${spoolId}/usage`, { method: 'DELETE' }),
   syncWeightsFromAms: () =>
-    request<{ synced: number; skipped: number }>('/inventory/sync-ams-weights', { method: 'POST' }),
+    request<{ synced: number; skipped: number; printing?: number }>('/inventory/sync-ams-weights', { method: 'POST' }),
   getSkuSettings: () =>
     request<FilamentSkuSettings[]>('/inventory/sku-settings'),
   upsertSkuSettings: (data: Omit<FilamentSkuSettings, 'id'>) =>
@@ -7310,7 +7310,7 @@ export const api = {
         : '/spoolman/inventory/slot-assignments/all',
     ),
   syncSpoolmanAmsWeights: () =>
-    request<{ synced: number; skipped: number }>('/spoolman/inventory/sync-ams-weights', { method: 'POST' }),
+    request<{ synced: number; skipped: number; printing?: number }>('/spoolman/inventory/sync-ams-weights', { method: 'POST' }),
 
   getSpoolmanKProfiles: (spoolId: number) =>
     request<SpoolKProfile[]>(`/spoolman/inventory/spools/${spoolId}/k-profiles`),

+ 2 - 0
frontend/src/components/SpoolmanSettings.tsx

@@ -153,6 +153,7 @@ export function SpoolmanSettings() {
       queryClient.invalidateQueries({ queryKey: ['spools'] });
       queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
       showToast(t('settings.amsSyncSuccess', { synced: data.synced, skipped: data.skipped }), 'success');
+      if (data.printing) showToast(t('settings.amsSyncSkippedPrinting', { count: data.printing }), 'info');
       setShowAmsSyncConfirm(false);
     },
     onError: () => {
@@ -167,6 +168,7 @@ export function SpoolmanSettings() {
     onSuccess: (data) => {
       queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] });
       showToast(t('settings.spoolmanAmsSyncSuccess', { synced: data.synced, skipped: data.skipped }), 'success');
+      if (data.printing) showToast(t('settings.amsSyncSkippedPrinting', { count: data.printing }), 'info');
       setShowSpoolmanAmsSyncConfirm(false);
     },
     onError: (error: Error) => {

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -2247,6 +2247,7 @@ export default {
     amsSyncing: 'Synchronisiere...',
     amsSyncSuccess: '{{synced}} Spule(n) synchronisiert, {{skipped}} übersprungen',
     amsSyncError: 'Synchronisierung der Gewichte vom AMS fehlgeschlagen',
+    amsSyncSkippedPrinting: '{{count}} Spule(n) an einem Drucker, der gerade druckt, unverändert gelassen. Nach dem Druck erneut synchronisieren.',
     spoolmanAmsSyncButton: 'Spoolman-Gewichte vom AMS synchronisieren',
     spoolmanAmsSyncTitle: 'Spoolman-Spulengewichte vom AMS synchronisieren',
     spoolmanAmsSyncMessage: 'Dabei werden alle Spoolman-Spulengewichte anhand der aktuellen AMS-Füllstandswerte der verbundenen Drucker aktualisiert. Die Drucker müssen online sein.',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -2269,6 +2269,7 @@ export default {
     amsSyncing: 'Syncing...',
     amsSyncSuccess: '{{synced}} spool(s) synced, {{skipped}} skipped',
     amsSyncError: 'Failed to sync weights from AMS',
+    amsSyncSkippedPrinting: '{{count}} spool(s) on a printer that is printing left alone. Sync again once the print has finished.',
     spoolmanAmsSyncButton: 'Sync Spoolman Weights from AMS',
     spoolmanAmsSyncTitle: 'Sync Spoolman Spool Weights from AMS',
     spoolmanAmsSyncMessage: 'This will update all Spoolman spool weights based on the current AMS remain% values from connected printers. Printers must be online.',

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -2250,6 +2250,7 @@ export default {
     amsSyncing: 'Sincronizando...',
     amsSyncSuccess: '{{synced}} bobina(s) sincronizada(s), {{skipped}} omitida(s)',
     amsSyncError: 'Error al sincronizar los pesos desde el AMS',
+    amsSyncSkippedPrinting: '{{count}} bobina(s) en una impresora que está imprimiendo se dejaron sin cambios. Sincroniza de nuevo cuando termine la impresión.',
     spoolmanAmsSyncButton: 'Sincronizar pesos de Spoolman desde el AMS',
     spoolmanAmsSyncTitle: 'Sincronizar los pesos de las bobinas de Spoolman desde el AMS',
     spoolmanAmsSyncMessage: 'Esto actualizará todos los pesos de las bobinas de Spoolman según los valores actuales de % restante del AMS de las impresoras conectadas. Las impresoras deben estar en línea.',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -2202,6 +2202,7 @@ export default {
     amsSyncing: 'Synchronisation...',
     amsSyncSuccess: '{{synced}} bobine(s) synchronisée(s), {{skipped}} ignorée(s)',
     amsSyncError: 'Échec de la synchronisation des poids depuis l\'AMS',
+    amsSyncSkippedPrinting: '{{count}} bobine(s) sur une imprimante en cours d\'impression laissée(s) telle(s) quelle(s). Synchronisez à nouveau une fois l\'impression terminée.',
     spoolmanAmsSyncButton: 'Synchroniser les poids Spoolman depuis l\'AMS',
     spoolmanAmsSyncTitle: 'Synchroniser les poids des bobines Spoolman depuis l\'AMS',
     spoolmanAmsSyncMessage: 'Cela mettra à jour les poids des bobines Spoolman en fonction des valeurs de remplissage AMS actuelles des imprimantes connectées. Les imprimantes doivent être en ligne.',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -2202,6 +2202,7 @@ export default {
     amsSyncing: 'Sincronizzazione...',
     amsSyncSuccess: '{{synced}} bobina/e sincronizzata/e, {{skipped}} saltata/e',
     amsSyncError: 'Impossibile sincronizzare i pesi dall\'AMS',
+    amsSyncSkippedPrinting: '{{count}} bobina/e su una stampante in stampa lasciata/e invariata/e. Sincronizza di nuovo al termine della stampa.',
     spoolmanAmsSyncButton: 'Sincronizza i pesi Spoolman dall\'AMS',
     spoolmanAmsSyncTitle: 'Sincronizza i pesi delle bobine Spoolman dall\'AMS',
     spoolmanAmsSyncMessage: 'Questo aggiornerà i pesi di tutte le bobine Spoolman in base ai valori di riempimento AMS correnti delle stampanti connesse. Le stampanti devono essere online.',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -2246,6 +2246,7 @@ export default {
     amsSyncing: '同期中...',
     amsSyncSuccess: '{{synced}}個のスプールを同期、{{skipped}}個をスキップ',
     amsSyncError: 'AMSからの重量同期に失敗しました',
+    amsSyncSkippedPrinting: '印刷中のプリンターの {{count}} 個のスプールは変更しませんでした。印刷終了後にもう一度同期してください。',
     spoolmanAmsSyncButton: 'AMSからSpoolmanの重量を同期',
     spoolmanAmsSyncTitle: 'AMSからSpoolmanスプール重量を同期',
     spoolmanAmsSyncMessage: '接続されたプリンターの現在のAMS残量値に基づいて、すべてのSpoolmanスプール重量を更新します。プリンターがオンラインである必要があります。',

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -2151,6 +2151,7 @@ export default {
     amsSyncing: '동기화 중...',
     amsSyncSuccess: '{{synced}}개 스풀 동기화됨, {{skipped}}개 건너뜀',
     amsSyncError: 'AMS에서 무게 동기화 실패',
+    amsSyncSkippedPrinting: '출력 중인 프린터의 스풀 {{count}}개는 변경하지 않았습니다. 출력이 끝난 뒤 다시 동기화하세요.',
     spoolmanAmsSyncButton: 'AMS에서 Spoolman 무게 동기화',
     spoolmanAmsSyncTitle: 'AMS에서 Spoolman 스풀 무게 동기화',
     spoolmanAmsSyncMessage: '연결된 프린터의 현재 AMS 잔여% 값으로 모든 Spoolman 스풀 무게를 업데이트합니다. 프린터가 온라인이어야 합니다.',

+ 1 - 0
frontend/src/i18n/locales/nl.ts

@@ -2267,6 +2267,7 @@ export default {
     amsSyncing: 'Synchroniseren...',
     amsSyncSuccess: '{{synced}} spoel(en) gesynchroniseerd, {{skipped}} overgeslagen',
     amsSyncError: 'Gewichten synchroniseren vanuit AMS mislukt',
+    amsSyncSkippedPrinting: '{{count}} spoel(en) op een printer die aan het printen is ongewijzigd gelaten. Synchroniseer opnieuw zodra de print klaar is.',
     spoolmanAmsSyncButton: 'Spoolman-gewichten synchroniseren vanuit AMS',
     spoolmanAmsSyncTitle: 'Spoolman-spoelgewichten synchroniseren vanuit AMS',
     spoolmanAmsSyncMessage: 'Hiermee worden alle Spoolman-spoelgewichten bijgewerkt op basis van de huidige AMS remain%-waarden van verbonden printers. Printers moeten online zijn.',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -2202,6 +2202,7 @@ export default {
     amsSyncing: 'Sincronizando...',
     amsSyncSuccess: '{{synced}} rolo(s) sincronizado(s), {{skipped}} ignorado(s)',
     amsSyncError: 'Falha ao sincronizar pesos do AMS',
+    amsSyncSkippedPrinting: '{{count}} carretel(is) em uma impressora que está imprimindo não foi(ram) alterado(s). Sincronize novamente quando a impressão terminar.',
     spoolmanAmsSyncButton: 'Sincronizar pesos do Spoolman pelo AMS',
     spoolmanAmsSyncTitle: 'Sincronizar pesos dos rolos Spoolman pelo AMS',
     spoolmanAmsSyncMessage: 'Isso atualizará os pesos de todos os rolos Spoolman com base nos valores atuais de % restante do AMS das impressoras conectadas. As impressoras devem estar online.',

+ 1 - 0
frontend/src/i18n/locales/ru.ts

@@ -2148,6 +2148,7 @@ export default {
     amsSyncing: "Синхронизация...",
     amsSyncSuccess: "Синхронизировано катушек: {{synced}}, пропущено: {{skipped}}",
     amsSyncError: "Не удалось синхронизировать вес из AMS",
+    amsSyncSkippedPrinting: "{{count}} катушк(и) на принтере, который сейчас печатает, оставлены без изменений. Синхронизируйте снова после окончания печати.",
     spoolmanAmsSyncButton: "Синхронизировать вес Spoolman из AMS",
     spoolmanAmsSyncTitle: "Синхронизация веса катушек Spoolman из AMS",
     spoolmanAmsSyncMessage: "Вес всех катушек Spoolman будет обновлён по текущим значениям остатка из AMS подключённых принтеров. Принтеры должны быть в сети.",

+ 1 - 0
frontend/src/i18n/locales/sv.ts

@@ -2266,6 +2266,7 @@ export default {
     amsSyncing: 'Synkroniserar...',
     amsSyncSuccess: '{{synced}} rulle(ar) synkroniserade, {{skipped}} hoppades över',
     amsSyncError: 'Kunde inte synkronisera vikter från AMS',
+    amsSyncSkippedPrinting: '{{count}} spole/spolar på en skrivare som skriver ut lämnades orörda. Synkronisera igen när utskriften är klar.',
     spoolmanAmsSyncButton: 'Synkronisera Spoolman-vikter från AMS',
     spoolmanAmsSyncTitle: 'Synkronisera Spoolman-rullvikter från AMS',
     spoolmanAmsSyncMessage: 'Detta kommer att uppdatera alla Spoolman-rullars vikter baserat på de aktuella AMS-procentvärdena från anslutna skrivare. Skrivaren måste vara online.',

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -2250,6 +2250,7 @@ export default {
     amsSyncing: 'Senkronize ediliyor...',
     amsSyncSuccess: '{{synced}} makara senkronize edildi, {{skipped}} atlandı',
     amsSyncError: "AMS'den ağırlıklar senkronize edilemedi",
+    amsSyncSkippedPrinting: "Yazdırma yapan bir yazıcıdaki {{count}} makara değiştirilmedi. Baskı bittikten sonra yeniden eşitleyin.",
     spoolmanAmsSyncButton: "AMS'den Spoolman Ağırlıklarını Senkronize Et",
     spoolmanAmsSyncTitle: "AMS'den Spoolman Makara Ağırlıklarını Senkronize Et",
     spoolmanAmsSyncMessage: 'Bu, bağlı yazıcılardan gelen mevcut AMS kalan% değerlerine göre tüm Spoolman makara ağırlıklarını güncelleyecek. Yazıcılar çevrimiçi olmalıdır.',

+ 1 - 0
frontend/src/i18n/locales/uk.ts

@@ -2265,6 +2265,7 @@ export default {
     amsSyncing: "Синхронізація...",
     amsSyncSuccess: "Синхронізовано: {{synced}}; пропущено: {{skipped}}",
     amsSyncError: "Не вдалося синхронізувати масу з AMS",
+    amsSyncSkippedPrinting: "{{count}} котушк(и) на принтері, який зараз друкує, залишено без змін. Синхронізуйте знову після завершення друку.",
     spoolmanAmsSyncButton: "Синхронізувати масу котушок Spoolman з AMS",
     spoolmanAmsSyncTitle: "Синхронізація маси котушок Spoolman з AMS",
     spoolmanAmsSyncMessage: "Масу всіх котушок Spoolman буде оновлено відповідно до поточних значень залишку в AMS підключених принтерів. Принтери мають бути в мережі.",

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -2247,6 +2247,7 @@ export default {
     amsSyncing: '同步中...',
     amsSyncSuccess: '已同步 {{synced}} 个耗材,跳过 {{skipped}} 个',
     amsSyncError: '从 AMS 同步重量失败',
+    amsSyncSkippedPrinting: '正在打印的打印机上的 {{count}} 个料卷未作更改。打印完成后请再次同步。',
     spoolmanAmsSyncButton: '从 AMS 同步 Spoolman 重量',
     spoolmanAmsSyncTitle: '从 AMS 同步 Spoolman 耗材重量',
     spoolmanAmsSyncMessage: '这将根据已连接打印机的当前 AMS 剩余百分比值更新所有 Spoolman 耗材重量。打印机必须在线。',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -2247,6 +2247,7 @@ export default {
     amsSyncing: '同步中...',
     amsSyncSuccess: '已同步 {{synced}} 個耗材,跳過 {{skipped}} 個',
     amsSyncError: '從 AMS 同步重量失敗',
+    amsSyncSkippedPrinting: '正在列印的印表機上的 {{count}} 個料卷未作變更。列印完成後請再次同步。',
     spoolmanAmsSyncButton: '從 AMS 同步 Spoolman 重量',
     spoolmanAmsSyncTitle: '從 AMS 同步 Spoolman 耗材重量',
     spoolmanAmsSyncMessage: '這將根據已連線印表機的目前 AMS 剩餘百分比值更新所有 Spoolman 耗材重量。印表機必須線上。',

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/AdditionalSection-Cw9KWIyU.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/AmsUnitCard-BC8tn_UY.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ArchivesPage-BSjtxefy.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/AssignToAmsModal-BxpwPkwW.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/CamWallPage-v8I0Gqb-.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/CameraDiagnoseModal-0-cRiSxx.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/CameraPage-xsaLCFyG.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/CameraWall-CeVpwH6L.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ColumnConfigModal-CwMWwpUd.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ConfigureAmsSlotModal-rs7Y8Skg.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ConnectAuthorizePage-KjigJQOw.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ContextMenu-Du5YL36o.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ExternalLinkPage-MjlhCrzH.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 1
static/assets/FileManagerPage-Di4Mvr--.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/FileUploadModal-OWjT06rp.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/FinancePage-Ct1FhT7d.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/GCodeViewerPage-DUcxo9Oa.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/GroupEditPage-BQ5YC2Mn.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ImagePreviewModal-DqEu28FX.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/InventoryPage-5oNzlDxv.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/LibraryTrashPage-DYQZWjeZ.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/LoginPage-CB_Af8aO.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/MaintenancePage-BbwyfPgb.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ModelSourcesPage-D9OKl32-.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/NotificationsPage-C8VzY4yW.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 1
static/assets/PdfPreviewModal-BLu569hL.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/PhotoGalleryModal-Ex172zPZ.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/PrinterLocationsPage-DuF0e4_X.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/PrintersPage-BvZoIm26.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ProfilesPage-BXCpTXul.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ProjectDetailPage-cPKsm2eT.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ProjectsPage-BMdO74Cl.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/QueuePage-p4xJMdL5.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/RichTextEditor-BegmvhWM.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SettingsPage-C0gagyhN.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SetupPage-L4dFtVQU.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SliceModal-DGEganT3.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpoolBuddyAmsPage-BuEPYNjx.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-BX4vEGrK.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpoolBuddyDashboard-Bx1sRPV1.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-BlJaphbG.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CO7t4Yp5.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-DH2QPRri.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SpreadsheetPreviewModal-Bxuio5iS.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/StatsPage-B8lecCwL.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/StreamOverlayPage-Bk6rcP3W.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/SystemInfoPage-NSqIz48P.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/de-CA69kxvp.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/de-mgb0RyZq.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/es--sQjgG5r.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/es-rm5cH5pl.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/fr-Et2eyvIH.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/fr-xnZWydOl.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-DO5B_8CV.js


+ 1 - 1
static/assets/inventorySearch-CWXiA9VI.js → static/assets/inventorySearch-p7JXaSAm.js

@@ -1 +1 @@
-import{mt as e}from"./index-CRDwbAQC.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
+import{mt as e}from"./index-DO5B_8CV.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/it-CXZoZunK.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/it-jJlvgvcv.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ja-D8He1KGK.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ja-DBLnMcoO.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ko-BLGRioLB.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ko-C4ZprGjR.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/locationSensorDefaults-Dai6d-Xs.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/nl-CStbtRev.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/nl-DgNbji2_.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/pdf-BPmrYjFp.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/pt-BR-D7VRKydp.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/pt-BR-Y5qDll9m.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ru-BP4Oxm6d.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/ru-Dy8PTljD.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/sv-B8l4Qqve.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/sv-DcXyMJhL.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/tr-CM_aDnky.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/tr-CpWorEfj.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/uk-Dk-Pyu68.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/uk-Dxd00yiD.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/utils-CZ1fdcj1.js


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio