Parcourir la source

chore(api): remove dead firmware WS progress, the delete-all plate calibration route and unused client wrappers

- Firmware upload progress was broadcast over WebSocket with no listener;
  the page polls the upload status. Broadcast removed, docstrings fixed.
- DELETE /printers/{id}/camera/plate-detection/calibrate (delete every
  reference at once) had no caller; references are deleted one at a time.
- 12 unused frontend API wrappers removed; the routes stay as API.
- /spoolbuddy/calibration redirects to the SpoolBuddy settings page, where
  calibration lives; the dead page is gone.
maziggy il y a 4 jours
Parent
commit
741539a740
100 fichiers modifiés avec 23 ajouts et 551 suppressions
  1. 1 0
      CHANGELOG.md
  2. 0 41
      backend/app/api/routes/camera.py
  3. 3 8
      backend/app/api/routes/firmware.py
  4. 1 26
      backend/app/services/firmware_update.py
  5. 0 19
      backend/app/services/plate_detection.py
  6. 5 16
      backend/tests/integration/test_camera_api.py
  7. 0 1
      backend/tests/integration/test_writes_need_write_permissions.py
  8. 0 18
      backend/tests/unit/services/test_plate_detection.py
  9. 2 2
      frontend/src/App.tsx
  10. 0 147
      frontend/src/__tests__/pages/SpoolBuddyCalibrationPage.test.tsx
  11. 0 37
      frontend/src/api/client.ts
  12. 0 222
      frontend/src/pages/spoolbuddy/SpoolBuddyCalibrationPage.tsx
  13. 0 0
      static/assets/AdditionalSection-BXLQTBd6.js
  14. 0 0
      static/assets/AmsUnitCard-D00iS9DC.js
  15. 0 0
      static/assets/ArchivesPage-DJdJWmtj.js
  16. 0 0
      static/assets/AreaChart-CjMaR62C.js
  17. 0 0
      static/assets/AssignToAmsModal-DkvnHNXK.js
  18. 0 0
      static/assets/AuthContext-BfmcZb9G.js
  19. 1 1
      static/assets/Button-B7HH19dk.js
  20. 0 0
      static/assets/CamWallPage-B3DtcQOQ.js
  21. 0 0
      static/assets/CameraDiagnoseModal-CdYuChg8.js
  22. 0 0
      static/assets/CameraDiagnoseModal-ilwgjRj_.js
  23. 0 0
      static/assets/CameraPage-DVUKNaO5.js
  24. 0 0
      static/assets/CameraPage-De72-X7F.js
  25. 0 0
      static/assets/CameraWall-Bsfqfb9Q.js
  26. 0 0
      static/assets/CameraWall-EsX_py5Z.js
  27. 1 1
      static/assets/Card-DgaxYs1A.js
  28. 0 0
      static/assets/CartesianChart-DEv1ukUA.js
  29. 0 0
      static/assets/ColumnConfigModal-C3ITDxW6.js
  30. 0 0
      static/assets/ConfigureAmsSlotModal-CfRenMqW.js
  31. 0 0
      static/assets/ConnectAuthorizePage-ByS3HAVw.js
  32. 0 0
      static/assets/ContextMenu-TouYu-I8.js
  33. 0 0
      static/assets/ContextMenu-ujcH62Z3.js
  34. 0 0
      static/assets/CopyButton-D37Ln8vd.js
  35. 0 0
      static/assets/CopyButton-D50XFTJw.js
  36. 0 0
      static/assets/ErrorBarContext-CsQLj0aW.js
  37. 0 0
      static/assets/ExternalLinkPage-B8V6zVXd.js
  38. 0 1
      static/assets/FileManagerPage-BWxGLI3L.js
  39. 0 0
      static/assets/FileUploadModal-BB_T27UR.js
  40. 0 0
      static/assets/FinancePage-DA7oJeE7.js
  41. 0 0
      static/assets/FinancePage-DNp3Qbe0.js
  42. 0 0
      static/assets/GCodeViewerPage-BAdDJcj4.js
  43. 0 0
      static/assets/GcodeToolpathViewer-B5RO-cba.js
  44. 0 0
      static/assets/GroupEditPage-OmF8VkK1.js
  45. 0 0
      static/assets/ImagePreviewModal-DBnkDnR9.js
  46. 0 0
      static/assets/InventoryPage-BFQlzCRH.js
  47. 0 0
      static/assets/LibraryTrashPage-C4Y77NYs.js
  48. 0 0
      static/assets/LoginPage-DZylQhD7.js
  49. 0 0
      static/assets/MaintenancePage-Bqc2qiUi.js
  50. 0 0
      static/assets/MaintenancePage-C_HJ2-Xk.js
  51. 0 0
      static/assets/ModelSourcesPage-DUvT0hO1.js
  52. 0 0
      static/assets/ModelSourcesPage-Up0E9JjS.js
  53. 0 0
      static/assets/NotificationsPage-BC8mBoNa.js
  54. 1 1
      static/assets/OverlayFrame-D1QU_K6T.js
  55. 1 0
      static/assets/PdfPreviewModal-BFLt-4gG.js
  56. 0 1
      static/assets/PdfPreviewModal-CJcTx6-v.js
  57. 0 0
      static/assets/PhotoGalleryModal-CG4HnDt0.js
  58. 0 0
      static/assets/PrinterLocationsPage-DkGeOfP1.js
  59. 0 0
      static/assets/PrinterLocationsPage-oWYMgF_L.js
  60. 0 0
      static/assets/PrintersPage-DNsllYKD.js
  61. 0 0
      static/assets/ProfilesPage-CuKL4VW9.js
  62. 0 0
      static/assets/ProfilesPage-DzNK0i8W.js
  63. 0 0
      static/assets/ProjectDetailPage-Cyz3s9Fa.js
  64. 0 0
      static/assets/ProjectsPage-BkHB6VSK.js
  65. 0 0
      static/assets/ProjectsPage-D4P5NxK3.js
  66. 0 0
      static/assets/QueryClientProvider-B5zKGR0V.js
  67. 0 0
      static/assets/QueuePage-DnGOIfo7.js
  68. 0 0
      static/assets/QueuePage-qy7-9qls.js
  69. 0 0
      static/assets/ReferenceLine-CtwazpKK.js
  70. 0 0
      static/assets/RichTextEditor-Dvs3mDsk.js
  71. 0 0
      static/assets/SettingsPage-CLe1k-pV.js
  72. 0 0
      static/assets/SettingsPage-Dlwq6CUV.js
  73. 0 0
      static/assets/SetupPage-CcR9qR2n.js
  74. 0 0
      static/assets/SliceModal-DV2zsi09.js
  75. 0 0
      static/assets/SpoolBuddyAmsPage-Da7IuL5a.js
  76. 0 0
      static/assets/SpoolBuddyCalibrationPage-DQIvapoi.js
  77. 0 0
      static/assets/SpoolBuddyDashboard-DOLVK7C-.js
  78. 0 0
      static/assets/SpoolBuddyInventoryPage-Yv_W3ELP.js
  79. 0 1
      static/assets/SpoolBuddySettingsPage-C_N3Dlt1.js
  80. 0 0
      static/assets/SpoolBuddyWriteTagPage-CdHop1zy.js
  81. 1 1
      static/assets/SpoolIcon-DAYZYbv-.js
  82. 0 0
      static/assets/SpreadsheetPreviewModal-BNsbtV78.js
  83. 0 0
      static/assets/StatsPage-CrzPQqB3.js
  84. 0 0
      static/assets/StatsPage-DNE2a2-2.js
  85. 0 0
      static/assets/StreamOverlayPage-mrcjRGWl.js
  86. 0 0
      static/assets/SystemInfoPage-BdOpeaaL.js
  87. 0 0
      static/assets/ToastContext-BMyWPryU.js
  88. 0 0
      static/assets/ToastContext-Bhj9DS2p.js
  89. 1 1
      static/assets/Toggle-BZiPSowU.js
  90. 1 0
      static/assets/arrow-down-DrsjDA8H.js
  91. 0 1
      static/assets/arrow-down-VmQok7Hw.js
  92. 0 1
      static/assets/arrow-left-BcrTUh-S.js
  93. 1 0
      static/assets/arrow-left-DG60mWnu.js
  94. 1 0
      static/assets/arrow-right-CLqCre6h.js
  95. 0 1
      static/assets/arrow-right-bfq0Qx0q.js
  96. 1 0
      static/assets/arrow-up-C169xoJg.js
  97. 0 1
      static/assets/arrow-up-DXtx7YeZ.js
  98. 1 0
      static/assets/arrow-up-down-BiE6NqjL.js
  99. 0 1
      static/assets/arrow-up-down-DUGhy30I.js
  100. 0 1
      static/assets/box-C9odJhAD.js

