فهرست منبع

Security hardening (maziggy/bambuddy-security #26)

-----------
fix(queue): every way of printing an archive again applies the same checks

Deleted archives read as missing when queueing, batching or running a
pipeline; a batch's archive or library file passes the same checks as
POST /queue/; running a pipeline on an archive needs the right to reprint
it, and the Run with pipeline dialog shows why a run is refused.
maziggy 1 هفته پیش
والد
کامیت
d1100f5a33
62فایلهای تغییر یافته به همراه143 افزوده شده و 26 حذف شده
  1. 5 0
      backend/app/api/routes/pipeline_runs.py
  2. 19 14
      backend/app/api/routes/print_queue.py
  3. 80 0
      backend/tests/integration/test_queue_source_gate.py
  4. 1 1
      backend/tests/integration/test_zero_ids_on_create.py
  5. 19 0
      frontend/src/__tests__/components/RunWithPipelineModal.test.tsx
  6. 6 2
      frontend/src/components/RunWithPipelineModal.tsx
  7. 10 4
      frontend/src/pages/ArchivesPage.tsx
  8. 0 0
      static/assets/AdditionalSection-Bl_ruhNV.js
  9. 0 0
      static/assets/AmsUnitCard-C1y65imE.js
  10. 0 0
      static/assets/ArchivesPage-BGKYZXGq.js
  11. 0 0
      static/assets/ArchivesPage-BtNYc-Nc.js
  12. 0 0
      static/assets/AssignToAmsModal-BQHrfs75.js
  13. 0 0
      static/assets/CamWallPage-B0th50z4.js
  14. 0 0
      static/assets/CameraDiagnoseModal-rUeQ8uid.js
  15. 0 0
      static/assets/CameraPage-CnIyaCER.js
  16. 0 0
      static/assets/CameraWall-CgOz7e5-.js
  17. 0 0
      static/assets/ColumnConfigModal-B4JbSKui.js
  18. 0 0
      static/assets/ConfigureAmsSlotModal-C3S9AGG6.js
  19. 0 0
      static/assets/ConnectAuthorizePage-P8QnD8ef.js
  20. 0 0
      static/assets/ContextMenu-Ck7qhxZe.js
  21. 0 0
      static/assets/ExternalLinkPage-DAYyahwF.js
  22. 0 1
      static/assets/FileManagerPage-y-WhmxY5.js
  23. 0 0
      static/assets/FileUploadModal-DiND4dFu.js
  24. 0 0
      static/assets/FinancePage-D3chJG-R.js
  25. 0 0
      static/assets/GCodeViewerPage-DriGXnZH.js
  26. 0 0
      static/assets/GroupEditPage-CKS_jMBo.js
  27. 0 0
      static/assets/ImagePreviewModal-v8BE6NO1.js
  28. 0 0
      static/assets/InventoryPage-fsotrOop.js
  29. 0 0
      static/assets/LibraryTrashPage-DLvNsh72.js
  30. 0 0
      static/assets/LoginPage-CEgsYqTD.js
  31. 0 0
      static/assets/MaintenancePage-BSXLsPfo.js
  32. 0 0
      static/assets/ModelSourcesPage-XBl-mJlW.js
  33. 0 0
      static/assets/NotificationsPage-D7rMF1eC.js
  34. 0 1
      static/assets/PdfPreviewModal-CZxGodsP.js
  35. 0 0
      static/assets/PhotoGalleryModal-BWbC1uoq.js
  36. 0 0
      static/assets/PhotoGalleryModal-C9dGGJPn.js
  37. 0 0
      static/assets/PrinterLocationsPage-D9c41LZN.js
  38. 0 0
      static/assets/PrintersPage-dfWcKJrc.js
  39. 0 0
      static/assets/ProfilesPage-CHhvGfs1.js
  40. 0 0
      static/assets/ProjectDetailPage-BDKpOiPw.js
  41. 0 0
      static/assets/ProjectsPage-ycDjTyT6.js
  42. 0 0
      static/assets/QueuePage-DOFltjAT.js
  43. 0 0
      static/assets/RichTextEditor-CX9hkjFK.js
  44. 0 0
      static/assets/SettingsPage-6KOwCJHm.js
  45. 0 0
      static/assets/SetupPage-CtL0Ktg2.js
  46. 0 0
      static/assets/SliceModal-DGYVQ7F-.js
  47. 0 0
      static/assets/SpoolBuddyAmsPage-CD0UaSO8.js
  48. 0 0
      static/assets/SpoolBuddyCalibrationPage-BiVyvOD4.js
  49. 0 0
      static/assets/SpoolBuddyDashboard-ClGS-Jdf.js
  50. 0 0
      static/assets/SpoolBuddyInventoryPage-ChMu3BRS.js
  51. 1 1
      static/assets/SpoolBuddySettingsPage-d88ME60m.js
  52. 0 0
      static/assets/SpoolBuddyWriteTagPage-CGjbl_93.js
  53. 0 0
      static/assets/SpreadsheetPreviewModal-yeBOszE1.js
  54. 0 0
      static/assets/StatsPage-nR7IdtRw.js
  55. 0 0
      static/assets/StreamOverlayPage-a5O_acD_.js
  56. 0 0
      static/assets/SystemInfoPage-40y3ApAO.js
  57. 0 0
      static/assets/index-dx8-YkAB.js
  58. 1 1
      static/assets/inventorySearch-DhdOL4y3.js
  59. 0 0
      static/assets/locationSensorDefaults-CPJXcEHG.js
  60. 0 0
      static/assets/pdf-DHmlXOUb.js
  61. 0 0
      static/assets/utils-BIG4vjte.js
  62. 1 1
      static/index.html

+ 5 - 0
backend/app/api/routes/pipeline_runs.py

@@ -415,6 +415,11 @@ async def _resolve_source(
     arc = (await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id))).scalar_one_or_none()
     can_read_all = user is None or user.has_permission(Permission.ARCHIVES_READ_ALL.value)
     arc = _ensure_archive_visible(arc, user, can_read_all, printer_scope)
