瀏覽代碼

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 4 天之前
父節點
當前提交
741539a740
共有 100 個文件被更改,包括 23 次插入 和 551 次删除
  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.
 
 ### 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.
 - **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.

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

@@ -2045,47 +2045,6 @@ async def calibrate_plate_detection(
     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")
 async def get_plate_detection_status(
     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
     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
     printer's screen (Settings > Firmware).
@@ -339,7 +338,7 @@ async def start_firmware_upload(
 
     return FirmwareUploadStartResponse(
         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),
 ):
     """
-    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)
     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
 """
 
-import asyncio
 import logging
 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.tasks import spawn_background_task
-from backend.app.core.websocket import ws_manager
 from backend.app.models.printer import Printer
 from backend.app.services.bambu_ftp import (
     get_ftp_retry_settings,
@@ -256,7 +254,6 @@ class FirmwareUpdateService:
         state = get_upload_state(printer_id)
         state.status = FirmwareUploadStatus.PREPARING
         state.message = "Preparing firmware update..."
-        await self._broadcast_progress(printer_id, state)
 
         # Run the upload in background
         spawn_background_task(
@@ -289,7 +286,6 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.DOWNLOADING
             state.progress = 0
             state.message = "Preparing firmware..."
-            await self._broadcast_progress(printer_id, state)
 
             firmware_path = await firmware_service.download_firmware(model, version=target_version)
 
@@ -310,26 +306,22 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.UPLOADING
             state.progress = 0
             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)
             remote_path = f"/{firmware_path.name}"
 
             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
 
             def on_upload_progress(uploaded: int, total: int):
                 nonlocal last_progress
                 if total > 0:
                     progress = int((uploaded / total) * 100)
-                    # Only broadcast every 1% to avoid flooding
                     if progress > last_progress:
                         last_progress = progress
                         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
             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! "
                 "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)
 
@@ -384,22 +375,6 @@ class FirmwareUpdateService:
             state.status = FirmwareUploadStatus.ERROR
             state.error = str(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

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

@@ -411,16 +411,6 @@ class PlateDetector:
         """Check if a printer has any calibration reference images."""
         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(
         self, image_data: bytes, printer_id: int, plate_type: str | None = None, include_debug_image: bool = False
     ) -> 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:
     """Check if plate detection feature is available (OpenCV installed)."""
     return OPENCV_AVAILABLE

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

@@ -676,25 +676,14 @@ class TestCameraAPI:
 
     @pytest.mark.asyncio
     @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()
 
-        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.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)
 ROUTES = [
     ("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"),
     ("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"),

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

@@ -178,21 +178,3 @@ class TestCalibrationStatus:
 
             pd_module.OPENCV_AVAILABLE = 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 SpoolBuddyAmsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyAmsPage'), 'SpoolBuddyAmsPage');
 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 SpoolBuddyInventoryPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyInventoryPage'), 'SpoolBuddyInventoryPage');
 
@@ -214,7 +213,8 @@ function App() {
                   <Route path="spoolbuddy/write-tag" element={<SpoolBuddyWriteTagPage />} />
                   <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
                   <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>
 
                 {/* 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}`, {
       method: 'DELETE',
     }),
-  getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
   getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
     request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
-  getAllCloudFields: () =>
-    request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
   getFilamentInfo: (settingIds: string[]) =>
     request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
       method: 'POST',
@@ -6516,7 +6513,6 @@ export const api = {
     if (targetModel) params.set('target_model', targetModel);
     return request<PrintQueueItem[]>(`/queue/?${params}`);
   },
-  getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
   addToQueue: (data: PrintQueueItemCreate) =>
     request<PrintQueueItem>('/queue/', {
       method: 'POST',
@@ -6578,7 +6574,6 @@ export const api = {
     const params = status ? `?status=${status}` : '';
     return request<PrintBatch[]>(`/queue/batches${params}`);
   },
-  getBatch: (id: number) => request<PrintBatch>(`/queue/batches/${id}`),
   cancelBatch: (id: number) =>
     request<{ message: string }>(`/queue/batches/${id}`, { method: 'DELETE' }),
   createBatch: (data: PrintBatchCreate) =>
@@ -6586,11 +6581,6 @@ export const api = {
       method: 'POST',
       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 = {}) =>
     request<PrintBatch>(`/queue/batches/${id}/dispatch`, {
       method: 'POST',
@@ -7469,8 +7459,6 @@ export const api = {
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`),
   getCameraSnapshotUrl: (printerId: number) =>
     withStreamToken(`${API_BASE}/printers/${printerId}/camera/snapshot`),
-  testCameraConnection: (printerId: number) =>
-    request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
   getCameraStatus: (printerId: number) =>
     request<{ active: boolean; stalled: boolean }>(`/printers/${printerId}/camera/status`),
   diagnoseCamera: (printerId: number) =>
@@ -7501,11 +7489,6 @@ export const api = {
       `/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 }) => {
     const params = new URLSearchParams();
     if (options?.label) params.set('label', options.label);
@@ -7517,12 +7500,6 @@ export const api = {
       { method: 'POST' }
     );
   },
-  deletePlateCalibration: (printerId: number) => {
-    return request<CalibrationResult>(
-      `/printers/${printerId}/camera/plate-detection/calibrate`,
-      { method: 'DELETE' }
-    );
-  },
   getPlateReferences: (printerId: number) => {
     return request<{
       references: PlateReference[];
@@ -8416,8 +8393,6 @@ export const api = {
   // Local Presets (OrcaSlicer imports)
   getLocalPresets: () =>
     request<LocalPresetsResponse>('/local-presets/'),
-  getLocalPresetDetail: (id: number) =>
-    request<LocalPresetDetail>(`/local-presets/${id}`),
   importLocalPresets: (formData: FormData) =>
     fetch(`${API_BASE}/local-presets/import`, {
       method: 'POST',
@@ -8430,20 +8405,8 @@ export const api = {
       }
       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) =>
     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

+ 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>
-  );
-}

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


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


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


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


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


File diff suppressed because it is too large
+ 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};

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


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


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/CameraPage-De72-X7F.js


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


File diff suppressed because it is too large
+ 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};

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


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


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/ContextMenu-TouYu-I8.js


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


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


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


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


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


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


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


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


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/GcodeToolpathViewer-B5RO-cba.js


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


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


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


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


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/MaintenancePage-C_HJ2-Xk.js


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


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


File diff suppressed because it is too large
+ 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};

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


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


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


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


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


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


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


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


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


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


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


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/QueuePage-qy7-9qls.js


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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/SettingsPage-CLe1k-pV.js


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


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


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


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


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


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


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


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


File diff suppressed because it is too large
+ 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};

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


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


File diff suppressed because it is too large
+ 0 - 0
static/assets/StatsPage-DNE2a2-2.js


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


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


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


File diff suppressed because it is too large
+ 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};

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