+ 1 - 0
CHANGELOG.md

@@ -164,6 +164,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.
 - **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
 ### Fixed
+- **Unused API pieces removed and the rest documented** — The firmware upload no longer broadcasts a WebSocket progress message nothing listened to (the page polls the upload status, as before); the old route that deleted every plate-detection calibration of a printer at once is gone (references are deleted one at a time from the plate-detection dialog); `/spoolbuddy/calibration` now opens the SpoolBuddy settings page, where calibration lives. The wiki now documents `POST /archives/upload-source` for slicer post-processing scripts and `PUT /local-presets/{id}`.
 - **Load and Unload work on an AMS-HT** — Clicking **Load** on an AMS-HT slot was refused with "tray_id must be 0..15 …": the printer card numbered the slot like a regular AMS (unit × 4 + slot, 512 for the first AMS-HT), and the printer's commands would have turned the AMS-HT into a unit it does not have. **Unload** while the AMS-HT was the loaded tray was addressed to that same non-existent unit. Both now address the AMS-HT by its own unit number, as Bambu Studio does, and **Unload** from an AMS-HT slot's menu is aimed at that slot. Loading from the A2L's AMS-Lite now sends the target Bambu Studio sends as well.
 - **Load and Unload work on an AMS-HT** — Clicking **Load** on an AMS-HT slot was refused with "tray_id must be 0..15 …": the printer card numbered the slot like a regular AMS (unit × 4 + slot, 512 for the first AMS-HT), and the printer's commands would have turned the AMS-HT into a unit it does not have. **Unload** while the AMS-HT was the loaded tray was addressed to that same non-existent unit. Both now address the AMS-HT by its own unit number, as Bambu Studio does, and **Unload** from an AMS-HT slot's menu is aimed at that slot. Loading from the A2L's AMS-Lite now sends the target Bambu Studio sends as well.
 - **A second Bambuddy on the same machine no longer cuts the camera every minute** — Bambuddy's once-a-minute cleanup kills camera ffmpeg processes left behind by a crash or restart, and recognised them by their command alone. Another Bambuddy on the same host (a second install, a test instance) runs the very same command, so each one's cleanup killed the other's live camera once a minute and the view reconnected. Only an ffmpeg started by this Bambuddy, or left orphaned by an earlier run, is cleaned up now.
 - **A second Bambuddy on the same machine no longer cuts the camera every minute** — Bambuddy's once-a-minute cleanup kills camera ffmpeg processes left behind by a crash or restart, and recognised them by their command alone. Another Bambuddy on the same host (a second install, a test instance) runs the very same command, so each one's cleanup killed the other's live camera once a minute and the view reconnected. Only an ffmpeg started by this Bambuddy, or left orphaned by an earlier run, is cleaned up now.
 - **A restart with a camera view open no longer logs errors** — A camera stream never ends by itself, so a restart closes it after 5 seconds. Each stream closed that way was logged as an ERROR with a full traceback ("Exception in ASGI application ... timeout graceful shutdown exceeded"), which looked like a crash in the log and in support bundles. It is now one INFO line per stream, and the "Cancel N running task(s)" line before them is INFO too; real errors are logged as before.
 - **A restart with a camera view open no longer logs errors** — A camera stream never ends by itself, so a restart closes it after 5 seconds. Each stream closed that way was logged as an ERROR with a full traceback ("Exception in ASGI application ... timeout graceful shutdown exceeded"), which looked like a crash in the log and in support bundles. It is now one INFO line per stream, and the "Cancel N running task(s)" line before them is INFO too; real errors are logged as before.

+ 0 - 41
backend/app/api/routes/camera.py