+    # Running a pipeline on an archive prints it again: the same rules as
+    # queueing it, including the right to reprint it.
+    from backend.app.api.routes.print_queue import _assert_can_queue_archive
+
+    _assert_can_queue_archive(arc, user, printer_scope)
     rel = arc.source_3mf_path or arc.file_path
     if not rel:
         raise HTTPException(400, "Archive has no source file to slice")

+ 19 - 14
backend/app/api/routes/print_queue.py

@@ -180,8 +180,8 @@ def _assert_can_queue_archive(
 
     Three checks:
 
-    * The archive's printer must be one the caller may use; otherwise the
-      archive reads as missing, as the archive routes answer for it.
+    * The archive must not be deleted, and its printer must be one the caller
+      may use; otherwise it reads as missing, as the archive routes answer.
     * IDOR fix (maziggy/bambuddy-security #2): without this, a caller with
       QUEUE_CREATE could queue any user's archive even without ARCHIVES_READ on
       it — Landon's PoC enumerated this on admin's archives as operator1. Gate
@@ -195,9 +195,9 @@ def _assert_can_queue_archive(
       allows any archive, REPRINT_OWN allows own only, ownerless archives
       require REPRINT_ALL (fail-closed).
     """
-    # An archive from a printer the caller can't use reads as missing, like the
-    # archive routes answer for it.
-    if not printer_scope.allows(archive.printer_id):
+    # A deleted archive, or one from a printer the caller can't use, reads as
+    # missing, like the archive routes answer for it.
+    if archive.deleted_at is not None or not printer_scope.allows(archive.printer_id):
         raise HTTPException(404, "Archive not found")
     # None is auth off (for an API key too).
     if current_user is None:
@@ -1505,17 +1505,22 @@ async def create_batch(
 
     plate_targets = _validate_plate_targets(data.plates)
     await _validate_batch_project(db, data.project_id, actor)
-    # The file the batch prints, for reference: 0 is none, any other id must exist.
+    # The file the batch prints: 0 is none, any other id passes the same source
+    # checks as POST /queue/, and a missing one answers like a hidden one.
     data.archive_id = data.archive_id or None
     data.library_file_id = data.library_file_id or None
-    if data.archive_id is not None and await db.get(PrintArchive, data.archive_id) is None:
-        raise HTTPException(400, "Archive not found")
-    if (
-        data.library_file_id is not None
-        and (await db.execute(LibraryFile.active().where(LibraryFile.id == data.library_file_id))).scalar_one_or_none()
-        is None
-    ):
-        raise HTTPException(400, "Library file not found")
+    if data.archive_id is not None:
+        batch_archive = await db.get(PrintArchive, data.archive_id)
+        if batch_archive is None:
+            raise HTTPException(404, "Archive not found")
+        _assert_can_queue_archive(batch_archive, actor, printer_scope)
+    if data.library_file_id is not None:
+        batch_file = (
+            await db.execute(LibraryFile.active().where(LibraryFile.id == data.library_file_id))
+        ).scalar_one_or_none()
+        if batch_file is None:
+            raise HTTPException(404, "Library file not found")
+        _assert_can_queue_library_file(batch_file, actor)
     if data.external_source is not None:
         existing = await db.execute(
             select(PrintBatch.id).where(

+ 80 - 0
backend/tests/integration/test_queue_source_gate.py

@@ -216,3 +216,83 @@ async def test_a_key_without_an_owner_needs_read_access_for_an_ownerless_library
 
     assert (refused.status_code, refused.json()["detail"]) == (404, "Library file not found")
     assert allowed.status_code == 200, allowed.text
+
+
+async def test_a_deleted_archive_reads_as_missing(
+    async_client: AsyncClient, db_session, printer_factory, archive_factory
+):
+    from datetime import datetime, timezone
+
+    printer = await printer_factory()
+    archive = await archive_factory(printer.id)
+    archive.deleted_at = datetime.now(timezone.utc)
+    await db_session.commit()
+
+    response = await async_client.post("/api/v1/queue/", json={"archive_id": archive.id, "printer_id": printer.id})
+
+    assert (response.status_code, response.json()["detail"]) == (404, "Archive not found")
+
+
+async def test_a_batch_cannot_name_an_archive_from_another_printer(
+    async_client: AsyncClient, printer_factory, archive_factory
+):
+    a, b = await printer_factory(name="A"), await printer_factory(name="B")
+    admin = await _admin_token(async_client)
+    perms = await _group(async_client, admin, "perms", permissions=[*TEAM_PERMISSIONS, "archives:reprint_all"])
+    team = await _group(async_client, admin, "team", printer_ids=[a.id])
+    member = _auth((await _user(async_client, admin, "member", [perms, team]))[0])
+    on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
+
+    refused = await async_client.post(
+        "/api/v1/queue/batches", headers=member, json={"name": "x", "archive_id": on_b.id}
+    )
+    allowed = await async_client.post(
+        "/api/v1/queue/batches", headers=member, json={"name": "x", "archive_id": on_a.id}
+    )
+
+    assert (refused.status_code, refused.json()["detail"]) == (404, "Archive not found")
+    assert allowed.status_code == 200, allowed.text
+
+
+async def test_running_a_pipeline_on_an_archive_needs_the_right_to_reprint_it(
+    async_client: AsyncClient, printer_factory, archive_factory
+):
+    printer = await printer_factory()
+    admin = await _admin_token(async_client)
+    pipeline = await async_client.post(
+        "/api/v1/slicer-pipelines/",
+        headers=_auth(admin),
+        json={
+            "name": "P",
+            "description": None,
+            "printer_preset": {"source": "local", "id": "1"},
+            "process_preset": {"source": "local", "id": "2"},
+            "filament_presets": [{"source": "local", "id": "3"}],
+            "bed_type": None,
+        },
+    )
+    assert pipeline.status_code == 201, pipeline.text
+    archive = await archive_factory(printer.id)
+    # Sees every archive and may run pipelines, but not reprint.
+    base = ["printers:read", "queue:create", "archives:read_all", "pipelines:read", "pipelines:run"]
+    without = _auth(
+        (await _user(async_client, admin, "without", [await _group(async_client, admin, "g1", permissions=base)]))[0]
+    )
+    with_reprint = _auth(
+        (
+            await _user(
+                async_client,
+                admin,
+                "with",
+                [await _group(async_client, admin, "g2", permissions=[*base, "archives:reprint_all"])],
+            )
+        )[0]
+    )
+    url = f"/api/v1/slicer-pipelines/{pipeline.json()['id']}/run"
+
+    refused = await async_client.post(url, headers=without, json={"source_archive_id": archive.id})
+    passed_gate = await async_client.post(url, headers=with_reprint, json={"source_archive_id": archive.id})
+
+    assert refused.status_code == 403, refused.text
+    # Past the gate, the run stops at the archive file this test does not create.
+    assert (passed_gate.status_code, passed_gate.json()["detail"]) == (404, "Archive source file missing on disk")

+ 1 - 1
backend/tests/integration/test_zero_ids_on_create.py

@@ -65,7 +65,7 @@ async def test_a_batch_with_archive_0_has_no_archive(async_client: AsyncClient,
 async def test_a_batch_for_an_unknown_file_is_refused(async_client: AsyncClient, db_session, field, detail):
     response = await async_client.post("/api/v1/queue/batches", json={"name": "B", field: 9999})
 
-    assert response.status_code == 400, response.text
+    assert response.status_code == 404, response.text
     assert response.json()["detail"] == detail
     assert (await db_session.execute(select(PrintBatch.id))).first() is None
 

+ 19 - 0
frontend/src/__tests__/components/RunWithPipelineModal.test.tsx

@@ -137,6 +137,25 @@ describe('RunWithPipelineModal', () => {
     });
   });
 
+  it('shows why the eligibility check was refused instead of going back silently', async () => {
+    mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [makePipeline()] });
+    mockApi.checkPipelineEligibility.mockRejectedValue(new Error('Not allowed to print this archive again'));
+    const onClose = vi.fn();
+    render(
+      <SliceJobTrackerProvider>
+        <RunWithPipelineModal source={{ kind: 'archive', id: 7, filename: 'part.3mf' }} onClose={onClose} />
+      </SliceJobTrackerProvider>,
+    );
+    await waitFor(() => expect(screen.getByText('Production Batch')).toBeInTheDocument());
+
+    const user = userEvent.setup();
+    await user.click(screen.getByRole('button', { name: /Production Batch/i }));
+
+    expect(await screen.findByText('Not allowed to print this archive again')).toBeInTheDocument();
+    expect(mockApi.runPipeline).not.toHaveBeenCalled();
+    expect(onClose).not.toHaveBeenCalled();
+  });
+
   it('threads source kind="archive" through eligibility + run', async () => {
     mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [makePipeline()] });
     mockApi.checkPipelineEligibility.mockResolvedValue({

+ 6 - 2
frontend/src/components/RunWithPipelineModal.tsx

@@ -103,8 +103,12 @@ export function RunWithPipelineModal({ source, onClose }: RunWithPipelineModalPr
       if (result.ok) {
         runMutation.mutate({ pipelineId: pipeline.id, force: false });
       }
-    } catch {
-      // Network error — keep the user on step 1 so they can retry.
+    } catch (err) {
+      // Refused or unreachable: say why and keep the user on step 1.
+      showToast(
+        (err instanceof Error && err.message) || t('library.runWithPipeline.toast.failed', 'Could not start run'),
+        'error',
+      );
       setPicked(null);
     }
   };

+ 10 - 4
frontend/src/pages/ArchivesPage.tsx

@@ -605,10 +605,13 @@ function ArchiveCard({
             label: t('library.runWithPipeline.actionLabel'),
             icon: <Play className="w-4 h-4" />,
             onClick: () => setShowRunPipeline(true),
-            disabled: !hasPermission('pipelines:run'),
+            // A run prints the archive again, so it needs the right to reprint it.
+            disabled: !hasPermission('pipelines:run') || !canModify('archives', 'reprint', archive.created_by_id),
             title: !hasPermission('pipelines:run')
               ? t('library.runWithPipeline.noPermission')
-              : undefined,
+              : !canModify('archives', 'reprint', archive.created_by_id)
+                ? t('archives.permission.noReprint')
+                : undefined,
           }]
         : []),
     ]),
@@ -2107,10 +2110,13 @@ function ArchiveListRow({
             label: t('library.runWithPipeline.actionLabel'),
             icon: <Play className="w-4 h-4" />,
             onClick: () => setShowRunPipeline(true),
-            disabled: !hasPermission('pipelines:run'),
+            // A run prints the archive again, so it needs the right to reprint it.
+            disabled: !hasPermission('pipelines:run') || !canModify('archives', 'reprint', archive.created_by_id),
             title: !hasPermission('pipelines:run')
               ? t('library.runWithPipeline.noPermission')
-              : undefined,
+              : !canModify('archives', 'reprint', archive.created_by_id)
+                ? t('archives.permission.noReprint')
+                : undefined,
           }]
         : []),
     ]),

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AdditionalSection-Bl_ruhNV.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AmsUnitCard-C1y65imE.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ArchivesPage-BGKYZXGq.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ArchivesPage-BtNYc-Nc.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AssignToAmsModal-BQHrfs75.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CamWallPage-B0th50z4.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraDiagnoseModal-rUeQ8uid.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraPage-CnIyaCER.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraWall-CgOz7e5-.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ColumnConfigModal-B4JbSKui.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ConfigureAmsSlotModal-C3S9AGG6.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ConnectAuthorizePage-P8QnD8ef.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ContextMenu-Ck7qhxZe.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ExternalLinkPage-DAYyahwF.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
static/assets/FileManagerPage-y-WhmxY5.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/FileUploadModal-DiND4dFu.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/FinancePage-D3chJG-R.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/GCodeViewerPage-DriGXnZH.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/GroupEditPage-CKS_jMBo.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ImagePreviewModal-v8BE6NO1.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/InventoryPage-fsotrOop.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/LibraryTrashPage-DLvNsh72.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/LoginPage-CEgsYqTD.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/MaintenancePage-BSXLsPfo.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ModelSourcesPage-XBl-mJlW.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/NotificationsPage-D7rMF1eC.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
static/assets/PdfPreviewModal-CZxGodsP.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PhotoGalleryModal-BWbC1uoq.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PhotoGalleryModal-C9dGGJPn.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PrinterLocationsPage-D9c41LZN.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PrintersPage-dfWcKJrc.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProfilesPage-CHhvGfs1.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProjectDetailPage-BDKpOiPw.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProjectsPage-ycDjTyT6.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/QueuePage-DOFltjAT.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/RichTextEditor-CX9hkjFK.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SettingsPage-6KOwCJHm.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SetupPage-CtL0Ktg2.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SliceModal-DGYVQ7F-.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyAmsPage-CD0UaSO8.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-BiVyvOD4.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyDashboard-ClGS-Jdf.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-ChMu3BRS.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
static/assets/SpoolBuddySettingsPage-d88ME60m.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-CGjbl_93.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpreadsheetPreviewModal-yeBOszE1.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/StatsPage-nR7IdtRw.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/StreamOverlayPage-a5O_acD_.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SystemInfoPage-40y3ApAO.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/index-dx8-YkAB.js


+ 1 - 1
static/assets/inventorySearch-CdflUdPi.js → static/assets/inventorySearch-DhdOL4y3.js

@@ -1 +1 @@
-import{ft as e}from"./index-Cd25WO_Z.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{ft as e}from"./index-dx8-YkAB.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};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/locationSensorDefaults-CPJXcEHG.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/pdf-DHmlXOUb.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/utils-BIG4vjte.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-Cd25WO_Z.js"></script>
+    <script type="module" crossorigin src="/assets/index-dx8-YkAB.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-C3LdD0iY.js">
     <link rel="modulepreload" crossorigin href="/assets/QueryClientProvider-DzWmuIjj.js">

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است