@@ -2045,47 +2045,6 @@ async def calibrate_plate_detection(
     return {"success": success, "message": message, "index": index}
     return {"success": success, "message": message, "index": index}
 
 
 
 
-@router.delete("/{printer_id}/camera/plate-detection/calibrate")
-async def delete_plate_calibration(
-    printer_id: int,
-    plate_type: str | None = None,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.PRINTERS_UPDATE),
-):
-    """Delete the plate detection calibration for a printer and plate type.
-
-    Args:
-        printer_id: Printer ID
-        plate_type: Type of build plate (if None, deletes legacy non-plate-specific calibration)
-
-    Returns:
-        Dict with:
-        - success: bool - Whether deletion succeeded
-        - message: str - Status message
-    """
-    from backend.app.services.plate_detection import (
-        delete_calibration,
-        is_plate_detection_available,
-    )
-
-    # Verify printer exists first (before OpenCV check)
-    await get_printer_or_404(printer_id, db)
-
-    if not is_plate_detection_available():
-        raise HTTPException(
-            status_code=503,
-            detail="Plate detection not available. Install opencv-python-headless to enable.",
-        )
-
-    deleted = delete_calibration(printer_id, plate_type)
-    plate_msg = f" for '{plate_type}'" if plate_type else ""
-
-    return {
-        "success": deleted,
-        "message": f"Calibration deleted{plate_msg}" if deleted else f"No calibration found{plate_msg}",
-    }
-
-
 @router.get("/{printer_id}/camera/plate-detection/status")
 @router.get("/{printer_id}/camera/plate-detection/status")
 async def get_plate_detection_status(
 async def get_plate_detection_status(
     printer_id: int,
     printer_id: int,

+ 3 - 8
backend/app/api/routes/firmware.py

@@ -305,8 +305,7 @@ async def start_firmware_upload(
     1. Downloads the firmware from Bambu Lab
     1. Downloads the firmware from Bambu Lab
     2. Uploads it to the printer's SD card via FTP
     2. Uploads it to the printer's SD card via FTP
 
 
-    Progress is broadcast via WebSocket with type "firmware_upload_progress".
-    Use GET /firmware/updates/{printer_id}/upload/status for polling fallback.
+    Poll GET /firmware/updates/{printer_id}/upload/status for progress.
 
 
     After upload completes, the user must trigger the update from the
     After upload completes, the user must trigger the update from the
     printer's screen (Settings > Firmware).
     printer's screen (Settings > Firmware).
@@ -339,7 +338,7 @@ async def start_firmware_upload(
 
 
     return FirmwareUploadStartResponse(
     return FirmwareUploadStartResponse(
         started=True,
         started=True,
-        message="Firmware upload started. Progress will be broadcast via WebSocket.",
+        message="Firmware upload started. Poll the upload status for progress.",
     )
     )
 
 
 
 
@@ -349,11 +348,7 @@ async def get_firmware_upload_status(
     _: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.FIRMWARE_READ),
     _: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.FIRMWARE_READ),
 ):
 ):
     """
     """
-    Get the current status of a firmware upload operation.
-
-    This is a polling fallback for clients that don't use WebSocket.
-    For real-time updates, connect to WebSocket and listen for
-    "firmware_upload_progress" messages.
+    Get the current status of a firmware upload operation (poll for progress).
     """
     """
     state = get_upload_state(printer_id)
     state = get_upload_state(printer_id)
     return FirmwareUploadStatusResponse(
     return FirmwareUploadStatusResponse(

+ 1 - 26
backend/app/services/firmware_update.py

@@ -8,7 +8,6 @@ Orchestrates firmware updates for Bambu Lab printers:
 4. Notify user to trigger update from printer screen
 4. Notify user to trigger update from printer screen
 """
 """
 
 
-import asyncio
 import logging
 import logging
 from dataclasses import dataclass
 from dataclasses import dataclass
 
 
@@ -17,7 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
 
 
 from backend.app.core.compat import StrEnum
 from backend.app.core.compat import StrEnum
 from backend.app.core.tasks import spawn_background_task
 from backend.app.core.tasks import spawn_background_task
-from backend.app.core.websocket import ws_manager
 from backend.app.models.printer import Printer
 from backend.app.models.printer import Printer
 from backend.app.services.bambu_ftp import (
 from backend.app.services.bambu_ftp import (
     get_ftp_retry_settings,
     get_ftp_retry_settings,
@@ -256,7 +254,6 @@ class FirmwareUpdateService:
         state = get_upload_state(printer_id)
         state = get_upload_state(printer_id)
         state.status = FirmwareUploadStatus.PREPARING
         state.status = FirmwareUploadStatus.PREPARING
         state.message = "Preparing firmware update..."
         state.message = "Preparing firmware update..."
-        await self._broadcast_progress(printer_id, state)
 
 
         # Run the upload in background
         # Run the upload in background
         spawn_background_task(
         spawn_background_task(
@@ -289,7 +286,6 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.DOWNLOADING
             state.status = FirmwareUploadStatus.DOWNLOADING
             state.progress = 0
             state.progress = 0
             state.message = "Preparing firmware..."
             state.message = "Preparing firmware..."
-            await self._broadcast_progress(printer_id, state)
 
 
             firmware_path = await firmware_service.download_firmware(model, version=target_version)
             firmware_path = await firmware_service.download_firmware(model, version=target_version)
 
 
@@ -310,26 +306,22 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.UPLOADING
             state.status = FirmwareUploadStatus.UPLOADING
             state.progress = 0
             state.progress = 0
             state.message = f"Uploading {firmware_path.name} to printer..."
             state.message = f"Uploading {firmware_path.name} to printer..."
-            await self._broadcast_progress(printer_id, state)
 
 
             # Upload to root of SD card (where printer expects firmware)
             # Upload to root of SD card (where printer expects firmware)
             remote_path = f"/{firmware_path.name}"
             remote_path = f"/{firmware_path.name}"
 
 
             logger.info("Uploading firmware to printer %s: %s", printer_id, remote_path)
             logger.info("Uploading firmware to printer %s: %s", printer_id, remote_path)
 
 
-            # Track real progress via FTP callback
-            loop = asyncio.get_event_loop()
+            # Track real progress via FTP callback; the UI polls the upload status
             last_progress = 0
             last_progress = 0
 
 
             def on_upload_progress(uploaded: int, total: int):
             def on_upload_progress(uploaded: int, total: int):
                 nonlocal last_progress
                 nonlocal last_progress
                 if total > 0:
                 if total > 0:
                     progress = int((uploaded / total) * 100)
                     progress = int((uploaded / total) * 100)
-                    # Only broadcast every 1% to avoid flooding
                     if progress > last_progress:
                     if progress > last_progress:
                         last_progress = progress
                         last_progress = progress
                         state.progress = min(99, progress)  # Cap at 99 until complete
                         state.progress = min(99, progress)  # Cap at 99 until complete
-                        asyncio.run_coroutine_threadsafe(self._broadcast_progress(printer_id, state), loop)
 
 
             # Get FTP retry settings
             # Get FTP retry settings
             ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
             ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
@@ -375,7 +367,6 @@ class FirmwareUpdateService:
                 f"Firmware {state.firmware_version or ''} uploaded successfully! "
                 f"Firmware {state.firmware_version or ''} uploaded successfully! "
                 "Please go to printer screen and trigger the update from Settings > Firmware."
                 "Please go to printer screen and trigger the update from Settings > Firmware."
             )
             )
-            await self._broadcast_progress(printer_id, state)
 
 
             logger.info("Firmware upload complete for printer %s", printer_id)
             logger.info("Firmware upload complete for printer %s", printer_id)
 
 
@@ -384,22 +375,6 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.ERROR
             state.status = FirmwareUploadStatus.ERROR
             state.error = str(e)
             state.error = str(e)
             state.message = f"Firmware upload failed: {e}"
             state.message = f"Firmware upload failed: {e}"
-            await self._broadcast_progress(printer_id, state)
-
-    async def _broadcast_progress(self, printer_id: int, state: FirmwareUploadState):
-        """Broadcast firmware upload progress via WebSocket."""
-        await ws_manager.broadcast(
-            {
-                "type": "firmware_upload_progress",
-                "printer_id": printer_id,
-                "status": state.status.value,
-                "progress": state.progress,
-                "message": state.message,
-                "error": state.error,
-                "firmware_filename": state.firmware_filename,
-                "firmware_version": state.firmware_version,
-            }
-        )
 
 
 
 
 # Singleton instance
 # Singleton instance

+ 0 - 19
backend/app/services/plate_detection.py

@@ -411,16 +411,6 @@ class PlateDetector:
         """Check if a printer has any calibration reference images."""
         """Check if a printer has any calibration reference images."""
         return len(self._get_reference_paths(printer_id)) > 0
         return len(self._get_reference_paths(printer_id)) > 0
 
 
-    def delete_calibration(self, printer_id: int, plate_type: str | None = None) -> bool:
-        """Delete all calibration reference images for a printer."""
-        paths = self._get_reference_paths(printer_id)
-        if not paths:
-            return False
-        for path in paths:
-            path.unlink()
-        logger.info("Deleted %s plate calibration reference(s) for printer %s", len(paths), printer_id)
-        return True
-
     def analyze_frame(
     def analyze_frame(
         self, image_data: bytes, printer_id: int, plate_type: str | None = None, include_debug_image: bool = False
         self, image_data: bytes, printer_id: int, plate_type: str | None = None, include_debug_image: bool = False
     ) -> PlateDetectionResult:
     ) -> PlateDetectionResult:
@@ -843,15 +833,6 @@ def get_calibration_status(printer_id: int, plate_type: str | None = None) -> di
     }
     }
 
 
 
 
-def delete_calibration(printer_id: int, plate_type: str | None = None) -> bool:
-    """Delete calibration for a printer and plate type."""
-    if not OPENCV_AVAILABLE:
-        return False
-
-    detector = PlateDetector()
-    return detector.delete_calibration(printer_id, plate_type)
-
-
 def is_plate_detection_available() -> bool:
 def is_plate_detection_available() -> bool:
     """Check if plate detection feature is available (OpenCV installed)."""
     """Check if plate detection feature is available (OpenCV installed)."""
     return OPENCV_AVAILABLE
     return OPENCV_AVAILABLE

+ 5 - 16
backend/tests/integration/test_camera_api.py

@@ -676,25 +676,14 @@ class TestCameraAPI:
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     @pytest.mark.integration
     @pytest.mark.integration
-    async def test_delete_calibration_printer_not_found(self, async_client: AsyncClient):
-        """Verify 404 when deleting calibration for non-existent printer."""
-        response = await async_client.delete("/api/v1/printers/99999/camera/plate-detection/calibrate")
-
-        assert response.status_code == 404
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_delete_calibration_success(self, async_client: AsyncClient, printer_factory):
-        """Verify delete calibration returns proper structure."""
+    async def test_the_legacy_delete_all_calibration_route_is_gone(self, async_client: AsyncClient, printer_factory):
+        """References are deleted one at a time (DELETE .../references/{index});
+        the old delete-everything route had no caller left."""
         printer = await printer_factory()
         printer = await printer_factory()
 
 
-        with patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True):
-            response = await async_client.delete(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")
+        response = await async_client.delete(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")
 
 
-        assert response.status_code == 200
-        result = response.json()
-        assert "success" in result
-        assert "message" in result
+        assert response.status_code == 405
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     @pytest.mark.integration
     @pytest.mark.integration

+ 0 - 1
backend/tests/integration/test_writes_need_write_permissions.py

@@ -19,7 +19,6 @@ pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
 # (method, path template, permission that used to suffice, permission needed now)
 # (method, path template, permission that used to suffice, permission needed now)
 ROUTES = [
 ROUTES = [
     ("POST", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
     ("POST", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
-    ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
     ("PUT", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
     ("PUT", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
     ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
     ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
     ("POST", "/api/v1/bug-report/stop-logging?was_debug=true", "settings:read", "settings:update"),
     ("POST", "/api/v1/bug-report/stop-logging?was_debug=true", "settings:read", "settings:update"),

+ 0 - 18
backend/tests/unit/services/test_plate_detection.py

@@ -178,21 +178,3 @@ class TestCalibrationStatus:
 
 
             pd_module.OPENCV_AVAILABLE = False
             pd_module.OPENCV_AVAILABLE = False
             assert pd_module.is_plate_detection_available() is False
             assert pd_module.is_plate_detection_available() is False
-
-
-class TestDeleteCalibration:
-    """Tests for delete_calibration function."""
-
-    def test_delete_calibration_no_opencv(self):
-        """Verify delete_calibration returns False when OpenCV not available."""
-        with patch.dict("sys.modules", {"cv2": cv2_mock, "numpy": np_mock}):
-            import importlib
-
-            import backend.app.services.plate_detection as pd_module
-
-            importlib.reload(pd_module)
-
-            pd_module.OPENCV_AVAILABLE = False
-
-            result = pd_module.delete_calibration(1)
-            assert result is False

+ 2 - 2
frontend/src/App.tsx

@@ -44,7 +44,6 @@ const GCodeViewerPage = lazyPage(() => import('./pages/GCodeViewerPage'), 'GCode
 const SpoolBuddyDashboard = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyDashboard'), 'SpoolBuddyDashboard');
 const SpoolBuddyDashboard = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyDashboard'), 'SpoolBuddyDashboard');
 const SpoolBuddyAmsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyAmsPage'), 'SpoolBuddyAmsPage');
 const SpoolBuddyAmsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyAmsPage'), 'SpoolBuddyAmsPage');
 const SpoolBuddySettingsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddySettingsPage'), 'SpoolBuddySettingsPage');
 const SpoolBuddySettingsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddySettingsPage'), 'SpoolBuddySettingsPage');
-const SpoolBuddyCalibrationPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyCalibrationPage'), 'SpoolBuddyCalibrationPage');
 const SpoolBuddyWriteTagPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyWriteTagPage'), 'SpoolBuddyWriteTagPage');
 const SpoolBuddyWriteTagPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyWriteTagPage'), 'SpoolBuddyWriteTagPage');
 const SpoolBuddyInventoryPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyInventoryPage'), 'SpoolBuddyInventoryPage');
 const SpoolBuddyInventoryPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyInventoryPage'), 'SpoolBuddyInventoryPage');
 
 
@@ -214,7 +213,8 @@ function App() {
                   <Route path="spoolbuddy/write-tag" element={<SpoolBuddyWriteTagPage />} />
                   <Route path="spoolbuddy/write-tag" element={<SpoolBuddyWriteTagPage />} />
                   <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
                   <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
                   <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
                   <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
-                  <Route path="spoolbuddy/calibration" element={<SpoolBuddyCalibrationPage />} />
+                  {/* Calibration moved into the settings page; old bookmarks land there. */}
+                  <Route path="spoolbuddy/calibration" element={<Navigate to="/spoolbuddy/settings" replace />} />
                 </Route>
                 </Route>
 
 
                 {/* Main app with WebSocket for real-time updates */}
                 {/* Main app with WebSocket for real-time updates */}

+ 0 - 147
frontend/src/__tests__/pages/SpoolBuddyCalibrationPage.test.tsx

@@ -1,147 +0,0 @@
-/**
- * Tests for SpoolBuddyCalibrationPage:
- * - Renders "Scale Calibration" heading
- * - Shows current weight display
- * - Shows Tare and Calibrate buttons
- * - Shows "No SpoolBuddy device found" when no device
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, waitFor } from '@testing-library/react';
-import React from 'react';
-import { render } from '@testing-library/react';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { MemoryRouter, Route, Routes, Outlet } from 'react-router-dom';
-import { SpoolBuddyCalibrationPage } from '../../pages/spoolbuddy/SpoolBuddyCalibrationPage';
-
-const mockDevice = {
-  id: 1,
-  device_id: 'sb-test-001',
-  hostname: 'spoolbuddy-pi',
-  ip_address: '192.168.1.100',
-  firmware_version: '1.2.3',
-  has_nfc: true,
-  has_scale: true,
-  tare_offset: 0,
-  calibration_factor: 1.0,
-  nfc_reader_type: 'PN532',
-  nfc_connection: 'I2C',
-  display_brightness: 80,
-  display_blank_timeout: 300,
-  has_backlight: true,
-  last_calibrated_at: null,
-  last_seen: '2026-03-22T12:00:00Z',
-  pending_command: null,
-  nfc_ok: true,
-  scale_ok: true,
-  uptime_s: 3600,
-  update_status: null,
-  update_message: null,
-  online: true,
-};
-
-let mockDevices = [mockDevice];
-
-vi.mock('../../api/client', () => ({
-  spoolbuddyApi: {
-    getDevices: vi.fn(() => Promise.resolve(mockDevices)),
-    tare: vi.fn().mockResolvedValue({ status: 'ok' }),
-    setCalibrationFactor: vi.fn().mockResolvedValue({ status: 'ok' }),
-  },
-}));
-
-vi.mock('react-i18next', () => ({
-  useTranslation: () => ({
-    t: (_key: string, fallback: string) => fallback,
-    i18n: { language: 'en', changeLanguage: vi.fn() },
-  }),
-}));
-
-function makeOutletContext(overrides: Record<string, unknown> = {}) {
-  return {
-    selectedPrinterId: null,
-    setSelectedPrinterId: vi.fn(),
-    sbState: {
-      weight: 250.5,
-      weightStable: true,
-      rawAdc: 12345,
-      matchedSpool: null,
-      unknownTagUid: null,
-      deviceOnline: true,
-      deviceId: 'sb-test-001',
-      remainingWeight: null,
-      netWeight: null,
-      ...(overrides.sbState as Record<string, unknown> || {}),
-    },
-    setAlert: vi.fn(),
-    displayBrightness: 100,
-    setDisplayBrightness: vi.fn(),
-    displayBlankTimeout: 0,
-    setDisplayBlankTimeout: vi.fn(),
-  };
-}
-
-function renderPage(contextOverrides: Record<string, unknown> = {}) {
-  const ctx = makeOutletContext(contextOverrides);
-  function Wrapper() {
-    return <Outlet context={ctx} />;
-  }
-  const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
-  return render(
-    <QueryClientProvider client={qc}>
-      <MemoryRouter initialEntries={['/spoolbuddy/calibration']}>
-        <Routes>
-          <Route element={<Wrapper />}>
-            <Route path="spoolbuddy/calibration" element={<SpoolBuddyCalibrationPage />} />
-          </Route>
-        </Routes>
-      </MemoryRouter>
-    </QueryClientProvider>
-  );
-}
-
-describe('SpoolBuddyCalibrationPage', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-    mockDevices = [mockDevice];
-  });
-
-  it('renders "Scale Calibration" heading', async () => {
-    renderPage();
-    await waitFor(() => {
-      expect(screen.getByText('Scale Calibration')).toBeDefined();
-    });
-  });
-
-  it('shows current weight display when device available', async () => {
-    renderPage();
-    await waitFor(() => {
-      expect(screen.getByText('Current weight')).toBeDefined();
-      expect(screen.getByText('250.5 g')).toBeDefined();
-    });
-  });
-
-  it('shows Tare and Calibrate buttons when device available', async () => {
-    renderPage();
-    await waitFor(() => {
-      expect(screen.getByText('Tare')).toBeDefined();
-      expect(screen.getByText('Calibrate')).toBeDefined();
-    });
-  });
-
-  it('shows "No SpoolBuddy device found" when no device', async () => {
-    mockDevices = [];
-    renderPage();
-    await waitFor(() => {
-      expect(screen.getByText('No SpoolBuddy device found')).toBeDefined();
-    });
-  });
-
-  it('shows back button that navigates to settings', () => {
-    renderPage();
-    // Find the back button (contains a chevron SVG)
-    const buttons = screen.getAllByRole('button');
-    // First button is the back button
-    expect(buttons.length).toBeGreaterThan(0);
-  });
-});

+ 0 - 37
frontend/src/api/client.ts

@@ -6402,11 +6402,8 @@ export const api = {
     request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
     request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
       method: 'DELETE',
       method: 'DELETE',
     }),
     }),
-  getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
   getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
   getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
     request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
     request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
-  getAllCloudFields: () =>
-    request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
   getFilamentInfo: (settingIds: string[]) =>
   getFilamentInfo: (settingIds: string[]) =>
     request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
     request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
       method: 'POST',
       method: 'POST',
@@ -6516,7 +6513,6 @@ export const api = {
     if (targetModel) params.set('target_model', targetModel);
     if (targetModel) params.set('target_model', targetModel);
     return request<PrintQueueItem[]>(`/queue/?${params}`);
     return request<PrintQueueItem[]>(`/queue/?${params}`);
   },
   },
-  getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
   addToQueue: (data: PrintQueueItemCreate) =>
   addToQueue: (data: PrintQueueItemCreate) =>
     request<PrintQueueItem>('/queue/', {
     request<PrintQueueItem>('/queue/', {
       method: 'POST',
       method: 'POST',
@@ -6578,7 +6574,6 @@ export const api = {
     const params = status ? `?status=${status}` : '';
     const params = status ? `?status=${status}` : '';
     return request<PrintBatch[]>(`/queue/batches${params}`);
     return request<PrintBatch[]>(`/queue/batches${params}`);
   },
   },
-  getBatch: (id: number) => request<PrintBatch>(`/queue/batches/${id}`),
   cancelBatch: (id: number) =>
   cancelBatch: (id: number) =>
     request<{ message: string }>(`/queue/batches/${id}`, { method: 'DELETE' }),
     request<{ message: string }>(`/queue/batches/${id}`, { method: 'DELETE' }),
   createBatch: (data: PrintBatchCreate) =>
   createBatch: (data: PrintBatchCreate) =>
@@ -6586,11 +6581,6 @@ export const api = {
       method: 'POST',
       method: 'POST',
       body: JSON.stringify(data),
       body: JSON.stringify(data),
     }),
     }),
-  updateBatch: (id: number, data: PrintBatchUpdate) =>
-    request<PrintBatch>(`/queue/batches/${id}`, {
-      method: 'PATCH',
-      body: JSON.stringify(data),
-    }),
   dispatchBatch: (id: number, data: PrintBatchDispatchRequest = {}) =>
   dispatchBatch: (id: number, data: PrintBatchDispatchRequest = {}) =>
     request<PrintBatch>(`/queue/batches/${id}/dispatch`, {
     request<PrintBatch>(`/queue/batches/${id}/dispatch`, {
       method: 'POST',
       method: 'POST',
@@ -7469,8 +7459,6 @@ export const api = {
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`),
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`),
   getCameraSnapshotUrl: (printerId: number) =>
   getCameraSnapshotUrl: (printerId: number) =>
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/snapshot`),
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/snapshot`),
-  testCameraConnection: (printerId: number) =>
-    request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
   getCameraStatus: (printerId: number) =>
   getCameraStatus: (printerId: number) =>
     request<{ active: boolean; stalled: boolean }>(`/printers/${printerId}/camera/status`),
     request<{ active: boolean; stalled: boolean }>(`/printers/${printerId}/camera/status`),
   diagnoseCamera: (printerId: number) =>
   diagnoseCamera: (printerId: number) =>
@@ -7501,11 +7489,6 @@ export const api = {
       `/printers/${printerId}/camera/check-plate?${params.toString()}`
       `/printers/${printerId}/camera/check-plate?${params.toString()}`
     );
     );
   },
   },
-  getPlateDetectionStatus: (printerId: number) => {
-    return request<PlateDetectionStatus & { chamber_light?: boolean }>(
-      `/printers/${printerId}/camera/plate-detection/status`
-    );
-  },
   calibratePlateDetection: (printerId: number, options?: { label?: string; useExternal?: boolean }) => {
   calibratePlateDetection: (printerId: number, options?: { label?: string; useExternal?: boolean }) => {
     const params = new URLSearchParams();
     const params = new URLSearchParams();
     if (options?.label) params.set('label', options.label);
     if (options?.label) params.set('label', options.label);
@@ -7517,12 +7500,6 @@ export const api = {
       { method: 'POST' }
       { method: 'POST' }
     );
     );
   },
   },
-  deletePlateCalibration: (printerId: number) => {
-    return request<CalibrationResult>(
-      `/printers/${printerId}/camera/plate-detection/calibrate`,
-      { method: 'DELETE' }
-    );
-  },
   getPlateReferences: (printerId: number) => {
   getPlateReferences: (printerId: number) => {
     return request<{
     return request<{
       references: PlateReference[];
       references: PlateReference[];
@@ -8416,8 +8393,6 @@ export const api = {
   // Local Presets (OrcaSlicer imports)
   // Local Presets (OrcaSlicer imports)
   getLocalPresets: () =>
   getLocalPresets: () =>
     request<LocalPresetsResponse>('/local-presets/'),
     request<LocalPresetsResponse>('/local-presets/'),
-  getLocalPresetDetail: (id: number) =>
-    request<LocalPresetDetail>(`/local-presets/${id}`),
   importLocalPresets: (formData: FormData) =>
   importLocalPresets: (formData: FormData) =>
     fetch(`${API_BASE}/local-presets/import`, {
     fetch(`${API_BASE}/local-presets/import`, {
       method: 'POST',
       method: 'POST',
@@ -8430,20 +8405,8 @@ export const api = {
       }
       }
       return res.json() as Promise<ImportResponse>;
       return res.json() as Promise<ImportResponse>;
     }),
     }),
-  createLocalPreset: (data: { name: string; preset_type: string; setting: Record<string, unknown> }) =>
-    request<LocalPreset>('/local-presets/', {
-      method: 'POST',
-      body: JSON.stringify(data),
-    }),
-  updateLocalPreset: (id: number, data: { name?: string; setting?: Record<string, unknown> }) =>
-    request<LocalPreset>(`/local-presets/${id}`, {
-      method: 'PUT',
-      body: JSON.stringify(data),
-    }),
   deleteLocalPreset: (id: number) =>
   deleteLocalPreset: (id: number) =>
     request<{ success: boolean }>(`/local-presets/${id}`, { method: 'DELETE' }),
     request<{ success: boolean }>(`/local-presets/${id}`, { method: 'DELETE' }),
-  refreshBaseProfileCache: () =>
-    request<{ refreshed: number; failed: number; total: number }>('/local-presets/base-cache/refresh', { method: 'POST' }),
 };
 };
 
 
 // AMS History types
 // AMS History types

+ 0 - 222
frontend/src/pages/spoolbuddy/SpoolBuddyCalibrationPage.tsx

@@ -1,222 +0,0 @@
-import { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { useNavigate, useOutletContext } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import type { SpoolBuddyOutletContext } from '../../components/spoolbuddy/SpoolBuddyLayout';
-import { spoolbuddyApi, type SpoolBuddyDevice } from '../../api/client';
-
-function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
-  device: SpoolBuddyDevice;
-  weight: number | null;
-  weightStable: boolean;
-  rawAdc: number | null;
-}) {
-  const { t } = useTranslation();
-  const [calibrating, setCalibrating] = useState(false);
-  const [calStep, setCalStep] = useState<'idle' | 'tare' | 'weight'>('idle');
-  const [knownWeight, setKnownWeight] = useState('500');
-  const [tareRawAdc, setTareRawAdc] = useState<number | null>(null);
-  const [taring, setTaring] = useState(false);
-
-  const numpadPress = (key: string) => {
-    if (key === 'backspace') {
-      setKnownWeight((v) => v.slice(0, -1) || '');
-    } else if (key === '.' && !knownWeight.includes('.')) {
-      setKnownWeight((v) => v + '.');
-    } else if (key >= '0' && key <= '9') {
-      setKnownWeight((v) => (v === '0' ? key : v + key));
-    }
-  };
-
-  const handleTare = async () => {
-    setTaring(true);
-    try {
-      await spoolbuddyApi.tare(device.device_id);
-    } catch (e) {
-      console.error('Failed to tare:', e);
-    } finally {
-      setTaring(false);
-    }
-  };
-
-  const startCalibration = () => {
-    setCalStep('tare');
-  };
-
-  const handleCalStep = async () => {
-    if (calStep === 'tare') {
-      setCalibrating(true);
-      try {
-        // Capture raw ADC before taring — this is our zero reference
-        setTareRawAdc(rawAdc);
-        await spoolbuddyApi.tare(device.device_id);
-        setCalStep('weight');
-      } catch (e) {
-        console.error('Failed to tare:', e);
-      } finally {
-        setCalibrating(false);
-      }
-    } else if (calStep === 'weight') {
-      const weightNum = parseFloat(knownWeight);
-      if (rawAdc === null || !weightNum || weightNum <= 0) return;
-      setCalibrating(true);
-      try {
-        await spoolbuddyApi.setCalibrationFactor(device.device_id, weightNum, rawAdc, tareRawAdc ?? undefined);
-        setCalStep('idle');
-      } catch (e) {
-        console.error('Failed to calibrate:', e);
-      } finally {
-        setCalibrating(false);
-      }
-    }
-  };
-
-  return (
-    <div className="space-y-4">
-      {/* Current weight */}
-      <div className="bg-zinc-800 rounded-lg p-4">
-        <div className="flex items-center justify-between">
-          <span className="text-sm text-zinc-400">{t('spoolbuddy.settings.currentWeight', 'Current weight')}</span>
-          <div className="flex items-center gap-2">
-            <div className={`w-2 h-2 rounded-full ${weightStable ? 'bg-green-500' : 'bg-amber-500 animate-pulse'}`} />
-            <span className="text-lg font-mono text-zinc-200">
-              {weight !== null ? `${weight.toFixed(1)} g` : '-- g'}
-            </span>
-          </div>
-        </div>
-
-        {/* Tare offset + calibration factor */}
-        <div className="grid grid-cols-2 gap-4 mt-3 text-xs">
-          <div className="flex justify-between">
-            <span className="text-zinc-500">{t('spoolbuddy.settings.tareOffset', 'Tare offset')}</span>
-            <span className="text-zinc-400 font-mono">{device.tare_offset}</span>
-          </div>
-          <div className="flex justify-between">
-            <span className="text-zinc-500">{t('spoolbuddy.settings.calFactor', 'Cal. factor')}</span>
-            <span className="text-zinc-400 font-mono">{device.calibration_factor.toFixed(2)}</span>
-          </div>
-        </div>
-      </div>
-
-      {/* Calibration flow */}
-      {calStep === 'idle' ? (
-        <div className="flex gap-2">
-          <button
-            onClick={handleTare}
-            disabled={taring}
-            className="flex-1 px-4 py-2.5 rounded-lg text-sm font-medium bg-zinc-700 text-zinc-200 hover:bg-zinc-600 disabled:opacity-40 transition-colors min-h-[44px]"
-          >
-            {taring ? '...' : t('spoolbuddy.weight.tare', 'Tare')}
-          </button>
-          <button
-            onClick={startCalibration}
-            className="flex-1 px-4 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 transition-colors min-h-[44px]"
-          >
-            {t('spoolbuddy.weight.calibrate', 'Calibrate')}
-          </button>
-        </div>
-      ) : (
-        <div className="bg-zinc-800 border border-zinc-700 rounded-lg p-3 space-y-2">
-          <div className="text-sm font-medium text-zinc-200">
-            {calStep === 'tare'
-              ? t('spoolbuddy.settings.calStep1', 'Step 1: Remove all items from the scale')
-              : t('spoolbuddy.settings.calStep2', 'Step 2: Place known weight on scale')}
-          </div>
-
-          {calStep === 'weight' && (
-            <div className="space-y-1.5">
-              <div className="flex items-center gap-2">
-                <span className="text-xs text-zinc-400">{t('spoolbuddy.settings.knownWeight', 'Known weight (g)')}</span>
-                <div className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-3 py-1.5 text-right text-base font-mono text-zinc-100">
-                  {knownWeight || '0'}<span className="text-zinc-500 ml-1">g</span>
-                </div>
-              </div>
-              <div className="grid grid-cols-4 gap-1">
-                {['7','8','9','backspace','4','5','6','.','1','2','3','0'].map((key) => (
-                  <button
-                    key={key}
-                    onClick={() => numpadPress(key)}
-                    className={`py-2 rounded text-sm font-medium transition-colors min-h-[36px] ${
-                      key === 'backspace'
-                        ? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
-                        : 'bg-zinc-800 text-zinc-100 hover:bg-zinc-700 border border-zinc-700'
-                    }`}
-                  >
-                    {key === 'backspace' ? '\u232B' : key}
-                  </button>
-                ))}
-              </div>
-            </div>
-          )}
-
-          <div className="flex gap-2">
-            <button
-              onClick={() => setCalStep('idle')}
-              className="flex-1 px-4 py-2 rounded-lg text-sm bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors min-h-[40px]"
-            >
-              {t('common.cancel', 'Cancel')}
-            </button>
-            <button
-              onClick={handleCalStep}
-              disabled={calibrating}
-              className="flex-1 px-4 py-2 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors min-h-[40px]"
-            >
-              {calibrating ? '...' : calStep === 'tare' ? t('spoolbuddy.settings.setZero', 'Set Zero') : t('spoolbuddy.settings.calibrateNow', 'Calibrate')}
-            </button>
-          </div>
-        </div>
-      )}
-    </div>
-  );
-}
-
-export function SpoolBuddyCalibrationPage() {
-  const { sbState } = useOutletContext<SpoolBuddyOutletContext>();
-  const { t } = useTranslation();
-  const navigate = useNavigate();
-
-  const { data: devices = [] } = useQuery({
-    queryKey: ['spoolbuddy-devices'],
-    queryFn: () => spoolbuddyApi.getDevices(),
-    refetchInterval: 10000,
-  });
-
-  const device = sbState.deviceId
-    ? devices.find((d) => d.device_id === sbState.deviceId) ?? devices[0]
-    : devices[0];
-
-  return (
-    <div className="h-full flex flex-col p-4">
-      <div className="flex items-center gap-3 mb-4">
-        <button
-          onClick={() => navigate('/spoolbuddy/settings')}
-          className="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 transition-colors"
-        >
-          <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
-            <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
-          </svg>
-        </button>
-        <h1 className="text-xl font-semibold text-zinc-100">
-          {t('spoolbuddy.settings.scaleCalibration', 'Scale Calibration')}
-        </h1>
-      </div>
-
-      <div className="flex-1 min-h-0 overflow-y-auto">
-        {!device ? (
-          <div className="flex items-center justify-center h-32">
-            <div className="text-center text-zinc-500">
-              <p className="text-sm">{t('spoolbuddy.settings.noDevice', 'No SpoolBuddy device found')}</p>
-            </div>
-          </div>
-        ) : (
-          <ScaleCalibration
-            device={device}
-            weight={sbState.weight}
-            weightStable={sbState.weightStable}
-            rawAdc={sbState.rawAdc}
-          />
-        )}
-      </div>
-    </div>
-  );
-}

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/AdditionalSection-BXLQTBd6.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/AmsUnitCard-D00iS9DC.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ArchivesPage-DJdJWmtj.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/AreaChart-CjMaR62C.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/AssignToAmsModal-DkvnHNXK.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/AuthContext-BfmcZb9G.js


+ 1 - 1
static/assets/Button-BXOnmmh7.js → static/assets/Button-B7HH19dk.js

@@ -1 +1 @@
-import{C as e}from"./client-1U8CP3-A.js";var t=e();function n({variant:e=`primary`,size:n=`md`,className:r=``,children:i,...a}){return(0,t.jsx)(`button`,{className:`inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-bambu-dark disabled:opacity-50 disabled:cursor-not-allowed ${{primary:`bg-bambu-green hover:bg-bambu-green-light text-white focus:ring-bambu-green`,secondary:`bg-bambu-dark-tertiary hover:bg-bambu-gray-dark text-white focus:ring-bambu-gray`,danger:`bg-red-600 hover:bg-red-700 text-white focus:ring-red-500`,ghost:`bg-transparent hover:bg-bambu-dark-tertiary text-bambu-gray-light hover:text-white`}[e]} ${{sm:`px-3 py-1.5 text-sm gap-1.5 min-h-[44px] md:min-h-0`,md:`px-4 py-2 text-sm gap-2 min-h-[44px] md:min-h-0`,lg:`px-6 py-3 text-base gap-2 min-h-[48px] md:min-h-0`}[n]} ${r}`,...a,children:i})}export{n as t};
+import{w as e}from"./client-p9vNUtaN.js";var t=e();function n({variant:e=`primary`,size:n=`md`,className:r=``,children:i,...a}){return(0,t.jsx)(`button`,{className:`inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-bambu-dark disabled:opacity-50 disabled:cursor-not-allowed ${{primary:`bg-bambu-green hover:bg-bambu-green-light text-white focus:ring-bambu-green`,secondary:`bg-bambu-dark-tertiary hover:bg-bambu-gray-dark text-white focus:ring-bambu-gray`,danger:`bg-red-600 hover:bg-red-700 text-white focus:ring-red-500`,ghost:`bg-transparent hover:bg-bambu-dark-tertiary text-bambu-gray-light hover:text-white`}[e]} ${{sm:`px-3 py-1.5 text-sm gap-1.5 min-h-[44px] md:min-h-0`,md:`px-4 py-2 text-sm gap-2 min-h-[44px] md:min-h-0`,lg:`px-6 py-3 text-base gap-2 min-h-[48px] md:min-h-0`}[n]} ${r}`,...a,children:i})}export{n as t};

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CamWallPage-B3DtcQOQ.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraDiagnoseModal-CdYuChg8.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraDiagnoseModal-ilwgjRj_.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraPage-DVUKNaO5.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraPage-De72-X7F.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraWall-Bsfqfb9Q.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CameraWall-EsX_py5Z.js


+ 1 - 1
static/assets/Card-PxwWH-ub.js → static/assets/Card-DgaxYs1A.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-1U8CP3-A.js";var r=e(n(),1),i=t(),a=(0,r.createContext)(`normal`);function o({density:e,children:t}){return(0,i.jsx)(a.Provider,{value:e,children:t})}function s({children:e,className:t=``,onClick:n,onContextMenu:r,...a}){return(0,i.jsx)(`div`,{className:`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${n?`cursor-pointer`:``} ${t}`,onClick:n,onContextMenu:r,...a,children:e})}function c({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`px-4 py-2.5`:`px-6 py-4`} border-b border-bambu-dark-tertiary ${t}`,children:e})}function l({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`p-4`:`p-6`} ${t}`,children:e})}export{c as i,l as n,o as r,s as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,w as n}from"./client-p9vNUtaN.js";var r=e(t(),1),i=n(),a=(0,r.createContext)(`normal`);function o({density:e,children:t}){return(0,i.jsx)(a.Provider,{value:e,children:t})}function s({children:e,className:t=``,onClick:n,onContextMenu:r,...a}){return(0,i.jsx)(`div`,{className:`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${n?`cursor-pointer`:``} ${t}`,onClick:n,onContextMenu:r,...a,children:e})}function c({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`px-4 py-2.5`:`px-6 py-4`} border-b border-bambu-dark-tertiary ${t}`,children:e})}function l({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`p-4`:`p-6`} ${t}`,children:e})}export{c as i,l as n,o as r,s as t};

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CartesianChart-DEv1ukUA.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ColumnConfigModal-C3ITDxW6.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ConfigureAmsSlotModal-CfRenMqW.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ConnectAuthorizePage-ByS3HAVw.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ContextMenu-TouYu-I8.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ContextMenu-ujcH62Z3.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CopyButton-D37Ln8vd.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/CopyButton-D50XFTJw.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ErrorBarContext-CsQLj0aW.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ExternalLinkPage-B8V6zVXd.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
static/assets/FileManagerPage-BWxGLI3L.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/FileUploadModal-BB_T27UR.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/FinancePage-DA7oJeE7.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/FinancePage-DNp3Qbe0.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/GCodeViewerPage-BAdDJcj4.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/GcodeToolpathViewer-B5RO-cba.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/GroupEditPage-OmF8VkK1.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ImagePreviewModal-DBnkDnR9.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/InventoryPage-BFQlzCRH.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/LibraryTrashPage-C4Y77NYs.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/LoginPage-DZylQhD7.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/MaintenancePage-Bqc2qiUi.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/MaintenancePage-C_HJ2-Xk.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ModelSourcesPage-DUvT0hO1.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ModelSourcesPage-Up0E9JjS.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/NotificationsPage-BC8mBoNa.js


+ 1 - 1
static/assets/OverlayFrame-C5o8QwmR.js → static/assets/OverlayFrame-D1QU_K6T.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r}from"./client-1U8CP3-A.js";var i=e(n(),1);function a(e,t=null,n=0){let[a,o]=(0,i.useState)(null);return(0,i.useEffect)(()=>{if(!e)return;let i=new AbortController,a;return r.getOverlayLogo(t,i.signal).then(e=>{i.signal.aborted||(e?(a=URL.createObjectURL(e),o({url:a,token:t,revision:n})):o(null))}).catch(()=>{i.signal.aborted||o(null)}),()=>{i.abort(),a&&URL.revokeObjectURL(a)}},[e,t,n]),e&&a?.token===t&&a.revision===n?a.url:null}var o={logo:!1,logoRevision:0,from:``,to:``};function s(e){return/^#[0-9a-f]{6}$/i.test(e)}function c(e,t){return e&&t&&s(e)&&s(t)?`linear-gradient(to right, ${e}, ${t})`:void 0}function l(e){if(e)return{backgroundImage:e,backgroundClip:`text`,WebkitBackgroundClip:`text`,color:`transparent`,WebkitTextFillColor:`transparent`}}var u={landscape:{width:1920,height:1080},portrait:{width:1080,height:1920}},d=t();function f({layout:e,preview:t=!1,children:n}){let r=(0,i.useRef)(null),[a,o]=(0,i.useState)(0),{width:s,height:c}=u[e];(0,i.useLayoutEffect)(()=>{let e=r.current;if(!e)return;let t=()=>o(Math.min(e.clientWidth/s,e.clientHeight/c));if(t(),typeof ResizeObserver>`u`)return window.addEventListener(`resize`,t),()=>window.removeEventListener(`resize`,t);let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[s,c]);let l={width:s,height:c,"--overlay-vmin":`10.8px`,"--overlay-height":`${c}px`,position:`absolute`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%) scale(${a})`,transformOrigin:`center`};return(0,d.jsx)(`div`,{ref:r,className:t?`relative w-full overflow-hidden`:`fixed inset-0 overflow-hidden`,style:t?{aspectRatio:`${s} / ${c}`}:void 0,children:(0,d.jsx)(`div`,{style:l,children:n})})}export{c as a,s as i,u as n,l as o,o as r,a as s,f as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,a as n,w as r}from"./client-p9vNUtaN.js";var i=e(t(),1);function a(e,t=null,r=0){let[a,o]=(0,i.useState)(null);return(0,i.useEffect)(()=>{if(!e)return;let i=new AbortController,a;return n.getOverlayLogo(t,i.signal).then(e=>{i.signal.aborted||(e?(a=URL.createObjectURL(e),o({url:a,token:t,revision:r})):o(null))}).catch(()=>{i.signal.aborted||o(null)}),()=>{i.abort(),a&&URL.revokeObjectURL(a)}},[e,t,r]),e&&a?.token===t&&a.revision===r?a.url:null}var o={logo:!1,logoRevision:0,from:``,to:``};function s(e){return/^#[0-9a-f]{6}$/i.test(e)}function c(e,t){return e&&t&&s(e)&&s(t)?`linear-gradient(to right, ${e}, ${t})`:void 0}function l(e){if(e)return{backgroundImage:e,backgroundClip:`text`,WebkitBackgroundClip:`text`,color:`transparent`,WebkitTextFillColor:`transparent`}}var u={landscape:{width:1920,height:1080},portrait:{width:1080,height:1920}},d=r();function f({layout:e,preview:t=!1,children:n}){let r=(0,i.useRef)(null),[a,o]=(0,i.useState)(0),{width:s,height:c}=u[e];(0,i.useLayoutEffect)(()=>{let e=r.current;if(!e)return;let t=()=>o(Math.min(e.clientWidth/s,e.clientHeight/c));if(t(),typeof ResizeObserver>`u`)return window.addEventListener(`resize`,t),()=>window.removeEventListener(`resize`,t);let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[s,c]);let l={width:s,height:c,"--overlay-vmin":`10.8px`,"--overlay-height":`${c}px`,position:`absolute`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%) scale(${a})`,transformOrigin:`center`};return(0,d.jsx)(`div`,{ref:r,className:t?`relative w-full overflow-hidden`:`fixed inset-0 overflow-hidden`,style:t?{aspectRatio:`${s} / ${c}`}:void 0,children:(0,d.jsx)(`div`,{style:l,children:n})})}export{c as a,s as i,u as n,l as o,o as r,a as s,f as t};

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
static/assets/PdfPreviewModal-BFLt-4gG.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
static/assets/PdfPreviewModal-CJcTx6-v.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/PhotoGalleryModal-CG4HnDt0.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/PrinterLocationsPage-DkGeOfP1.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/PrinterLocationsPage-oWYMgF_L.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/PrintersPage-DNsllYKD.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ProfilesPage-CuKL4VW9.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ProfilesPage-DzNK0i8W.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ProjectDetailPage-Cyz3s9Fa.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ProjectsPage-BkHB6VSK.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ProjectsPage-D4P5NxK3.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/QueryClientProvider-B5zKGR0V.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/QueuePage-DnGOIfo7.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/QueuePage-qy7-9qls.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ReferenceLine-CtwazpKK.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/RichTextEditor-Dvs3mDsk.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SettingsPage-CLe1k-pV.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SettingsPage-Dlwq6CUV.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SetupPage-CcR9qR2n.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SliceModal-DV2zsi09.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpoolBuddyAmsPage-Da7IuL5a.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-DQIvapoi.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpoolBuddyDashboard-DOLVK7C-.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-Yv_W3ELP.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
static/assets/SpoolBuddySettingsPage-C_N3Dlt1.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-CdHop1zy.js


+ 1 - 1
static/assets/SpoolIcon-Ckk9N7-6.js → static/assets/SpoolIcon-DAYZYbv-.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-1U8CP3-A.js";import{n as r}from"./filamentSwatchHelpers-D3oXQSGk.js";import{t as i}from"./spoolPaint-CrvpHqdD.js";var a=e(n(),1),o=t();function s({spool:e,inset:t,size:n,border:i}){let s=n>=80?`card`:`preview`,c=(0,a.useMemo)(()=>r({effectSize:s,rgba:e.rgba,extraColors:e.extra_colors,effectType:e.effect_type,subtype:e.subtype}),[s,e.rgba,e.extra_colors,e.effect_type,e.subtype]),l=`${t*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-paint`,className:`absolute rounded-full box-border`,style:{top:l,left:l,right:l,bottom:l,...c,backgroundPosition:`center`,border:i}})}function c({inset:e}){let t=`${e*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-shade`,className:`absolute rounded-full`,style:{top:t,left:t,right:t,bottom:t,backgroundColor:`rgba(0, 0, 0, 0.15)`}})}function l({color:e,isEmpty:t,size:n=32,spool:r}){return t?(0,o.jsx)(`div`,{className:`rounded-full border-2 border-dashed border-zinc-500 flex items-center justify-center`,style:{width:n,height:n},children:(0,o.jsx)(`div`,{className:`w-2 h-2 rounded-full bg-zinc-600`})}):i(r)?(0,o.jsxs)(`div`,{className:`relative shrink-0`,style:{width:n,height:n},children:[(0,o.jsx)(s,{spool:r,size:n,inset:2/32,border:`${1.5*n/32}px solid rgba(255, 255, 255, 0.7)`}),(0,o.jsx)(c,{inset:5/32})]}):(0,o.jsxs)(`svg`,{width:n,height:n,viewBox:`0 0 32 32`,children:[(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`14`,fill:e,stroke:`white`,strokeWidth:`1.5`,strokeOpacity:`0.7`}),(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`11`,fill:e,style:{filter:`brightness(0.85)`}})]})}export{s as n,c as r,l as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,w as n}from"./client-p9vNUtaN.js";import{n as r}from"./filamentSwatchHelpers-D3oXQSGk.js";import{t as i}from"./spoolPaint-CrvpHqdD.js";var a=e(t(),1),o=n();function s({spool:e,inset:t,size:n,border:i}){let s=n>=80?`card`:`preview`,c=(0,a.useMemo)(()=>r({effectSize:s,rgba:e.rgba,extraColors:e.extra_colors,effectType:e.effect_type,subtype:e.subtype}),[s,e.rgba,e.extra_colors,e.effect_type,e.subtype]),l=`${t*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-paint`,className:`absolute rounded-full box-border`,style:{top:l,left:l,right:l,bottom:l,...c,backgroundPosition:`center`,border:i}})}function c({inset:e}){let t=`${e*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-shade`,className:`absolute rounded-full`,style:{top:t,left:t,right:t,bottom:t,backgroundColor:`rgba(0, 0, 0, 0.15)`}})}function l({color:e,isEmpty:t,size:n=32,spool:r}){return t?(0,o.jsx)(`div`,{className:`rounded-full border-2 border-dashed border-zinc-500 flex items-center justify-center`,style:{width:n,height:n},children:(0,o.jsx)(`div`,{className:`w-2 h-2 rounded-full bg-zinc-600`})}):i(r)?(0,o.jsxs)(`div`,{className:`relative shrink-0`,style:{width:n,height:n},children:[(0,o.jsx)(s,{spool:r,size:n,inset:2/32,border:`${1.5*n/32}px solid rgba(255, 255, 255, 0.7)`}),(0,o.jsx)(c,{inset:5/32})]}):(0,o.jsxs)(`svg`,{width:n,height:n,viewBox:`0 0 32 32`,children:[(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`14`,fill:e,stroke:`white`,strokeWidth:`1.5`,strokeOpacity:`0.7`}),(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`11`,fill:e,style:{filter:`brightness(0.85)`}})]})}export{s as n,c as r,l as t};

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SpreadsheetPreviewModal-BNsbtV78.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/StatsPage-CrzPQqB3.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/StatsPage-DNE2a2-2.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/StreamOverlayPage-mrcjRGWl.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/SystemInfoPage-BdOpeaaL.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ToastContext-BMyWPryU.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/ToastContext-Bhj9DS2p.js


+ 1 - 1
static/assets/Toggle-M-zinw5Z.js → static/assets/Toggle-BZiPSowU.js

@@ -1 +1 @@
-import{C as e}from"./client-1U8CP3-A.js";var t=e();function n({checked:e,onChange:n,disabled:r}){return(0,t.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:r,onClick:t=>{t.preventDefault(),t.stopPropagation(),r||n(!e)},className:`relative inline-flex w-11 h-7 md:w-9 md:h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${r?`bg-bambu-dark-tertiary/50 cursor-not-allowed opacity-50`:e?`bg-bambu-green cursor-pointer`:`bg-bambu-dark-tertiary cursor-pointer hover:bg-bambu-dark-tertiary/80`}`,children:(0,t.jsx)(`span`,{className:`pointer-events-none absolute top-[3px] md:top-[2px] left-[3px] md:left-[2px] w-5 h-5 md:w-4 md:h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${e?`translate-x-4`:`translate-x-0`}`})})}export{n as t};
+import{w as e}from"./client-p9vNUtaN.js";var t=e();function n({checked:e,onChange:n,disabled:r}){return(0,t.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:r,onClick:t=>{t.preventDefault(),t.stopPropagation(),r||n(!e)},className:`relative inline-flex w-11 h-7 md:w-9 md:h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${r?`bg-bambu-dark-tertiary/50 cursor-not-allowed opacity-50`:e?`bg-bambu-green cursor-pointer`:`bg-bambu-dark-tertiary cursor-pointer hover:bg-bambu-dark-tertiary/80`}`,children:(0,t.jsx)(`span`,{className:`pointer-events-none absolute top-[3px] md:top-[2px] left-[3px] md:left-[2px] w-5 h-5 md:w-4 md:h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${e?`translate-x-4`:`translate-x-0`}`})})}export{n as t};

+ 1 - 0
static/assets/arrow-down-DrsjDA8H.js

@@ -0,0 +1 @@
+import{C as e}from"./client-p9vNUtaN.js";var t=e(`arrow-down`,[[`path`,{d:`M12 5v14`,key:`s699le`}],[`path`,{d:`m19 12-7 7-7-7`,key:`1idqje`}]]);export{t};

+ 0 - 1
static/assets/arrow-down-VmQok7Hw.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-down`,[[`path`,{d:`M12 5v14`,key:`s699le`}],[`path`,{d:`m19 12-7 7-7-7`,key:`1idqje`}]]);export{t};

+ 0 - 1
static/assets/arrow-left-BcrTUh-S.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};

+ 1 - 0
static/assets/arrow-left-DG60mWnu.js

@@ -0,0 +1 @@
+import{C as e}from"./client-p9vNUtaN.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};

+ 1 - 0
static/assets/arrow-right-CLqCre6h.js

@@ -0,0 +1 @@
+import{C as e}from"./client-p9vNUtaN.js";var t=e(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]);export{t};

+ 0 - 1
static/assets/arrow-right-bfq0Qx0q.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]);export{t};

+ 1 - 0
static/assets/arrow-up-C169xoJg.js

@@ -0,0 +1 @@
+import{C as e}from"./client-p9vNUtaN.js";var t=e(`arrow-up`,[[`path`,{d:`m5 12 7-7 7 7`,key:`hav0vg`}],[`path`,{d:`M12 19V5`,key:`x0mq9r`}]]);export{t};

+ 0 - 1
static/assets/arrow-up-DXtx7YeZ.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-up`,[[`path`,{d:`m5 12 7-7 7 7`,key:`hav0vg`}],[`path`,{d:`M12 19V5`,key:`x0mq9r`}]]);export{t};

+ 1 - 0
static/assets/arrow-up-down-BiE6NqjL.js

@@ -0,0 +1 @@
+import{C as e}from"./client-p9vNUtaN.js";var t=e(`arrow-up-down`,[[`path`,{d:`m21 16-4 4-4-4`,key:`f6ql7i`}],[`path`,{d:`M17 20V4`,key:`1ejh1v`}],[`path`,{d:`m3 8 4-4 4 4`,key:`11wl7u`}],[`path`,{d:`M7 4v16`,key:`1glfcx`}]]);export{t};

+ 0 - 1
static/assets/arrow-up-down-DUGhy30I.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-up-down`,[[`path`,{d:`m21 16-4 4-4-4`,key:`f6ql7i`}],[`path`,{d:`M17 20V4`,key:`1ejh1v`}],[`path`,{d:`m3 8 4-4 4 4`,key:`11wl7u`}],[`path`,{d:`M7 4v16`,key:`1glfcx`}]]);export{t};

+ 0 - 1
static/assets/box-C9odJhAD.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`box`,[[`path`,{d:`M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z`,key:`hh9hay`}],[`path`,{d:`m3.3 7 8.7 5 8.7-5`,key:`g66t2b`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}]]);export{t};

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff