Explorar o código

chore: remove dead code (audit step 4): unused backend helpers and schema classes, 51 unused API client wrappers, test-only components (BackupModal, RestoreModal, VirtualPrinterSettings, TagDetectedModal, WeightDisplay, useLongPress) and their translations; removed POST /printers/test, the two printer debug routes and the SpoolBuddy update-status channel; legacy virtual printer settings fields removed from the settings API; first-run setup redirect tested

maziggy hai 1 día
pai
achega
460c11ca30
Modificáronse 100 ficheiros con 148 adicións e 3364 borrados
  1. 0 88
      .basedpyright/baseline.json
  2. 3 0
      CHANGELOG.md
  3. 0 9
      backend/app/api/routes/archives.py
  4. 1 1
      backend/app/api/routes/orca_cloud.py
  5. 0 98
      backend/app/api/routes/printers.py
  6. 7 37
      backend/app/api/routes/settings.py
  7. 0 36
      backend/app/api/routes/spoolbuddy.py
  8. 0 17
      backend/app/api/routes/support.py
  9. 0 11
      backend/app/core/auth.py
  10. 0 4
      backend/app/core/printer_scope.py
  11. 0 4
      backend/app/main.py
  12. 0 87
      backend/app/models/auth_ephemeral.py
  13. 0 12
      backend/app/schemas/archive.py
  14. 0 6
      backend/app/schemas/auth.py
  15. 0 18
      backend/app/schemas/cloud.py
  16. 0 14
      backend/app/schemas/library.py
  17. 0 5
      backend/app/schemas/library_trash.py
  18. 0 8
      backend/app/schemas/maintenance.py
  19. 0 72
      backend/app/schemas/notification.py
  20. 0 7
      backend/app/schemas/notification_template.py
  21. 1 1
      backend/app/schemas/orca_cloud.py
  22. 0 44
      backend/app/schemas/printer.py
  23. 0 19
      backend/app/schemas/project.py
  24. 0 9
      backend/app/schemas/settings.py
  25. 0 7
      backend/app/schemas/spoolbuddy.py
  26. 0 6
      backend/app/services/bambu_cloud.py
  27. 0 135
      backend/app/services/bambu_ftp.py
  28. 3 18
      backend/app/services/bambu_mqtt.py
  29. 1 13
      backend/app/services/github_backup.py
  30. 0 5
      backend/app/services/github_restore.py
  31. 0 5
      backend/app/services/layer_timelapse.py
  32. 2 3
      backend/app/services/location_service.py
  33. 0 4
      backend/app/services/mqtt_smart_plug.py
  34. 0 3
      backend/app/services/notify_client.py
  35. 4 49
      backend/app/services/printer_manager.py
  36. 0 1
      backend/app/services/secret_fields.py
  37. 0 11
      backend/app/services/slicer_api.py
  38. 2 12
      backend/app/services/smart_plug_manager.py
  39. 0 72
      backend/app/services/spoolman.py
  40. 0 3
      backend/app/services/telegram_reactions.py
  41. 0 82
      backend/app/services/virtual_printer/certificate.py
  42. 11 25
      backend/app/services/virtual_printer/manager.py
  43. 0 5
      backend/app/utils/filament_types.py
  44. 0 21
      backend/app/utils/threemf_tools.py
  45. 0 1
      backend/tests/conftest.py
  46. 0 1
      backend/tests/integration/test_settings_and_plug_secrets.py
  47. 0 2
      backend/tests/integration/test_settings_api_key_scrubbing.py
  48. 0 88
      backend/tests/integration/test_spoolbuddy.py
  49. 0 2
      backend/tests/integration/test_spoolman_api.py
  50. 0 2
      backend/tests/integration/test_system_support_vp_scope.py
  51. 12 7
      backend/tests/integration/test_telegram_reactions.py
  52. 22 4
      backend/tests/integration/test_virtual_printer_api.py
  53. 5 127
      backend/tests/unit/services/test_bambu_ftp.py
  54. 0 33
      backend/tests/unit/services/test_layer_timelapse.py
  55. 4 5
      backend/tests/unit/services/test_notify_widget_client.py
  56. 0 45
      backend/tests/unit/services/test_printer_manager.py
  57. 0 23
      backend/tests/unit/services/test_slicer_api.py
  58. 0 15
      backend/tests/unit/services/test_smart_plug_manager.py
  59. 0 47
      backend/tests/unit/services/test_spoolman_service.py
  60. 12 55
      backend/tests/unit/services/test_virtual_printer.py
  61. 7 5
      backend/tests/unit/test_filament_type_equivalence.py
  62. 1 2
      backend/tests/unit/test_github_restore.py
  63. 0 38
      backend/tests/unit/test_permissions_stats_filter.py
  64. 0 1
      backend/tests/unit/test_printer_scope.py
  65. 1 39
      backend/tests/unit/test_spoolbuddy_schema_validation.py
  66. 0 186
      backend/tests/unit/test_spoolman_clear_location.py
  67. 0 40
      backend/tests/unit/test_support_helpers.py
  68. 13 13
      backend/tests/unit/test_threemf_tools.py
  69. 0 76
      backend/tests/unit/test_vp_certificate_rotation.py
  70. 1 1
      backend/tests/unit/test_vp_tls_ciphers.py
  71. 0 3
      frontend/src/__tests__/components/AdminDisableTwoFactor.test.tsx
  72. 0 88
      frontend/src/__tests__/components/BackupModal.test.tsx
  73. 0 3
      frontend/src/__tests__/components/BulkPrinterToolbarWiring.test.tsx
  74. 0 3
      frontend/src/__tests__/components/PrintModalBilling.test.tsx
  75. 0 89
      frontend/src/__tests__/components/RestoreModal.test.tsx
  76. 0 3
      frontend/src/__tests__/components/SkipObjectsModalWiring.test.tsx
  77. 0 2
      frontend/src/__tests__/components/SpoolmanSettings.test.tsx
  78. 0 110
      frontend/src/__tests__/components/TagDetectedModal.test.tsx
  79. 0 680
      frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx
  80. 0 80
      frontend/src/__tests__/components/WeightDisplay.test.tsx
  81. 0 1
      frontend/src/__tests__/components/spool-form/PAProfileSectionNozzle.test.tsx
  82. 7 23
      frontend/src/__tests__/components/spoolbuddy/SpoolBuddyColorName3090.test.tsx
  83. 27 45
      frontend/src/__tests__/contexts/AuthContext.test.tsx
  84. 0 161
      frontend/src/__tests__/hooks/useLongPress.test.ts
  85. 0 20
      frontend/src/__tests__/hooks/useSpoolBuddyState.test.ts
  86. 1 66
      frontend/src/__tests__/hooks/useWebSocket.test.ts
  87. 0 3
      frontend/src/__tests__/pages/InventoryPageArchivedConsumed.test.tsx
  88. 0 3
      frontend/src/__tests__/pages/InventoryPageCopyButton.test.tsx
  89. 0 3
      frontend/src/__tests__/pages/InventoryPageDeepLink.test.tsx
  90. 0 3
      frontend/src/__tests__/pages/InventoryPageGroupRestore.test.tsx
  91. 0 3
      frontend/src/__tests__/pages/InventoryPageLowStock.test.tsx
  92. 0 3
      frontend/src/__tests__/pages/InventoryPagePrintLabels.test.tsx
  93. 0 3
      frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx
  94. 0 3
      frontend/src/__tests__/pages/PrinterLocationsPage.test.tsx
  95. 0 3
      frontend/src/__tests__/pages/PrintersPageAmsSlotActionsWiring.test.tsx
  96. 0 3
      frontend/src/__tests__/pages/PrintersPageBulkActionsWiring.test.tsx
  97. 0 3
      frontend/src/__tests__/pages/PrintersPageCameraSplitButton.test.tsx
  98. 0 3
      frontend/src/__tests__/pages/PrintersPageDeletePowerWiring.test.tsx
  99. 0 3
      frontend/src/__tests__/pages/PrintersPageDropPermission.test.tsx
  100. 0 3
      frontend/src/__tests__/pages/PrintersPageFirmwareModalWiring.test.tsx

+ 0 - 88
.basedpyright/baseline.json

@@ -2159,30 +2159,6 @@
                     "lineCount": 1
                 }
             },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 45,
-                    "endColumn": 55,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 31,
-                    "endColumn": 41,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 35,
-                    "endColumn": 45,
-                    "lineCount": 1
-                }
-            },
             {
                 "code": "reportArgumentType",
                 "range": {
@@ -4398,14 +4374,6 @@
                     "endColumn": 88,
                     "lineCount": 1
                 }
-            },
-            {
-                "code": "reportArgumentType",
-                "range": {
-                    "startColumn": 58,
-                    "endColumn": 66,
-                    "lineCount": 1
-                }
             }
         ],
         "./backend/app/services/spoolman_tracking.py": [
@@ -4519,62 +4487,6 @@
             }
         ],
         "./backend/app/services/virtual_printer/certificate.py": [
-            {
-                "code": "reportAttributeAccessIssue",
-                "range": {
-                    "startColumn": 37,
-                    "endColumn": 43,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportAttributeAccessIssue",
-                "range": {
-                    "startColumn": 37,
-                    "endColumn": 43,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportAttributeAccessIssue",
-                "range": {
-                    "startColumn": 37,
-                    "endColumn": 43,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportAttributeAccessIssue",
-                "range": {
-                    "startColumn": 37,
-                    "endColumn": 43,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportCallIssue",
-                "range": {
-                    "startColumn": 20,
-                    "endColumn": 38,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportArgumentType",
-                "range": {
-                    "startColumn": 20,
-                    "endColumn": 49,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportCallIssue",
-                "range": {
-                    "startColumn": 20,
-                    "endColumn": 49,
-                    "lineCount": 1
-                }
-            },
             {
                 "code": "reportReturnType",
                 "range": {

+ 3 - 0
CHANGELOG.md

@@ -135,6 +135,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **Dutch (nl) is now a supported interface language (#2891, requested and contributed by @Igiegel)** — Adds `nl` as the fourteenth locale, listed as "Nederlands" in the language picker. The translation was contributed as a file on the issue and needed three corrections before it could be wired up, all of which the parity gate found. First, the nine `stats.timeframe.*` entries had their **keys** translated along with their values (`'today'` had become `'vandaag'`), which would have left the Statistics timeframe selector resolving nothing and rendering raw key names for every Dutch user — the values were kept and the keys restored. Second, the file was translated against an older `en.ts` and was 84 leaves short, missing the Filament Track Switch feed prompts, the AI-detection status strings, the no-3MF internal-history banner, the batch-order stranded-plate notices, the Avery starting-position field and the whole `locationHaSensors` section from #2824; those were translated and added. Rather than splice them in, `nl.ts` was regenerated from the `en.ts` skeleton with the contributor's strings carried over, so its structure, key order and section comments now match the reference locale exactly and a future diff against `en.ts` reads as content rather than as reordering. Third, 229 leaves were identical to English; each was checked individually and all were kept, because Dutch takes most technical UI vocabulary verbatim — printer, filament, status, nozzle, timelapse, dashboard — and Dutch slicer users use the English feature names (support, ironing, prime tower, gap fill) untranslated. Those 123 distinct values are now listed explicitly in a `NL_COGNATES` allow-list in `check-i18n-parity.mjs`, the same shape the other twelve locales use, so the exemption is an enumerated translator decision rather than a blanket skip. Parity green at 6264 leaves across all 14 locales.
 
 ### Changed
+- **Unused API routes and settings removed** — For anyone calling the API from scripts:
+  - **Removed routes:** `POST /printers/test` (it took the access code in the URL; adding a printer tests the connection itself), the two debug routes `POST /printers/{id}/debug/simulate-print-complete` and `GET /printers/{id}/runtime-debug`, and `POST /spoolbuddy/devices/{id}/update-status`, which SpoolBuddy stopped calling when updates moved to SSH.
+  - **Old virtual printer settings:** `GET /settings/virtual-printer` now returns only `tailscale_disabled`, `archive_name_source` and `status`, and `GET /settings` no longer lists `virtual_printer_enabled`, `virtual_printer_access_code` and `virtual_printer_mode` (`PUT /settings` ignores them). These settings stopped doing anything when each virtual printer got its own settings; use `/virtual-printers` instead.
 - **A grouping that ended in failures is marked failed** — A grouping whose jobs all failed, or failed and were cancelled, with nothing printed, stayed **active** for good, even through the start-up clean-up, and piled up next to real work. It is now marked **failed**, shown in red with a **Failed** filter on the Batches tab, and opens again if a job is added to it. Orders are unchanged: a failed run still leaves an order owing it.
 - **Deleting a custom maintenance type says it applies to every printer** — Maintenance types are shared, so deleting a custom one removes it, with its maintenance history, from every printer, including those of other teams when printer access is limited. The confirmation now says so.
 - **The web interface downloads about 70% less on each new version (#3175, requested by @mason9468)** — The interface's script files were sent uncompressed and with no caching instructions, so a phone fetched the whole 11 MB again and the browser asked for it on every load.

+ 0 - 9
backend/app/api/routes/archives.py

@@ -244,15 +244,6 @@ def _validate_user_filter_permission(current_user: User | ApiKeyActor | None, cr
         raise HTTPException(status_code=403, detail="Permission stats:filter_by_user required")
 
 
-def _apply_user_filter(conditions: list, created_by_id: int | None):
-    """Append created_by_id filter to conditions list if specified."""
-    if created_by_id is not None:
-        if created_by_id == -1:
-            conditions.append(PrintArchive.created_by_id.is_(None))
-        else:
-            conditions.append(PrintArchive.created_by_id == created_by_id)
-
-
 def _apply_run_user_filter(conditions: list, created_by_id: int | None):
     """Append created_by_id filter scoped to PrintLogEntry rows."""
     from backend.app.models.print_log import PrintLogEntry

+ 1 - 1
backend/app/api/routes/orca_cloud.py

@@ -679,7 +679,7 @@ async def get_profile(
     current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
 ):
     """Fetch a single profile's full content, shaped like
-    ``SlicerSettingDetail`` so the Bambu Cloud detail modal can render it
+    the frontend's ``SlicerSettingDetail`` so the Bambu Cloud detail modal can render it
     unchanged. The inner ``setting`` field is the raw slicer-format JSON
     Orca stores — same shape Bambu Cloud uses since OrcaSlicer is a
     BambuStudio fork."""

+ 0 - 98
backend/app/api/routes/printers.py

@@ -1036,22 +1036,6 @@ async def disconnect_printer(
     return {"connected": False}
 
 
-@router.post("/test")
-async def test_printer_connection(
-    ip_address: str,
-    serial_number: str,
-    access_code: str,
-    _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CREATE),
-):
-    """Test connection to a printer without saving."""
-    result = await printer_manager.test_connection(
-        ip_address=ip_address,
-        serial_number=serial_number,
-        access_code=access_code,
-    )
-    return result
-
-
 @router.post("/diagnostic", response_model=PrinterDiagnosticResult)
 async def diagnose_connection(
     req: DiagnosticRequest,
@@ -3549,54 +3533,6 @@ async def delete_ams_label(
     return {"success": True}
 
 
-@router.post("/{printer_id}/debug/simulate-print-complete")
-async def debug_simulate_print_complete(
-    printer_id: int,
-    db: AsyncSession = Depends(get_db),
-    _=RequirePrinterPermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
-):
-    """DEBUG: Simulate print completion to test freeze behavior.
-
-    This triggers the same code path as a real print completion,
-    without needing to wait for an actual print to finish.
-    """
-    from backend.app.main import _active_prints, on_print_complete
-    from backend.app.models.archive import PrintArchive
-
-    # Get the most recent archive for this printer
-    result = await db.execute(
-        select(PrintArchive)
-        .where(PrintArchive.printer_id == printer_id)
-        .order_by(PrintArchive.created_at.desc())
-        .limit(1)
-    )
-    archive = result.scalar_one_or_none()
-
-    if not archive:
-        raise HTTPException(status_code=404, detail="No archives found for this printer")
-
-    # Register this archive as "active" so on_print_complete can find it
-    filename = archive.file_path.split("/")[-1] if archive.file_path else "test.3mf"
-    subtask_name = archive.print_name or "Test Print"
-    _active_prints[(printer_id, filename)] = archive.id
-    _active_prints[(printer_id, subtask_name)] = archive.id
-
-    # Simulate print completion data
-    data = {
-        "status": "completed",
-        "filename": filename,
-        "subtask_name": subtask_name,
-        "timelapse_was_active": False,
-    }
-
-    logger.info("Simulating print complete for printer %s, archive %s", printer_id, archive.id)
-
-    # Call the actual on_print_complete handler
-    await on_print_complete(printer_id, data)
-
-    return {"success": True, "archive_id": archive.id, "message": "Print completion simulated"}
-
-
 # =============================================================================
 # Print Control Endpoints
 # =============================================================================
@@ -4780,40 +4716,6 @@ async def ams_unload(
     return {"success": True, "message": "Unloading filament"}
 
 
-@router.get("/{printer_id}/runtime-debug")
-async def get_runtime_debug(
-    printer_id: int,
-    _=RequirePrinterPermissionIfAuthEnabled(Permission.PRINTERS_READ),
-    db: AsyncSession = Depends(get_db),
-):
-    """Debug endpoint: Get runtime tracking status for a printer."""
-    result = await db.execute(select(Printer).where(Printer.id == printer_id))
-    printer = result.scalar_one_or_none()
-    if not printer:
-        raise HTTPException(404, "Printer not found")
-
-    state = printer_manager.get_status(printer_id)
-
-    return {
-        "printer_name": printer.name,
-        "runtime_seconds": printer.runtime_seconds,
-        "runtime_hours": printer.runtime_seconds / 3600.0 if printer.runtime_seconds else 0,
-        "print_hours_offset": printer.print_hours_offset,
-        "total_hours": (printer.runtime_seconds / 3600.0 if printer.runtime_seconds else 0)
-        + (printer.print_hours_offset or 0),
-        "last_runtime_update": printer.last_runtime_update.isoformat() if printer.last_runtime_update else None,
-        "mqtt_state": {
-            "connected": state.connected if state else False,
-            "state": state.state if state else None,
-            "progress": state.progress if state else None,
-            "gcode_file": state.gcode_file if state else None,
-        }
-        if state
-        else None,
-        "is_active": printer.is_active,
-    }
-
-
 @router.post("/{printer_id}/hms/execute-action")
 async def execute_hms_action(
     printer_id: int,

+ 7 - 37
backend/app/api/routes/settings.py

@@ -190,7 +190,6 @@ _BOOL_SETTING_KEYS = frozenset(
         "include_beta_updates",
         "announcements_enabled",
         "announcements_all_users",
-        "virtual_printer_enabled",
         "ftp_retry_enabled",
         "mqtt_enabled",
         "mqtt_use_tls",
@@ -1835,35 +1834,17 @@ async def get_virtual_printer_settings(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
     printer_scope: PrinterScope = RequestPrinterScope,
 ):
-    """Get virtual printer settings and status."""
-    from backend.app.services.virtual_printer import (
-        DEFAULT_VIRTUAL_PRINTER_MODEL,
-        virtual_printer_manager,
-    )
+    """Get the settings this route still sets, and the virtual printers' status.
+
+    Every other virtual printer setting lives on its own row
+    (``GET /virtual-printers``).
+    """
+    from backend.app.services.virtual_printer import virtual_printer_manager
 
-    enabled = await get_setting(db, "virtual_printer_enabled")
-    access_code = await get_setting(db, "virtual_printer_access_code")
-    mode = await get_setting(db, "virtual_printer_mode")
-    model = await get_setting(db, "virtual_printer_model")
-    target_printer_id = await get_setting(db, "virtual_printer_target_printer_id")
-    remote_interface_ip = await get_setting(db, "virtual_printer_remote_interface_ip")
     tailscale_disabled_raw = await get_setting(db, "virtual_printer_tailscale_disabled")
     archive_name_source = await get_setting(db, "virtual_printer_archive_name_source")
 
-    from backend.app.models.virtual_printer import VP_MODE_ARCHIVE, normalize_vp_mode
-
     return {
-        "enabled": enabled == "true" if enabled else False,
-        "access_code_set": bool(access_code),
-        # Normalize on read so older settings rows (with `immediate` /
-        # `print_queue`) come out as `archive` / `queue` for the frontend.
-        "mode": normalize_vp_mode(mode) or VP_MODE_ARCHIVE,
-        "model": model or DEFAULT_VIRTUAL_PRINTER_MODEL,
-        # A target the caller can't see is not named.
-        "target_printer_id": int(target_printer_id)
-        if target_printer_id and printer_scope.allows(int(target_printer_id))
-        else None,
-        "remote_interface_ip": remote_interface_ip or "",
         "tailscale_disabled": tailscale_disabled_raw == "true" if tailscale_disabled_raw else True,
         "archive_name_source": archive_name_source if archive_name_source in ("metadata", "filename") else "metadata",
         "status": _legacy_vp_status(virtual_printer_manager.get_status(), printer_scope),
@@ -1894,7 +1875,6 @@ async def update_virtual_printer_settings(
     from sqlalchemy import select
 
     from backend.app.models.virtual_printer import VirtualPrinter as FirstVP
-    from backend.app.services.virtual_printer import virtual_printer_manager
 
     current_target_id_str = await get_setting(db, "virtual_printer_target_printer_id")
     current_target_id = int(current_target_id_str) if current_target_id_str else None
@@ -1941,7 +1921,7 @@ async def update_virtual_printer_settings(
 
     if tailscale_disabled is not None:
         await set_setting(db, "virtual_printer_tailscale_disabled", "true" if tailscale_disabled else "false")
-        # The running virtual printers read it from the first row.
+        # The first virtual printer's card shows its Tailscale address by it.
         if first_vp is not None:
             first_vp.tailscale_disabled = tailscale_disabled
     if archive_name_source is not None:
@@ -1950,16 +1930,6 @@ async def update_virtual_printer_settings(
     await db.commit()
     db.expire_all()
 
-    if tailscale_disabled is not None:
-        try:
-            await virtual_printer_manager.sync_from_db()
-        except Exception as e:
-            logger.error("Failed to configure virtual printer: %s", e, exc_info=True)
-            return JSONResponse(
-                status_code=500,
-                content={"detail": "Failed to configure virtual printer. Check server logs for details."},
-            )
-
     return await get_virtual_printer_settings(db, printer_scope=printer_scope)
 
 

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

@@ -38,7 +38,6 @@ from backend.app.schemas.spoolbuddy import (
     TagRemovedRequest,
     TagScannedRequest,
     UpdateSpoolWeightRequest,
-    UpdateStatusRequest,
     WriteTagRequest,
     WriteTagResultRequest,
 )
@@ -1585,41 +1584,6 @@ async def get_ssh_public_key(
         raise HTTPException(status_code=500, detail="Failed to retrieve SSH public key") from e
 
 
-@router.post("/devices/{device_id}/update-status")
-async def report_update_status(
-    device_id: str,
-    req: UpdateStatusRequest,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
-):
-    """Daemon reports update progress back to the backend."""
-    result = await db.execute(select(SpoolBuddyDevice).where(SpoolBuddyDevice.device_id == device_id))
-    device = result.scalar_one_or_none()
-    if not device:
-        raise HTTPException(status_code=404, detail="Device not registered")
-
-    device.update_status = req.status
-    device.update_message = req.message
-    # Only "complete" clears pending_command here. "error" leaves it set so the user can retry
-    # via the UI. The SSH service's own _update_progress clears on both "complete" and "error"
-    # because it owns the full update lifecycle end-to-end.
-    if req.status == "complete":
-        device.pending_command = None
-    await db.commit()
-
-    logger.info("SpoolBuddy %s: update status=%s msg=%s", device_id, req.status, req.message)
-    await ws_manager.broadcast(
-        {
-            "type": "spoolbuddy_update",
-            "device_id": device_id,
-            "update_status": req.status,
-            "update_message": req.message,
-        }
-    )
-
-    return {"status": "ok"}
-
-
 # --- Background watchdog ---
 
 

+ 0 - 17
backend/app/api/routes/support.py

@@ -710,23 +710,6 @@ async def _collect_github_backup_info(db: AsyncSession) -> dict:
     }
 
 
-async def _check_url_reachable(url: str, timeout: float = 2.0) -> bool | None:
-    """Single HEAD/GET ping with a short timeout. Returns None if URL is empty."""
-    if not url or not url.strip():
-        return None
-    try:
-        import httpx
-
-        async with httpx.AsyncClient(timeout=timeout, verify=False) as client:  # nosec B501 — local sidecars often use self-signed; this is a reachability/health probe only, no secrets are sent
-            r = await client.get(url, follow_redirects=False)
-            # Anything that returned a status code counts as reachable, even 404
-            # (the API server is up, just the path was wrong) — separates network
-            # failure from configuration mistakes for the user.
-            return r.status_code is not None
-    except Exception:
-        return False
-
-
 async def _fetch_slicer_health(url: str, timeout: float = 2.0) -> dict | None:
     """Fetch ``/health`` from a slicer sidecar and extract the CLI version.
 

+ 0 - 11
backend/app/core/auth.py

@@ -1036,17 +1036,6 @@ async def verify_websocket_token_principal(token: str) -> tuple[str, int | None]
         return row.username or "", row.api_key_id
 
 
-async def verify_websocket_token(token: str) -> str | None:
-    """Verify a WebSocket connect token.
-
-    Returns the recorded ``username`` (possibly ``""`` for API-key
-    callers, never ``None`` on success) when the token is valid, or
-    ``None`` when it is missing / expired / unknown.
-    """
-    principal = await verify_websocket_token_principal(token)
-    return None if principal is None else principal[0]
-
-
 _NO_PRINTERS = PrinterScope(frozenset())
 
 

+ 0 - 4
backend/app/core/printer_scope.py

@@ -60,10 +60,6 @@ class PrinterScope:
             return self
         return PrinterScope(self.printer_ids & other.printer_ids)
 
-    def filter_ids(self, printer_ids: Iterable[int]) -> list[int]:
-        """``printer_ids`` minus the ones out of scope, order kept."""
-        return [pid for pid in printer_ids if self.allows(pid)]
-
     def where(self, column):
         """A WHERE clause limiting ``column`` (a printer id column) to the scope.
 

+ 0 - 4
backend/app/main.py

@@ -10976,10 +10976,6 @@ async def lifespan(app: FastAPI):
                     # but the K-profile (cali_idx) did not — the reporter's exact
                     # "loaded but no flow profile" symptom.
                     "kprofile_applied": detail.get("kprofile_applied", True),
-                    # Present on failure: whether any tray telemetry was seen in
-                    # the window (distinguishes "printer silent" from "printer
-                    # stored something else").
-                    "saw_tray": detail.get("saw_tray", False),
                 }
             )
         except Exception as e:

+ 0 - 87
backend/app/models/auth_ephemeral.py

@@ -109,93 +109,6 @@ class AuthEphemeralToken(Base):
     # and prevent accidentally leaving optional fields at their defaults.
     # ------------------------------------------------------------------
 
-    @classmethod
-    def new_pre_auth(
-        cls,
-        token: str,
-        username: str,
-        expires_at: datetime,
-        challenge_id: str | None = None,
-    ) -> AuthEphemeralToken:
-        """Create a pre-auth token (issued after password check, before 2FA)."""
-        return cls(
-            token=token,
-            token_type=TokenType.PRE_AUTH,
-            username=username,
-            expires_at=expires_at,
-            challenge_id=challenge_id,
-        )
-
-    @classmethod
-    def new_oidc_state(
-        cls,
-        token: str,
-        provider_id: int,
-        nonce: str,
-        code_verifier: str,
-        expires_at: datetime,
-    ) -> AuthEphemeralToken:
-        """Create an OIDC state token (CSRF protection + PKCE for authorize redirect)."""
-        return cls(
-            token=token,
-            token_type=TokenType.OIDC_STATE,
-            provider_id=provider_id,
-            nonce=nonce,
-            code_verifier=code_verifier,
-            expires_at=expires_at,
-        )
-
-    @classmethod
-    def new_oidc_exchange(
-        cls,
-        token: str,
-        username: str,
-        expires_at: datetime,
-    ) -> AuthEphemeralToken:
-        """Create an OIDC exchange token (bridge from callback to SPA)."""
-        return cls(
-            token=token,
-            token_type=TokenType.OIDC_EXCHANGE,
-            username=username,
-            expires_at=expires_at,
-        )
-
-    @classmethod
-    def new_password_reset(
-        cls,
-        token: str,
-        username: str,
-        expires_at: datetime,
-    ) -> AuthEphemeralToken:
-        """Create a password-reset token (single-use link emailed to the user)."""
-        return cls(
-            token=token,
-            token_type=TokenType.PASSWORD_RESET,
-            username=username,
-            expires_at=expires_at,
-        )
-
-    @classmethod
-    def new_email_otp_setup(
-        cls,
-        token: str,
-        username: str,
-        code_hash: str,
-        expires_at: datetime,
-    ) -> AuthEphemeralToken:
-        """Create an email-OTP setup token.
-
-        The ``code_hash`` is stored in the ``nonce`` column (field reuse
-        documented inline in the enable_email_otp endpoint).
-        """
-        return cls(
-            token=token,
-            token_type=TokenType.EMAIL_OTP_SETUP,
-            username=username,
-            nonce=code_hash,
-            expires_at=expires_at,
-        )
-
     @classmethod
     def new_connect_code(
         cls,

+ 0 - 12
backend/app/schemas/archive.py

@@ -254,15 +254,3 @@ class ProjectPageResponse(BaseModel):
     model_pictures: list[ProjectPageImage] = []
     profile_pictures: list[ProjectPageImage] = []
     thumbnails: list[ProjectPageImage] = []
-
-
-class ProjectPageUpdate(BaseModel):
-    """Update project page data in 3MF file."""
-
-    title: str | None = None
-    description: str | None = None
-    designer: str | None = None
-    license: str | None = None
-    copyright: str | None = None
-    profile_title: str | None = None
-    profile_description: str | None = None

+ 0 - 6
backend/app/schemas/auth.py

@@ -283,12 +283,6 @@ class BackupCodesResponse(BaseModel):
     message: str
 
 
-class EmailOTPEnableRequest(BaseModel):
-    """No body required — email is taken from the authenticated user's profile."""
-
-    pass
-
-
 class TwoFAVerifyRequest(BaseModel):
     pre_auth_token: str = Field(..., max_length=128)
     # TOTP/email codes are 6 digits; backup codes are 8 uppercase alphanumeric chars.

+ 0 - 18
backend/app/schemas/cloud.py

@@ -104,24 +104,6 @@ class SlicerSettingUpdate(BaseModel):
     setting: dict | None = Field(None, description="Setting key-value pairs to update")
 
 
-class SlicerSettingDetail(BaseModel):
-    """Detailed slicer setting/preset response."""
-
-    message: str | None = None
-    code: str | None = None
-    error: str | None = None
-    public: bool = False
-    version: str | None = None
-    type: str
-    name: str
-    update_time: str | None = None
-    nickname: str | None = None
-    base_id: str | None = None
-    setting: dict = Field(default_factory=dict)
-    filament_id: str | None = None
-    setting_id: str | None = None  # For response after create
-
-
 class SlicerSettingDeleteResponse(BaseModel):
     """Response from deleting a preset."""
 

+ 0 - 14
backend/app/schemas/library.py

@@ -122,20 +122,6 @@ class FolderTreeItem(BaseModel):
 # ============ File Schemas ============
 
 
-class FileCreate(BaseModel):
-    """Schema for creating a file entry (internal use after upload)."""
-
-    filename: str
-    file_path: str
-    file_type: str
-    file_size: int
-    file_hash: str | None = None
-    thumbnail_path: str | None = None
-    metadata: dict | None = None
-    folder_id: int | None = None
-    project_id: int | None = None
-
-
 class FileUpdate(IgnoresNullForRequiredColumns):
     """Schema for updating a file."""
 

+ 0 - 5
backend/app/schemas/library_trash.py

@@ -7,11 +7,6 @@ from datetime import datetime
 from pydantic import BaseModel, Field
 
 
-class PurgePreviewRequest(BaseModel):
-    older_than_days: int = Field(ge=1, le=3650, description="Age threshold in days.")
-    include_never_printed: bool = True
-
-
 class PurgePreviewResponse(BaseModel):
     count: int
     total_bytes: int

+ 0 - 8
backend/app/schemas/maintenance.py

@@ -49,10 +49,6 @@ class PrinterMaintenanceBase(BaseModel):
     enabled: bool = True
 
 
-class PrinterMaintenanceCreate(PrinterMaintenanceBase):
-    pass
-
-
 class PrinterMaintenanceUpdate(IgnoresNullForRequiredColumns):
     __orm_table__ = "printer_maintenance"
     custom_interval_hours: float | None = None
@@ -81,10 +77,6 @@ class MaintenanceHistoryBase(BaseModel):
     notes: str | None = None
 
 
-class MaintenanceHistoryCreate(MaintenanceHistoryBase):
-    pass
-
-
 class MaintenanceHistoryResponse(MaintenanceHistoryBase):
     id: int
     printer_maintenance_id: int

+ 0 - 72
backend/app/schemas/notification.py

@@ -356,78 +356,6 @@ class NotificationTestResponse(BaseModel):
     message: str
 
 
-# Provider-specific config schemas for documentation/validation reference
-class CallMeBotConfig(BaseModel):
-    """CallMeBot/WhatsApp configuration."""
-
-    phone: str = Field(..., description="Phone number with country code (e.g., +1234567890)")
-    apikey: str = Field(..., description="API key from CallMeBot")
-
-
-class NtfyConfig(BaseModel):
-    """ntfy configuration."""
-
-    server: str = Field(default="https://ntfy.sh", description="ntfy server URL")
-    topic: str = Field(..., description="Topic name to publish to")
-    auth_token: str | None = Field(default=None, description="Optional authentication token")
-    event_priorities: dict[str, int] | None = Field(
-        default=None,
-        description=(
-            "Per-event priority override. Keys are event names, either the provider's "
-            "toggle column ('on_print_failed', what the UI writes) or the bare event "
-            "name ('print_failed'); both are accepted. Values are ntfy priorities 1-5 "
-            "(1=min, 2=low, 3=default, 4=high, 5=urgent). Events without an entry use "
-            "ntfy's server-side default."
-        ),
-    )
-
-
-class GotifyConfig(BaseModel):
-    """Gotify configuration (#2743)."""
-
-    server: str = Field(..., description="Gotify server URL")
-    app_token: str = Field(..., description="Token of the Gotify application to post as")
-    event_priorities: dict[str, int] | None = Field(
-        default=None,
-        description=(
-            "Per-event priority override, keyed like NtfyConfig.event_priorities. Values are "
-            "levels 1-5 (min, low, default, high, urgent), sent to Gotify as 0, 2, 5, 8 and 10. "
-            "Events without an entry are sent at 5."
-        ),
-    )
-
-
-class PushoverConfig(BaseModel):
-    """Pushover configuration."""
-
-    user_key: str = Field(..., description="Your Pushover user key")
-    app_token: str = Field(..., description="Your Pushover application token")
-    priority: int = Field(default=0, ge=-2, le=2, description="Message priority (-2 to 2)")
-    # Emergency priority (2) only: how often to re-alert and when to stop.
-    # Pushover requires retry >= 30s and expire <= 10800s (3h).
-    retry: int = Field(default=60, ge=30, le=10800, description="Emergency re-alert interval in seconds (priority 2)")
-    expire: int = Field(default=3600, ge=30, le=10800, description="Emergency alert expiry in seconds (priority 2)")
-
-
-class TelegramConfig(BaseModel):
-    """Telegram bot configuration."""
-
-    bot_token: str = Field(..., description="Bot token from @BotFather")
-    chat_id: str = Field(..., description="Chat ID to send messages to")
-
-
-class EmailConfig(BaseModel):
-    """Email/SMTP configuration."""
-
-    smtp_server: str = Field(..., description="SMTP server hostname")
-    smtp_port: int = Field(default=587, description="SMTP port (587 for TLS, 465 for SSL)")
-    username: str = Field(..., description="SMTP username/email")
-    password: str = Field(..., description="SMTP password or app password")
-    from_email: str = Field(..., description="From email address")
-    to_email: str = Field(..., description="Recipient email address")
-    use_tls: bool = Field(default=True, description="Use TLS encryption")
-
-
 # Notification Log schemas
 class NotificationLogResponse(BaseModel):
     """Schema for notification log API responses."""

+ 0 - 7
backend/app/schemas/notification_template.py

@@ -500,13 +500,6 @@ class NotificationTemplateResponse(NotificationTemplateBase):
         from_attributes = True
 
 
-class TemplateVariableInfo(BaseModel):
-    """Information about a template variable."""
-
-    name: str
-    description: str
-
-
 class EventVariablesResponse(BaseModel):
     """Response for available variables per event type."""
 

+ 1 - 1
backend/app/schemas/orca_cloud.py

@@ -74,7 +74,7 @@ class OrcaProfileListResponse(BaseModel):
 
 
 class OrcaProfileDetail(BaseModel):
-    """Single profile's full content, shaped to match ``SlicerSettingDetail``
+    """Single profile's full content, shaped to match the frontend's ``SlicerSettingDetail``
     so the frontend's detail modal can render it without translation."""
 
     setting_id: str

+ 0 - 44
backend/app/schemas/printer.py

@@ -4,7 +4,6 @@ from pydantic import BaseModel, Field, field_validator, model_validator
 
 from backend.app.schemas.not_null import IgnoresNullForRequiredColumns
 from backend.app.schemas.printer_location import normalize_location_name
-from backend.app.utils.printer_models import supports_nozzle_flow_type
 
 
 def _mask_url_login(url: str | None) -> str | None:
@@ -136,49 +135,6 @@ class PrinterResponse(PrinterBase):
     class Config:
         from_attributes = True
 
-    @classmethod
-    def from_orm_with_roi(cls, printer) -> "PrinterResponse":
-        """Create response from ORM model, converting ROI fields to nested object."""
-        data = {
-            "id": printer.id,
-            "name": printer.name,
-            "serial_number": printer.serial_number,
-            "ip_address": printer.ip_address,
-            "model": printer.model,
-            "location": printer.location,
-            "auto_archive": printer.auto_archive,
-            "external_camera_url": printer.external_camera_url,
-            "external_camera_type": printer.external_camera_type,
-            "external_camera_enabled": printer.external_camera_enabled,
-            "external_camera_snapshot_url": printer.external_camera_snapshot_url,
-            "camera_rotation": printer.camera_rotation,
-            "camera_light_auto": bool(printer.camera_light_auto),
-            "is_active": printer.is_active,
-            "nozzle_count": printer.nozzle_count,
-            "supports_nozzle_flow_type": supports_nozzle_flow_type(printer.model),
-            "print_hours_offset": printer.print_hours_offset,
-            "wear_cost_per_hour": printer.wear_cost_per_hour,
-            "plate_detection_enabled": printer.plate_detection_enabled,
-            "created_at": printer.created_at,
-            "updated_at": printer.updated_at,
-        }
-        # Build ROI object if any ROI field is set
-        if any(
-            [
-                printer.plate_detection_roi_x is not None,
-                printer.plate_detection_roi_y is not None,
-                printer.plate_detection_roi_w is not None,
-                printer.plate_detection_roi_h is not None,
-            ]
-        ):
-            data["plate_detection_roi"] = PlateDetectionROI(
-                x=printer.plate_detection_roi_x or 0.15,
-                y=printer.plate_detection_roi_y or 0.35,
-                w=printer.plate_detection_roi_w or 0.70,
-                h=printer.plate_detection_roi_h or 0.55,
-            )
-        return cls(**data)
-
     @field_validator("external_camera_url", "external_camera_snapshot_url", mode="after")
     @classmethod
     def _camera_credentials(cls, value: str | None) -> str | None:

+ 0 - 19
backend/app/schemas/project.py

@@ -304,25 +304,6 @@ class LinkedFolderExport(BaseModel):
     name: str
 
 
-class ProjectExport(BaseModel):
-    """Schema for exporting a project."""
-
-    name: str
-    description: str | None
-    color: str | None
-    status: str
-    target_count: int | None
-    target_parts_count: int | None
-    target_sets: int | None = None
-    notes: str | None
-    tags: str | None
-    due_date: datetime | None
-    priority: str
-    budget: float | None
-    bom_items: list[BOMItemExport] = []
-    linked_folders: list[LinkedFolderExport] = []
-
-
 class ProjectImport(BaseModel):
     """Schema for importing a project."""
 

+ 0 - 9
backend/app/schemas/settings.py

@@ -233,12 +233,6 @@ class AppSettings(BaseModel):
     )
 
     # Virtual Printer
-    virtual_printer_enabled: bool = Field(default=False, description="Enable virtual printer for slicer uploads")
-    virtual_printer_access_code: str = Field(default="", description="Access code for virtual printer authentication")
-    virtual_printer_mode: str = Field(
-        default="archive",
-        description="Mode: 'archive' (archive now), 'review' (pending review), 'queue' (add to print queue), or 'proxy' (relay to real printer)",
-    )
     virtual_printer_archive_name_source: str = Field(
         default="metadata",
         description="Source for the archive's display name on virtual-printer uploads: 'metadata' uses the 3MF's embedded print_name (default, matches Bambu's behavior), 'filename' uses the filename Bambu Studio sent over FTP (lets users rename via the slicer's 'send to printer' dialog).",
@@ -773,9 +767,6 @@ class AppSettingsUpdate(BaseModel):
     time_format: str | None = None
     default_printer_id: int | None = None
     pipeline_max_copies: int | None = None
-    virtual_printer_enabled: bool | None = None
-    virtual_printer_access_code: str | None = None
-    virtual_printer_mode: str | None = None
     virtual_printer_archive_name_source: str | None = None
     dark_style: str | None = None
     dark_background: str | None = None

+ 0 - 7
backend/app/schemas/spoolbuddy.py

@@ -1,6 +1,5 @@
 import json
 from datetime import datetime
-from typing import Literal
 
 from pydantic import BaseModel, Field, field_validator
 
@@ -95,7 +94,6 @@ class TagScannedRequest(BaseModel):
     tray_uuid: str | None = Field(None, max_length=32, pattern=r"^[0-9A-Fa-f]*$")
     sak: int | None = None
     tag_type: str | None = Field(None, max_length=50)
-    raw_blocks: dict | None = None
 
 
 class TagRemovedRequest(BaseModel):
@@ -174,11 +172,6 @@ class SystemCommandResultRequest(BaseModel):
     message: str | None = Field(None, max_length=500)
 
 
-class UpdateStatusRequest(BaseModel):
-    status: Literal["updating", "complete", "error"]
-    message: str | None = Field(None, max_length=255)
-
-
 # --- Diagnostics schemas ---
 
 

+ 0 - 6
backend/app/services/bambu_cloud.py

@@ -778,12 +778,6 @@ class BambuCloudService:
         self.access_token = access_token
         self.token_expiry = None
 
-    def logout(self):
-        """Clear authentication state."""
-        self.access_token = None
-        self.refresh_token = None
-        self.token_expiry = None
-
     async def get_user_profile(self) -> dict:
         """Get user profile information."""
         if not self.is_authenticated:

+ 0 - 135
backend/app/services/bambu_ftp.py

@@ -605,12 +605,6 @@ class BambuFTPClient:
         # can say how long the session was held (#3009).
         self._connected_at: float | None = None
 
-    def _is_a1_model(self) -> bool:
-        """Check if this is an A1 series printer."""
-        if not self.printer_model:
-            return False
-        return self.printer_model in self.A1_MODELS
-
     def _get_cached_mode(self) -> str | None:
         """Get cached FTP mode for this printer."""
         return self._mode_cache.get(self.ip_address)
@@ -1093,62 +1087,6 @@ class BambuFTPClient:
             logger.info("FTP download failed for %s: %s", remote_path, e)
             return False
 
-    def diagnose_storage(self) -> dict:
-        """Run storage diagnostics and return results. For debugging upload issues."""
-        results = {
-            "connected": self._ftp is not None,
-            "can_list_root": False,
-            "root_files": [],
-            "can_list_cache": False,
-            "storage_info": None,
-            "pwd": None,
-            "errors": [],
-        }
-
-        if not self._ftp:
-            results["errors"].append("FTP not connected")
-            return results
-
-        # Try to get current directory
-        try:
-            results["pwd"] = self._ftp.pwd()
-            logger.debug("FTP current directory: %s", results["pwd"])
-        except (OSError, ftplib.Error) as e:
-            results["errors"].append(f"PWD failed: {e}")
-            logger.debug("FTP PWD failed: %s", e)
-
-        # Try to list root directory
-        try:
-            self._ftp.cwd("/")
-            items = []
-            self._ftp.retrlines("LIST", items.append)
-            results["can_list_root"] = True
-            results["root_files"] = items[:10]  # First 10 entries
-            logger.debug("FTP root listing (%s items): %s", len(items), items[:5])
-        except (OSError, ftplib.Error) as e:
-            results["errors"].append(f"LIST / failed: {e}")
-            logger.debug("FTP LIST / failed: %s", e)
-
-        # Try to list /cache (should exist on all printers)
-        try:
-            self._ftp.cwd("/cache")
-            items = []
-            self._ftp.retrlines("LIST", items.append)
-            results["can_list_cache"] = True
-            logger.debug("FTP /cache listing: %s items", len(items))
-        except (OSError, ftplib.Error) as e:
-            results["errors"].append(f"LIST /cache failed: {e}")
-            logger.debug("FTP LIST /cache failed: %s", e)
-
-        # Try to get storage info
-        try:
-            results["storage_info"] = self.get_storage_info()
-            logger.debug("FTP storage info: %s", results["storage_info"])
-        except (OSError, ftplib.Error) as e:
-            results["errors"].append(f"Storage info failed: {e}")
-
-        return results
-
     def upload_file(
         self,
         local_path: Path,
@@ -1353,79 +1291,6 @@ class BambuFTPClient:
             self.last_failure = FtpFailure(kind, str(e), code)
             return False
 
-    def upload_bytes(self, data: bytes, remote_path: str) -> bool:
-        """Upload bytes to the printer."""
-        if not self._ftp:
-            return False
-
-        try:
-            # Use manual transfer instead of storbinary() for A1 compatibility
-            conn = self._ftp.transfercmd(f"STOR {remote_path}")
-            conn.setblocking(True)
-            conn.settimeout(self.timeout)
-
-            try:
-                # Send data in chunks
-                offset = 0
-                while offset < len(data):
-                    chunk = data[offset : offset + self.CHUNK_SIZE]
-                    conn.sendall(chunk)
-                    offset += len(chunk)
-            except OSError as e:
-                logger.error("FTP connection lost during upload_bytes: %s", e)
-                raise
-            finally:
-                try:
-                    conn.close()
-                except OSError:
-                    pass
-            # Wait for 226 confirmation (see upload_file for rationale).
-            # ftplib.Error subclasses (e.g. 426 error_temp) mean the server
-            # rejected the transfer and the file is partial — fail. Other
-            # exceptions (timeout, socket-level) are tolerated as in upload_file.
-            try:
-                old_timeout = self._ftp.sock.gettimeout()
-                self._ftp.sock.settimeout(max(self.timeout, 60))
-                try:
-                    self._ftp.voidresp()
-                finally:
-                    self._ftp.sock.settimeout(old_timeout)
-            except ftplib.Error as e:
-                # Same SIZE-verify path as upload_file (#1417 follow-up):
-                # tolerate a transient 426 if the bytes are actually on the
-                # printer, fail loudly if they aren't.
-                try:
-                    server_size = self._ftp.size(remote_path)
-                except (OSError, ftplib.Error) as size_err:
-                    logger.debug("Post-error SIZE check failed: %s", size_err)
-                    server_size = None
-                if server_size is not None and server_size == len(data):
-                    # INFO for the same reason as upload_file above (#2987).
-                    logger.info(
-                        "FTP STOR returned %s for %s but file is intact on the "
-                        "printer (%s bytes match) — proceeding: %s",
-                        type(e).__name__,
-                        remote_path,
-                        len(data),
-                        e,
-                    )
-                else:
-                    logger.error(
-                        "FTP STOR rejected by printer for %s: %s (%s); server size=%s expected=%s",
-                        remote_path,
-                        e,
-                        type(e).__name__,
-                        server_size,
-                        len(data),
-                    )
-                    return False
-            except Exception:
-                # Timeout / socket-level — proceed, data was sent.
-                self._check_unconfirmed_upload(remote_path, len(data))
-            return True
-        except (OSError, ftplib.Error):
-            return False
-
     def _check_unconfirmed_upload(self, remote_path: str, expected_size: int) -> None:
         """Read an upload's size back when its 226 never came.
 

+ 3 - 18
backend/app/services/bambu_mqtt.py

@@ -2090,8 +2090,8 @@ class BambuMQTTClient:
                 # never show -- which is why "what does Studio put in the drying
                 # command?" had no answer from a user's log (#2774). Filed as
                 # "out" so the direction filter groups it with our own commands
-                # rather than with printer telemetry; anything sent through
-                # send_command lands twice, once on publish and once on the
+                # rather than with printer telemetry; anything we send
+                # lands twice, once on publish and once on the
                 # broker's echo, and the pair is itself evidence the command
                 # reached the broker.
                 if self._logging_enabled:
@@ -6819,21 +6819,6 @@ class BambuMQTTClient:
             # The callers drop the client from the manager anyway, so the next
             # status read already shows it gone.
 
-    def send_command(self, command: dict):
-        """Send a command to the printer."""
-        if self._client and self.state.connected:
-            # Log outgoing message if logging is enabled
-            if self._logging_enabled:
-                self._message_log.append(
-                    MQTTLogEntry(
-                        timestamp=datetime.now(timezone.utc).isoformat(),
-                        topic=self.topic_publish,
-                        direction="out",
-                        payload=command,
-                    )
-                )
-            self._client.publish(self.topic_publish, json.dumps(command), qos=1)
-
     def enable_logging(self, enabled: bool = True):
         """Enable or disable MQTT message logging."""
         self._logging_enabled = enabled
@@ -6873,7 +6858,7 @@ class BambuMQTTClient:
         """Publish a pre-formed payload directly to the printer's MQTT broker.
 
         Used by the VP MQTT bridge to forward slicer-originated commands without
-        going through send_command's sequence-id mangling. Returns False if the
+        giving it one of our own sequence ids. Returns False if the
         underlying paho client isn't ready.
         """
         if self._client is None:

+ 1 - 13
backend/app/services/github_backup.py

@@ -8,7 +8,7 @@ import logging
 from datetime import datetime, timedelta, timezone
 
 import httpx
-from sqlalchemy import desc, or_, select
+from sqlalchemy import or_, select
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from backend.app.core.database import async_session
@@ -964,18 +964,6 @@ class GitHubBackupService:
         """Get current backup progress message."""
         return self._backup_progress
 
-    async def get_logs(self, config_id: int, limit: int = 50, offset: int = 0) -> list[GitHubBackupLog]:
-        """Get backup logs for a configuration."""
-        async with async_session() as db:
-            result = await db.execute(
-                select(GitHubBackupLog)
-                .where(GitHubBackupLog.config_id == config_id)
-                .order_by(desc(GitHubBackupLog.started_at))
-                .offset(offset)
-                .limit(limit)
-            )
-            return list(result.scalars().all())
-
 
 # Singleton instance
 github_backup_service = GitHubBackupService()

+ 0 - 5
backend/app/services/github_restore.py

@@ -348,10 +348,6 @@ def _checked_setting(key: str, value: object) -> tuple[bool | None, str]:
 #   * the other four switch an integration on with no way to authenticate to it,
 #     which breaks the login path (LDAP) or the connection (MQTT, HA).
 #
-# virtual_printer_enabled is largely vestigial post-migration — core/database.py
-# copies the rows into the virtual_printers table — but it is the same shape, and
-# refusing a vestigial toggle is a harmless no-op.
-#
 # ldap_enabled is deliberately NOT here. It was, paired with
 # ldap_bind_password — but this rule judges availability ("will the integration
 # work?"), and that is the wrong question for an authentication source. An
@@ -363,7 +359,6 @@ _COMPANION_CREDENTIALS = {
     "prometheus_enabled": "prometheus_token",
     "mqtt_enabled": "mqtt_password",
     "ha_enabled": "ha_token",
-    "virtual_printer_enabled": "virtual_printer_access_code",
 }
 
 # Companion credentials a reader takes from the environment rather than from a

+ 0 - 5
backend/app/services/layer_timelapse.py

@@ -401,11 +401,6 @@ def cancel_session(printer_id: int):
         logger.info("Cancelled timelapse session for printer %s", printer_id)
 
 
-def get_active_sessions() -> dict[int, TimelapseSession]:
-    """Get all active timelapse sessions."""
-    return _active_sessions.copy()
-
-
 def cleanup_orphaned_timelapse_sessions(min_age_seconds: float = 300) -> int:
     """Remove timelapse_frames/<printer_id>/* left behind by a crash or
     restart that happened while a session was active.

+ 2 - 3
backend/app/services/location_service.py

@@ -20,9 +20,8 @@ logger = logging.getLogger(__name__)
 DUPLICATE_LOCATION_NAME = "A location with this name already exists"
 
 # AMS residency markers, not storage locations. Bambuddy used to write the slot
-# a spool was loaded into -- "<printer> - AMS A1", the shape
-# `SpoolmanClient.convert_ams_slot_to_location` still produces -- straight into
-# Spoolman's `location` field. That writer went away when Storage Location
+# a spool was loaded into -- "<printer> - AMS A1", "<printer> - AMS-HT A1" or
+# "<printer> - External Spool" -- straight into Spoolman's `location` field. That writer went away when Storage Location
 # became a place the user chooses (#1114), but the strings survive on people's
 # Spoolman spools, and importing them offers a printer slot as somewhere to put
 # a spool away. A slot is where a spool is loaded, not where it is stored, and

+ 0 - 4
backend/app/services/mqtt_smart_plug.py

@@ -69,10 +69,6 @@ class MQTTSmartPlugService:
         """Check if the MQTT service is configured and connected."""
         return self._configured and self.connected
 
-    def has_broker_settings(self) -> bool:
-        """Check if broker settings are available (even if not connected yet)."""
-        return bool(self._broker)
-
     async def configure(self, settings: dict) -> bool:
         """Configure MQTT connection from settings.
 

+ 0 - 3
backend/app/services/notify_client.py

@@ -291,9 +291,6 @@ class NotifyClient:
     async def update_widget(self, widget_id: str, token: str, content: dict) -> dict:
         return await self._request("POST", f"/widgets/{self._widget_id(widget_id)}", token, content=content)
 
-    async def get_widget(self, widget_id: str, token: str) -> dict:
-        return await self._request("GET", f"/widgets/{self._widget_id(widget_id)}", token)
-
     async def list_widgets(self, device_id: str, token: str) -> dict:
         result = await self._request("GET", f"/widgets/{self._id(device_id)}", token)
         widgets = result.get("widgets")

+ 4 - 49
backend/app/services/printer_manager.py

@@ -1036,51 +1036,6 @@ class PrinterManager:
             return self._clients[printer_id].stop_print()
         return False
 
-    async def wait_for_cooldown(
-        self,
-        printer_id: int,
-        target_temp: float = 50.0,
-        timeout: int = 600,
-        check_interval: int = 10,
-    ) -> bool:
-        """Wait for the nozzle to cool down to a safe temperature.
-
-        Args:
-            printer_id: The printer to monitor
-            target_temp: Target temperature to wait for (default 50°C)
-            timeout: Maximum seconds to wait (default 600s = 10 min)
-            check_interval: Seconds between temperature checks (default 10s)
-
-        Returns:
-            True if cooled down, False if timeout or not connected
-        """
-        import logging
-
-        logger = logging.getLogger(__name__)
-
-        elapsed = 0
-        while elapsed < timeout:
-            state = self.get_status(printer_id)
-            if not state or not state.connected:
-                logger.warning("Printer %s disconnected during cooldown wait", printer_id)
-                return False
-
-            # Check nozzle temperature (and nozzle_2 for dual extruders)
-            nozzle_temp = state.temperatures.get("nozzle", 0)
-            nozzle_2_temp = state.temperatures.get("nozzle_2", 0)
-            max_temp = max(nozzle_temp, nozzle_2_temp)
-
-            if max_temp <= target_temp:
-                logger.info("Printer %s cooled down to %s°C", printer_id, max_temp)
-                return True
-
-            logger.debug("Printer %s nozzle at %s°C, waiting for %s°C...", printer_id, max_temp, target_temp)
-            await asyncio.sleep(check_interval)
-            elapsed += check_interval
-
-        logger.warning("Printer %s cooldown timeout after %ss", printer_id, timeout)
-        return False
-
     def enable_logging(self, printer_id: int, enabled: bool = True) -> bool:
         """Enable or disable MQTT logging for a printer."""
         if printer_id in self._clients:
@@ -1861,10 +1816,10 @@ def printer_state_to_dict(
         result["cover_url"] = f"/api/v1/printers/{printer_id}/cover"
     else:
         result["cover_url"] = None
-    # Surface the display name + model so WS consumers (gcode viewer printer
-    # selector) can render proper labels on the initial snapshot without racing
-    # a separate /api/v1/printers fetch (#963 follow-up). PrinterInfo only
-    # carries name/serial_number; the model comes through via the `model` arg.
+    # The display name + model, so a WS consumer can label the printer from the
+    # snapshot alone (#963 follow-up; the SPA itself takes them from
+    # /api/v1/printers). PrinterInfo only carries name/serial_number; the model
+    # comes through via the `model` arg.
     if printer_id:
         _printer_info = printer_manager.get_printer(printer_id)
         if _printer_info is not None:

+ 0 - 1
backend/app/services/secret_fields.py

@@ -121,7 +121,6 @@ SETTINGS_SECRETS: dict[str, tuple[str, ...]] = {
     "ha_token": ("ha_url",),
     "obico_ml_token": ("obico_ml_url",),
     "prometheus_token": (),
-    "virtual_printer_access_code": (),
     "ldap_bind_password": ("ldap_server_url", "ldap_security"),
 }
 

+ 0 - 11
backend/app/services/slicer_api.py

@@ -408,17 +408,6 @@ class SlicerApiService:
     async def __aexit__(self, *_: object) -> None:
         await self.close()
 
-    async def health(self) -> dict:
-        """GET /health — used to surface a clear "sidecar offline" error before
-        accepting a slice request from the user."""
-        try:
-            response = await self._client.get(f"{self.base_url}/health", timeout=10.0)
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {_transport_error_reason(exc)}") from exc
-        if response.status_code >= 400:
-            raise SlicerApiUnavailableError(f"Slicer sidecar /health returned {response.status_code}")
-        return response.json()
-
     async def resolve_profile(self, profile_json: str, category: str) -> "ResolvedProfile":
         """POST /profiles/resolve — flatten a preset's ``inherits:`` chain.
 

+ 2 - 12
backend/app/services/smart_plug_manager.py

@@ -132,7 +132,6 @@ class SmartPlugManager:
 
     def __init__(self):
         self._pending_off: dict[int, asyncio.Task] = {}  # plug_id -> task
-        self._loop: asyncio.AbstractEventLoop | None = None
         self._scheduler_task: asyncio.Task | None = None
         self._snapshot_task: asyncio.Task | None = None
         self._power_alert_task: asyncio.Task | None = None
@@ -171,10 +170,6 @@ class SmartPlugManager:
         except Exception as e:
             logger.warning("Failed to configure HA service: %s", e)
 
-    def set_event_loop(self, loop: asyncio.AbstractEventLoop):
-        """Set the event loop for async operations."""
-        self._loop = loop
-
     def start_scheduler(self):
         """Start the background scheduler for time-based plug control."""
         if self._scheduler_task is None:
@@ -530,8 +525,8 @@ class SmartPlugManager:
         so unlike :meth:`on_print_complete` this does NOT gate on ``plug.auto_off``.
         It still honours ``enabled`` and skips HA-script entities (which can only
         be triggered, not turned off), and uses each plug's configured off
-        strategy via :meth:`_schedule_off_per_mode`. Replaces the three inline
-        ``wait_for_cooldown(50°C, 600s)`` blocks that ignored plug settings,
+        strategy via :meth:`_schedule_off_per_mode`. Replaces three inline
+        cooldown waits (50°C, 600s) that ignored plug settings,
         fired on the cooldown *timeout* regardless of print state, and could not
         be cancelled by a re-print (#1890).
         """
@@ -902,11 +897,6 @@ class SmartPlugManager:
         if task is not None:
             task.cancel()
 
-    def cancel_all_pending(self):
-        """Cancel all pending turn-off tasks."""
-        for plug_id in list(self._pending_off.keys()):
-            self._cancel_pending_off(plug_id)
-
     async def resume_pending_auto_offs(self):
         """Resume any pending auto-offs that were interrupted by a restart.
 

+ 0 - 72
backend/app/services/spoolman.py

@@ -17,8 +17,6 @@ from backend.app.utils.color_utils import color_match_key, spoolman_color_hex
 
 logger = logging.getLogger(__name__)
 
-BAMBU_RFID_TAG_LENGTH = 32
-
 # How long a Spoolman without native tags is believed before it is asked again,
 # so upgrading Spoolman to 0.27 needs no Bambuddy restart.
 TAG_API_RECHECK_SECONDS = 600
@@ -1093,63 +1091,6 @@ class SpoolmanClient:
                         return spool
         return None
 
-    async def find_spools_by_location_prefix(
-        self, location_prefix: str, cached_spools: list[dict] | None = None
-    ) -> list[dict]:
-        """Return all spools whose location starts with location_prefix."""
-        # Use cached spools if provided, otherwise fetch from API
-        spools = cached_spools if cached_spools is not None else await self.get_spools()
-        matching = []
-        for spool in spools:
-            location = spool.get("location", "")
-            if location and location.startswith(location_prefix):
-                matching.append(spool)
-        return matching
-
-    async def clear_location_for_removed_spools(
-        self,
-        printer_name: str,
-        current_tray_uuids: set[str],
-        cached_spools: list[dict] | None = None,
-        synced_spool_ids: set[int] | None = None,
-    ) -> int:
-        """Clear location for Bambu Lab spools at this printer whose tray_uuid is no longer in the AMS."""
-        location_prefix = f"{printer_name} - "
-        spools_at_printer = await self.find_spools_by_location_prefix(location_prefix, cached_spools=cached_spools)
-        cleared_count = 0
-
-        for spool in spools_at_printer:
-            spool_id = spool.get("id")
-
-            # Skip spools that were just synced (matched by location or tag)
-            if synced_spool_ids and spool_id in synced_spool_ids:
-                continue
-
-            # Get the tray_uuid (stored as "tag" in extra field)
-            extra = spool.get("extra", {}) or {}
-            stored_tag = extra.get("tag", "")
-            if stored_tag:
-                # Normalize: strip quotes and uppercase
-                spool_uuid = stored_tag.strip('"').upper()
-            else:
-                spool_uuid = ""
-
-            # Only clear location for Bambu Lab spools (those with a stored 32-character RFID tag).
-            if len(spool_uuid) != BAMBU_RFID_TAG_LENGTH:
-                continue
-
-            # If this spool's UUID is not in the current AMS, clear its location
-            if spool_uuid not in current_tray_uuids:
-                logger.info(
-                    f"Clearing location for spool {spool_id} "
-                    f"(was: {spool.get('location')}, uuid: {spool_uuid[:16] if spool_uuid else 'none'}...)"
-                )
-                result = await self.update_spool(spool_id=spool_id, clear_location=True)
-                if result:
-                    cleared_count += 1
-
-        return cleared_count
-
     async def ensure_bambu_vendor(self) -> int | None:
         """Return the Bambu Lab vendor ID in Spoolman, creating the vendor if absent."""
         vendors = await self.get_vendors()
@@ -1349,19 +1290,6 @@ class SpoolmanClient:
             tray_weight=int(tray_data.get("tray_weight", 1000)),
         )
 
-    def convert_ams_slot_to_location(self, ams_id: int, tray_id: int) -> str:
-        """Return a human-readable location string (e.g. "AMS A1") for the given AMS slot."""
-        if ams_id >= 254:
-            return "External Spool"
-
-        if 128 <= ams_id <= 135:
-            # AMS-HT units use IDs 128-135
-            ht_letter = chr(ord("A") + (ams_id - 128))
-            return f"AMS-HT {ht_letter}{tray_id + 1}"
-
-        ams_letter = chr(ord("A") + ams_id)
-        return f"AMS {ams_letter}{tray_id + 1}"
-
     def is_bambu_lab_spool(self, tray_uuid: str, tag_uid: str = "", tray_info_idx: str = "") -> bool:
         """Return True if tray_uuid or tag_uid identifies a Bambu Lab spool; tray_info_idx is ignored."""
         # Check tray_uuid (preferred - consistent across printer models)

+ 0 - 3
backend/app/services/telegram_reactions.py

@@ -174,9 +174,6 @@ class TelegramReactionPoller:
                     ", ".join(str(i) for i in sorted(provider_ids)),
                 )
 
-    def is_polling(self, provider_id: int) -> bool:
-        return any(provider_id in ids for token, ids in self._providers.items() if token in self._tasks)
-
     # ------------------------------------------------------------------
     # Plumbing
     # ------------------------------------------------------------------

+ 0 - 82
backend/app/services/virtual_printer/certificate.py

@@ -73,67 +73,6 @@ class CertificateService:
         self.cert_path = cert_dir / "virtual_printer.crt"
         self.key_path = cert_dir / "virtual_printer.key"
 
-    def ensure_certificates(self) -> tuple[Path, Path]:
-        """Ensure certificates exist, generate if needed.
-
-        Returns:
-            Tuple of (cert_path, key_path)
-        """
-        if self.cert_path.exists() and self.key_path.exists():
-            if self._cert_matches_current_ca():
-                logger.debug("Using existing virtual printer certificates")
-                return self.cert_path, self.key_path
-            logger.warning(
-                "Existing per-VP certificate's issuer doesn't match the current CA "
-                "(likely a CA rotation since the cert was signed). Regenerating "
-                "to keep the slicer's imported CA in sync with the served chain."
-            )
-        return self.generate_certificates()
-
-    def _cert_matches_current_ca(self) -> bool:
-        """Check whether the on-disk per-VP cert was signed by the current CA.
-
-        Slicers that import the shared CA validate the per-VP cert against it.
-        If the CA has been rotated since the per-VP cert was signed, the chain
-        is broken even though both files exist on disk. ``ensure_certificates``
-        uses this to decide whether to regenerate.
-
-        Uses real signature verification — every CA generated before the
-        common name carried a per-install suffix is literally
-        "CN=Virtual Printer CA", so on those installs a DN-only compare would
-        incorrectly return True even after rotation.
-        """
-        try:
-            if not self.ca_cert_path.exists():
-                # No CA yet — let generate_certificates create one and the
-                # matching per-VP chain.
-                return False
-            cert_pem = self.cert_path.read_bytes()
-            cert = x509.load_pem_x509_certificate(cert_pem)
-            ca_pem = self.ca_cert_path.read_bytes()
-            ca_cert = x509.load_pem_x509_certificate(ca_pem)
-            from cryptography.exceptions import InvalidSignature
-            from cryptography.hazmat.primitives.asymmetric import padding
-
-            try:
-                ca_cert.public_key().verify(
-                    cert.signature,
-                    cert.tbs_certificate_bytes,
-                    padding.PKCS1v15(),
-                    cert.signature_hash_algorithm,
-                )
-                return True
-            except InvalidSignature:
-                return False
-        except (OSError, ValueError) as e:
-            logger.debug("CA-match probe failed for %s: %s", self.cert_path, e)
-            return False
-        except Exception as e:
-            # Any unexpected exception during verification → treat as mismatch
-            # and regenerate. Safer than reusing a cert we can't validate.
-            logger.debug("CA-match verification failed for %s: %s", self.cert_path, e)
-            return False
-
     def _load_existing_ca(self) -> tuple[rsa.RSAPrivateKey, x509.Certificate] | None:
         """Try to load existing CA certificate and key.
 
@@ -458,24 +397,3 @@ class CertificateService:
             if path.exists():
                 path.unlink()
         logger.info("Deleted printer certificate (CA preserved)")
-
-    def delete_certificates(self, include_ca: bool = False) -> None:
-        """Delete existing certificates.
-
-        Args:
-            include_ca: If True, also delete CA certificate and key.
-                       If False (default), only delete printer certificate.
-        """
-        # Always delete printer certificate
-        for path in [self.cert_path, self.key_path]:
-            if path.exists():
-                path.unlink()
-
-        # Only delete CA if explicitly requested
-        if include_ca:
-            for path in [self.ca_cert_path, self.ca_key_path]:
-                if path.exists():
-                    path.unlink()
-            logger.info("Deleted all certificates including CA")
-        else:
-            logger.info("Deleted printer certificate (CA preserved)")

+ 11 - 25
backend/app/services/virtual_printer/manager.py

@@ -257,7 +257,6 @@ class VirtualPrinterInstance:
         gcode_injection: bool = False,
         bind_ip: str = "",
         remote_interface_ip: str = "",
-        tailscale_disabled: bool = True,
         base_dir: Path,
         session_factory: Callable | None = None,
         printer_manager: "PrinterManager | None" = None,
@@ -281,7 +280,6 @@ class VirtualPrinterInstance:
         self.gcode_injection = gcode_injection
         self.bind_ip = bind_ip
         self.remote_interface_ip = remote_interface_ip
-        self.tailscale_disabled = tailscale_disabled
         self._session_factory = session_factory
         self._printer_manager = printer_manager
 
@@ -302,7 +300,8 @@ class VirtualPrinterInstance:
             shared_ca_dir=shared_ca_dir,
         )
 
-        # Pending files for MQTT correlation
+        # Uploads received and not yet archived or queued; only counted, for
+        # the status's pending_files.
         self._pending_files: dict[str, Path] = {}
 
         # Slicer-side print options captured from the MQTT `project_file`
@@ -762,6 +761,7 @@ class VirtualPrinterInstance:
         """Archive file immediately."""
         if not self._session_factory:
             logger.error("Cannot archive: no database session factory configured")
+            self._pending_files.pop(file_path.name, None)
             return
 
         if file_path.suffix.lower() != ".3mf":
@@ -801,11 +801,10 @@ class VirtualPrinterInstance:
         except Exception as e:
             logger.error("Error archiving file: %s", e)
         finally:
-            # Always release the in-flight marker and delete the temp file —
-            # previously the failure paths only logged and the next upload of
-            # the same name was silently rejected with "already uploading",
-            # the upload_dir filled up indefinitely, and the slicer received
-            # a clean 226 even though no archive existed (#audit-R2-1).
+            # Always drop it from the pending count and delete the temp file —
+            # previously the failure paths only logged, the upload_dir filled
+            # up indefinitely, and the slicer received a clean 226 even though
+            # no archive existed (#audit-R2-1).
             self._pending_files.pop(file_path.name, None)
             if archived:
                 try:
@@ -823,6 +822,7 @@ class VirtualPrinterInstance:
         """Queue file for user review."""
         if not self._session_factory:
             logger.error("Cannot queue: no database session factory configured")
+            self._pending_files.pop(file_path.name, None)
             return
 
         if file_path.suffix.lower() != ".3mf":
@@ -874,15 +874,15 @@ class VirtualPrinterInstance:
             except OSError:
                 pass
         finally:
-            # Always release the in-flight marker so concurrent uploads
-            # with the same filename aren't spuriously rejected after
-            # a queue failure.
+            # Always drop it from the pending count, also after a queue
+            # failure.
             self._pending_files.pop(file_path.name, None)
 
     async def _add_to_print_queue(self, file_path: Path, source_ip: str) -> None:
         """Archive file and add to print queue, assigned to target printer or model."""
         if not self._session_factory:
             logger.error("Cannot add to print queue: no database session factory configured")
+            self._pending_files.pop(file_path.name, None)
             return
 
         if file_path.suffix.lower() != ".3mf":
@@ -1906,7 +1906,6 @@ class VirtualPrinterManager:
                     gcode_injection=vp.gcode_injection,
                     bind_ip=vp.bind_ip or "",
                     remote_interface_ip=vp.remote_interface_ip or "",
-                    tailscale_disabled=vp.tailscale_disabled,
                     base_dir=self._base_dir,
                     session_factory=self._session_factory,
                 )
@@ -1928,7 +1927,6 @@ class VirtualPrinterManager:
                     gcode_injection=vp.gcode_injection,
                     bind_ip=vp.bind_ip or "",
                     remote_interface_ip=vp.remote_interface_ip or "",
-                    tailscale_disabled=vp.tailscale_disabled,
                     base_dir=self._base_dir,
                     session_factory=self._session_factory,
                     printer_manager=self._printer_manager,
@@ -1960,18 +1958,6 @@ class VirtualPrinterManager:
         """Get a running instance by ID."""
         return self._instances.get(vp_id)
 
-    def get_all_status(self) -> list[dict]:
-        """Get status for all running instances."""
-        return [
-            {
-                "id": inst.id,
-                "name": inst.name,
-                "mode": inst.mode,
-                **inst.get_status(),
-            }
-            for inst in self._instances.values()
-        ]
-
     # -- Legacy single-printer compat --
 
     def get_status(self) -> dict:

+ 0 - 5
backend/app/utils/filament_types.py

@@ -67,11 +67,6 @@ def canonical_filament_type(ftype: str | None) -> str:
     return _EQUIV_MAP.get(upper, upper)
 
 
-def filament_types_compatible(a: str | None, b: str | None) -> bool:
-    """Whether two filament types may stand in for one another."""
-    return canonical_filament_type(a) == canonical_filament_type(b)
-
-
 # ---------------------------------------------------------------------------
 # Printer-side material names
 # ---------------------------------------------------------------------------

+ 0 - 21
backend/app/utils/threemf_tools.py

@@ -1004,27 +1004,6 @@ def extract_print_time_from_3mf(file_path: Path, plate_id: int | None = None) ->
     return extract_plate_metadata_from_3mf(file_path, plate_id).print_time_seconds
 
 
-def extract_bed_type_from_3mf(file_path: Path, plate_id: int | None = None) -> str | None:
-    """Extract the build plate type (`curr_bed_type`) for a specific plate (#1281).
-
-    ``archive.bed_type`` is captured at ingest time but is one value per archive
-    (the first plate's `curr_bed_type` — see services/archive.py:235). For a
-    multi-plate 3MF where different plates target different beds (e.g. a 40-plate
-    file mixing PEI + Engineering), the archive-level value lies. When a queue
-    item or print modal targets a specific plate, this re-reads the 3MF and
-    returns that plate's actual bed type.
-
-    Args:
-        file_path: Path to the 3MF file
-        plate_id: Plate index to filter for; if None, returns the first plate's
-            ``curr_bed_type`` (matches the archive-level capture).
-
-    Returns:
-        Bed type string (e.g. "Textured PEI Plate"), or None if not found.
-    """
-    return extract_plate_metadata_from_3mf(file_path, plate_id).bed_type
-
-
 # Bed temperature is not one key in a BambuStudio project. Every plate type has
 # its own per-filament array, and the plate actually fitted is named separately
 # in ``curr_bed_type`` -- so reading a bed temperature means picking the array

+ 0 - 1
backend/tests/conftest.py

@@ -640,7 +640,6 @@ def mock_mqtt_smart_plug_service():
         # Create a mock smart_plug_service
         mock_service = MagicMock()
         mock_service.is_configured = MagicMock(return_value=True)
-        mock_service.has_broker_settings = MagicMock(return_value=True)
         mock_service.configure = AsyncMock(return_value=True)
         mock_service.subscribe = MagicMock()
         mock_service.unsubscribe = MagicMock()

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

@@ -28,7 +28,6 @@ SETTINGS_SECRETS = {
     "ha_token": "ha-tok",
     "prometheus_token": "prom-tok",
     "obico_ml_token": "obico-tok",
-    "virtual_printer_access_code": "12345678",
 }
 
 

+ 0 - 2
backend/tests/integration/test_settings_api_key_scrubbing.py

@@ -36,7 +36,6 @@ async def sensitive_settings(db_session):
         "mqtt_password",
         "ha_token",
         "prometheus_token",
-        "virtual_printer_access_code",
         "ldap_bind_password",
     ]
     for key in _credential_keys:
@@ -66,7 +65,6 @@ class TestSettingsScrubForApiKey:
         assert data["mqtt_password"] == ""
         assert data["ha_token"] == ""
         assert data["prometheus_token"] == ""
-        assert data["virtual_printer_access_code"] == ""
         assert data["ldap_bind_password"] == ""
 
     @pytest.mark.asyncio

+ 0 - 88
backend/tests/integration/test_spoolbuddy.py

@@ -1379,94 +1379,6 @@ class TestUpdateEndpoints:
         assert resp.status_code == 200
         assert resp.json()["status"] == "already_updating"
 
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_updating(self, async_client: AsyncClient, device_factory):
-        await device_factory(device_id="sb-upd-st", pending_command="update", update_status="pending")
-
-        with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
-            mock_ws.broadcast = AsyncMock()
-            resp = await async_client.post(
-                f"{API}/devices/sb-upd-st/update-status",
-                json={"status": "updating", "message": "Fetching latest code..."},
-            )
-
-        assert resp.status_code == 200
-        mock_ws.broadcast.assert_called_once()
-        msg = mock_ws.broadcast.call_args[0][0]
-        assert msg["type"] == "spoolbuddy_update"
-        assert msg["update_status"] == "updating"
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_complete_clears_command(self, async_client: AsyncClient, device_factory):
-        await device_factory(device_id="sb-upd-done", pending_command="update", update_status="updating")
-
-        with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
-            mock_ws.broadcast = AsyncMock()
-            await async_client.post(
-                f"{API}/devices/sb-upd-done/update-status",
-                json={"status": "complete", "message": "Update complete, restarting..."},
-            )
-
-        # Heartbeat should have no pending command
-        with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
-            mock_ws.broadcast = AsyncMock()
-            hb = await async_client.post(
-                f"{API}/devices/sb-upd-done/heartbeat",
-                json={"nfc_ok": True, "scale_ok": True, "uptime_s": 10},
-            )
-
-        assert hb.json()["pending_command"] is None
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_error(self, async_client: AsyncClient, device_factory):
-        await device_factory(device_id="sb-upd-err", pending_command="update", update_status="updating")
-
-        with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
-            mock_ws.broadcast = AsyncMock()
-            resp = await async_client.post(
-                f"{API}/devices/sb-upd-err/update-status",
-                json={"status": "error", "message": "git fetch failed: network unreachable"},
-            )
-
-        assert resp.status_code == 200
-        msg = mock_ws.broadcast.call_args[0][0]
-        assert msg["update_status"] == "error"
-        assert "git fetch failed" in msg["update_message"]
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_unknown_device_404(self, async_client: AsyncClient):
-        resp = await async_client.post(
-            f"{API}/devices/ghost/update-status",
-            json={"status": "updating", "message": "test"},
-        )
-        assert resp.status_code == 404
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_invalid_status_422(self, async_client: AsyncClient, device_factory):
-        """Arbitrary status strings must be rejected with 422 (H2: UpdateStatusRequest validation)."""
-        await device_factory(device_id="sb-upd-inv")
-        resp = await async_client.post(
-            f"{API}/devices/sb-upd-inv/update-status",
-            json={"status": "hacked", "message": "injected"},
-        )
-        assert resp.status_code == 422
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_report_update_status_oversized_message_422(self, async_client: AsyncClient, device_factory):
-        """Message exceeding 255 chars must be rejected with 422 (H2/M4)."""
-        await device_factory(device_id="sb-upd-big")
-        resp = await async_client.post(
-            f"{API}/devices/sb-upd-big/update-status",
-            json={"status": "updating", "message": "x" * 256},
-        )
-        assert resp.status_code == 422
-
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_ssh_public_key_error_does_not_leak_exception_text(self, async_client: AsyncClient):

+ 0 - 2
backend/tests/integration/test_spoolman_api.py

@@ -1049,9 +1049,7 @@ class TestSpoolmanAPI:
             tray_weight=1000,
         )
         mock_spoolman_client.parse_ams_tray.return_value = mock_tray
-        mock_spoolman_client.convert_ams_slot_to_location = MagicMock(return_value="AMS A1")
         mock_spoolman_client.sync_ams_tray = AsyncMock(return_value={"id": 42})
-        mock_spoolman_client.clear_location_for_removed_spools = AsyncMock(return_value=0)
 
         with patch("backend.app.api.routes.spoolman.printer_manager") as pm_mock:
             mock_state = MagicMock()

+ 0 - 2
backend/tests/integration/test_system_support_vp_scope.py

@@ -166,10 +166,8 @@ async def test_the_legacy_virtual_printer_settings_keep_to_the_caller_printers(
     db_session.add(Settings(key="virtual_printer_target_printer_id", value=str(b_id)))
     await db_session.commit()
 
-    shown = await async_client.get("/api/v1/settings/virtual-printer", headers=member)
     changed = await async_client.put("/api/v1/settings/virtual-printer?archive_name_source=filename", headers=member)
 
-    assert shown.json()["target_printer_id"] is None
     assert changed.status_code == 404
 
     # The settings also reach the first virtual printer: with the stored target

+ 12 - 7
backend/tests/integration/test_telegram_reactions.py

@@ -92,6 +92,11 @@ def _track(poller: TelegramReactionPoller, *providers, token: str = BOT_TOKEN):
     poller._providers.setdefault(token, set()).update(p.id for p in providers)
 
 
+def _polling(poller: TelegramReactionPoller, provider_id: int) -> bool:
+    """Whether a running poll serves this provider."""
+    return any(provider_id in ids for token, ids in poller._providers.items() if token in poller._tasks)
+
+
 @pytest.fixture
 def telegram_provider(notification_provider_factory):
     async def _create(**kwargs):
@@ -740,9 +745,9 @@ class TestPoller:
         disabled = await telegram_provider(name="D", telegram_verdict_mode="both", enabled=False)
 
         await poller.sync()
-        assert poller.is_polling(reactions.id)
-        assert not poller.is_polling(buttons.id)
-        assert not poller.is_polling(disabled.id)
+        assert _polling(poller, reactions.id)
+        assert not _polling(poller, buttons.id)
+        assert not _polling(poller, disabled.id)
         assert set(poller._tasks) == {BOT_TOKEN}
 
         # The bot keeps its single poll while any provider wants it; the
@@ -752,8 +757,8 @@ class TestPoller:
         buttons.telegram_verdict_mode = "both"
         await db_session.commit()
         await poller.sync()
-        assert not poller.is_polling(reactions.id)
-        assert poller.is_polling(buttons.id)
+        assert not _polling(poller, reactions.id)
+        assert _polling(poller, buttons.id)
         assert poller._tasks[BOT_TOKEN] is first_task
         assert poller._providers[BOT_TOKEN] == {buttons.id}
 
@@ -764,7 +769,7 @@ class TestPoller:
         await poller.sync()
         assert first_task.cancelled() or first_task.cancelling()
         assert set(poller._tasks) == {"999:newtoken"}
-        assert poller.is_polling(buttons.id)
+        assert _polling(poller, buttons.id)
 
         await poller.aclose()
         assert poller._tasks == {}
@@ -786,7 +791,7 @@ class TestPoller:
 
         await poller.sync()
         assert len(poller._tasks) == 1
-        assert poller.is_polling(first.id) and poller.is_polling(second.id)
+        assert _polling(poller, first.id) and _polling(poller, second.id)
         poller.stop()
         _track(poller, first, second)
 

+ 22 - 4
backend/tests/integration/test_virtual_printer_api.py

@@ -24,10 +24,7 @@ class TestVirtualPrinterSettingsAPI:
 
         assert response.status_code == 200
         result = response.json()
-        assert "enabled" in result
-        assert "access_code_set" in result
-        assert "mode" in result
-        assert "status" in result
+        assert set(result) == {"tailscale_disabled", "archive_name_source", "status"}
 
     @pytest.mark.asyncio
     @pytest.mark.integration
@@ -103,6 +100,27 @@ class TestVirtualPrinterSettingsAPI:
         assert response.status_code == 200
         assert response.json()["archive_name_source"] == "filename"
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_the_tailscale_switch_reaches_the_first_virtual_printer(self, async_client: AsyncClient, db_session):
+        """It only changes what that printer's card shows; nothing is restarted."""
+        from sqlalchemy import select
+
+        from backend.app.models.virtual_printer import VirtualPrinter
+
+        with patch("backend.app.services.virtual_printer.virtual_printer_manager.sync_from_db", AsyncMock()) as sync:
+            created = await async_client.post("/api/v1/virtual-printers", json={"name": "first", "mode": "archive"})
+            assert created.status_code in (200, 201), created.text
+            sync.reset_mock()
+            response = await async_client.put("/api/v1/settings/virtual-printer?tailscale_disabled=false")
+
+        assert response.status_code == 200, response.text
+        assert response.json()["tailscale_disabled"] is False
+        db_session.expire_all()
+        first = (await db_session.execute(select(VirtualPrinter))).scalar_one()
+        assert first.tailscale_disabled is False
+        sync.assert_not_called()
+
 
 class TestPendingUploadsAPI:
     """Integration tests for /api/v1/pending-uploads/ endpoints."""

+ 5 - 127
backend/tests/unit/services/test_bambu_ftp.py

@@ -7,7 +7,7 @@ Tests against a real mock implicit FTPS server, covering:
 - Upload (chunked transfer, progress, error codes)
 - Delete
 - File size
-- Storage info (AVBL, directory scan, diagnose_storage)
+- Storage info (AVBL, directory scan)
 - Model-specific behavior (X1C prot_p, A1 prot_c fallback)
 - Async wrappers
 - Failure injection scenarios (regressions for 0.1.8 bugs)
@@ -489,12 +489,14 @@ class TestUpload:
         errors = [r for r in caplog.records if r.levelno >= logging.ERROR]
         assert bool(errors) is logged_error
 
-    def test_the_read_back_reads_the_size_on_its_own_connection(self, ftp_client_factory, ftp_server):
+    def test_the_read_back_reads_the_size_on_its_own_connection(self, ftp_client_factory, ftp_server, tmp_path):
         import backend.app.services.bambu_ftp as ftp_mod
 
+        local = tmp_path / "r.bin"
+        local.write_bytes(b"x" * 300)
         client = ftp_client_factory()
         client.connect()
-        assert client.upload_bytes(b"x" * 300, "/cache/r.bin") is True
+        assert client.upload_file(local, "/cache/r.bin") is True
 
         assert ftp_mod._size_on_new_connection(client, "/cache/r.bin") == 300
         client.disconnect()
@@ -512,28 +514,6 @@ class TestUpload:
 
         assert not ftp_mod.BambuFTPClient.handshake_blocked("127.0.0.1")
 
-    def test_upload_bytes_without_a_226_is_checked_too(self, ftp_client_factory, ftp_server):
-        import backend.app.services.bambu_ftp as ftp_mod
-
-        client = ftp_client_factory()
-        client.connect()
-
-        def never_answers():
-            raise TimeoutError("timed out")
-
-        client._ftp.voidresp = never_answers
-        checked: list[str] = []
-
-        def fake_size(c, remote_path):
-            checked.append(remote_path)
-            return 3
-
-        with patch.object(ftp_mod, "_size_on_new_connection", fake_size):
-            assert client.upload_bytes(b"abc", "/cache/b.bin") is True
-        client.disconnect()
-
-        assert checked == ["/cache/b.bin"]
-
     def test_upload_426_with_intact_file_proceeds(self, ftp_client_factory, ftp_server, tmp_path):
         """Some P2S firmware revisions return 426 on voidresp() even when the
         file landed fully (TLS data-channel close races the 226). #1417
@@ -676,73 +656,6 @@ class TestUpload:
         assert result is False
         client.disconnect()
 
-    def test_upload_bytes_426_with_intact_file_proceeds(self, ftp_client_factory, ftp_server):
-        """upload_bytes() mirrors the same SIZE-verify logic as upload_file."""
-        import ftplib  # nosec B402 — tests need the real ftplib to construct mock 426 responses
-
-        client = ftp_client_factory()
-        client.connect()
-        data = b"x" * 1024
-
-        def raise_426():
-            raise ftplib.error_temp("426 Failure reading network stream.")
-
-        def fake_size(_path):
-            return 1024  # printer-side file matches expected size
-
-        client._ftp.voidresp = raise_426
-        client._ftp.size = fake_size
-
-        result = client.upload_bytes(data, "/cache/bytes.bin")
-        assert result is True
-        client.disconnect()
-
-    def test_upload_bytes_426_with_truncated_file_returns_false(self, ftp_client_factory, ftp_server):
-        """The truncated branch for upload_bytes()."""
-        import ftplib  # nosec B402 — tests need the real ftplib to construct mock 426 responses
-
-        client = ftp_client_factory()
-        client.connect()
-        data = b"x" * 1024
-
-        def raise_426():
-            raise ftplib.error_temp("426 Failure reading network stream.")
-
-        def fake_size(_path):
-            return 100
-
-        client._ftp.voidresp = raise_426
-        client._ftp.size = fake_size
-
-        result = client.upload_bytes(data, "/cache/bytes.bin")
-        assert result is False
-        client.disconnect()
-
-    def test_upload_bytes_success(self, ftp_client_factory, ftp_server):
-        """upload_bytes() writes data to server."""
-        data = b"Bytes upload content"
-        client = ftp_client_factory()
-        client.connect()
-        result = client.upload_bytes(data, "/cache/bytes.bin")
-        assert result is True
-        client.disconnect()
-        # Verify via fresh connection
-        time.sleep(_UPLOAD_FLUSH_DELAY)
-        client2 = ftp_client_factory()
-        client2.connect()
-        downloaded = client2.download_file("/cache/bytes.bin")
-        assert downloaded == data
-        client2.disconnect()
-
-    def test_upload_bytes_failure(self, ftp_client_factory, ftp_server):
-        """upload_bytes() returns False on STOR failure."""
-        ftp_server.inject_failure("STOR", 553, "No space.")
-        client = ftp_client_factory()
-        client.connect()
-        result = client.upload_bytes(b"data", "/cache/fail.bin")
-        assert result is False
-        client.disconnect()
-
     def test_upload_large_chunked(self, ftp_client_factory, ftp_server, tmp_path):
         """Large file upload in chunks completes without error.
 
@@ -868,25 +781,6 @@ class TestStorageInfo:
         client = BambuFTPClient("127.0.0.1", "12345678")
         assert client.get_storage_info() is None
 
-    def test_diagnose_storage_success(self, ftp_client_factory, ftp_server):
-        """diagnose_storage() returns connected=True with working diagnostics."""
-        client = ftp_client_factory()
-        client.connect()
-        diag = client.diagnose_storage()
-        assert diag["connected"] is True
-        assert diag["can_list_root"] is True
-        assert diag["can_list_cache"] is True
-        assert diag["pwd"] is not None
-        assert diag["storage_info"] is not None
-        client.disconnect()
-
-    def test_diagnose_storage_not_connected(self):
-        """diagnose_storage() reports not connected."""
-        client = BambuFTPClient("127.0.0.1", "12345678")
-        diag = client.diagnose_storage()
-        assert diag["connected"] is False
-        assert "FTP not connected" in diag["errors"]
-
 
 # ---------------------------------------------------------------------------
 # TestModelSpecificBehavior
@@ -955,7 +849,6 @@ class TestModelSpecificBehavior:
     def test_unknown_model_defaults_prot_p(self, ftp_client_factory):
         """Unknown model defaults to prot_p."""
         client = ftp_client_factory(printer_model="FuturePrinter3000")
-        assert client._is_a1_model() is False
         assert client._should_use_prot_c() is False
         assert client.connect() is True
         client.disconnect()
@@ -1427,21 +1320,6 @@ class TestFailureScenarios:
         assert result is False
         client.disconnect()
 
-    def test_diagnose_storage_cwd_failure_doesnt_propagate(self, ftp_client_factory, ftp_server):
-        """diagnose_storage CWD failure doesn't crash the whole operation.
-
-        Regression: diagnose_storage() was called in the upload path and
-        a CWD failure would propagate and crash the upload.
-        """
-        ftp_server.inject_failure("CWD", 550, "No such directory.", count=2)
-        client = ftp_client_factory()
-        client.connect()
-        diag = client.diagnose_storage()
-        # Should still return results (with errors noted)
-        assert diag["connected"] is True
-        assert len(diag["errors"]) > 0
-        client.disconnect()
-
     def test_failure_injection_count_decrements(self, ftp_client_factory, ftp_server):
         """Failure injection with count decrements and eventually succeeds."""
         ftp_server.add_file("cache/retry.bin", b"data after retry")

+ 0 - 33
backend/tests/unit/services/test_layer_timelapse.py

@@ -381,39 +381,6 @@ class TestOnLayerChange:
         await on_layer_change(999, 10)
 
 
-class TestGetActiveSessions:
-    """Tests for get_active_sessions."""
-
-    def test_get_active_sessions_returns_copy(self):
-        """Verify get_active_sessions returns a copy, not the original dict."""
-        from backend.app.services.layer_timelapse import (
-            _active_sessions,
-            cancel_session,
-            get_active_sessions,
-            start_session,
-        )
-
-        _active_sessions.clear()
-
-        with patch("backend.app.services.layer_timelapse.settings") as mock_settings:
-            mock_settings.base_dir = Path("/tmp/test")
-
-            with patch.object(Path, "mkdir"):
-                start_session(1, 100, "http://test/", "mjpeg")
-
-                sessions = get_active_sessions()
-
-                # Should be a copy
-                assert sessions is not _active_sessions
-                assert 1 in sessions
-
-                # Modifying copy shouldn't affect original
-                sessions.clear()
-                assert 1 in _active_sessions
-
-                cancel_session(1)
-
-
 class TestCleanupOrphanedTimelapseSessions:
     """_active_sessions is in-memory only, so a process restart mid-print
     loses track of an active session without ever cleaning up its frames

+ 4 - 5
backend/tests/unit/services/test_notify_widget_client.py

@@ -30,7 +30,6 @@ async def test_widgets_create_explicitly_then_use_the_exact_saved_handle():
         client = NotifyClient(http)
         assert await client.create_widget("ABC12345", "a+b&c", widget["content"]) == widget
         assert await client.update_widget("WG123456", "a+b&c", {"value": "Idle", "progress": None}) == widget
-        assert await client.get_widget("WG123456", "a+b&c") == widget
         assert await client.list_widgets("ABC12345", "a+b&c") == {"widgets": [widget]}
         assert (await client.delete_widget("WG123456", "a+b&c"))["deleted"] is True
 
@@ -38,12 +37,12 @@ async def test_widgets_create_explicitly_then_use_the_exact_saved_handle():
     assert requests[0].url.params["new"] == "1"
     assert requests[0].headers["content-type"] == "application/json"
     assert json.loads(requests[0].content) == widget["content"]
-    assert requests[1].url.path == requests[2].url.path == requests[4].url.path == "/widgets/WG123456"
+    assert requests[1].url.path == requests[3].url.path == "/widgets/WG123456"
     assert json.loads(requests[1].content) == {"value": "Idle", "progress": None}
     for request in requests[1:]:
         assert dict(request.url.params) == {"token": "a+b&c"}
-    assert [r.method for r in requests] == ["POST", "POST", "GET", "GET", "DELETE"]
-    assert not requests[2].content and not requests[3].content and not requests[4].content
+    assert [r.method for r in requests] == ["POST", "POST", "GET", "DELETE"]
+    assert not requests[2].content and not requests[3].content
 
 
 async def test_pasted_credentials_are_normalized_for_activity_and_widget_requests():
@@ -101,7 +100,7 @@ async def test_legacy_la_prefixed_device_can_start_after_identity_verification()
     assert requests[1].url.params["new"] == "1"
 
 
-@pytest.mark.parametrize("method", ["update_widget", "get_widget", "delete_widget"])
+@pytest.mark.parametrize("method", ["update_widget", "delete_widget"])
 @pytest.mark.parametrize("widget_id", ["ABC12345", "WGbad123", "WG12345", "WG1234567", "../WG123456", None])
 async def test_widget_operations_reject_device_dialect_and_invalid_handles(method, widget_id):
     http = AsyncMock(spec=httpx.AsyncClient)

+ 0 - 45
backend/tests/unit/services/test_printer_manager.py

@@ -528,51 +528,6 @@ class TestPrinterManager:
         result = manager.stop_print(999)
         assert result is False
 
-    # ========================================================================
-    # Tests for wait_for_cooldown
-    # ========================================================================
-
-    @pytest.mark.asyncio
-    async def test_wait_for_cooldown_returns_true_when_cool(self, manager, mock_client):
-        """Verify wait_for_cooldown returns True when printer is cool."""
-        mock_client.state.connected = True
-        mock_client.state.temperatures = {"nozzle": 40, "bed": 30}
-        mock_client.check_staleness.return_value = True
-        manager._clients[1] = mock_client
-
-        result = await manager.wait_for_cooldown(1, target_temp=50)
-
-        assert result is True
-
-    @pytest.mark.asyncio
-    async def test_wait_for_cooldown_returns_false_on_disconnect(self, manager, mock_client):
-        """Verify wait_for_cooldown returns False when printer disconnects."""
-        mock_client.state.connected = False
-        mock_client.check_staleness.return_value = False
-        manager._clients[1] = mock_client
-
-        result = await manager.wait_for_cooldown(1, target_temp=50, timeout=1)
-
-        assert result is False
-
-    @pytest.mark.asyncio
-    async def test_wait_for_cooldown_returns_false_for_unknown(self, manager):
-        """Verify wait_for_cooldown returns False for unknown printer."""
-        result = await manager.wait_for_cooldown(999, target_temp=50, timeout=1)
-        assert result is False
-
-    @pytest.mark.asyncio
-    async def test_wait_for_cooldown_checks_both_nozzles(self, manager, mock_client):
-        """Verify wait_for_cooldown checks both nozzles for dual extruders."""
-        mock_client.state.connected = True
-        mock_client.state.temperatures = {"nozzle": 40, "nozzle_2": 45, "bed": 30}
-        mock_client.check_staleness.return_value = True
-        manager._clients[1] = mock_client
-
-        result = await manager.wait_for_cooldown(1, target_temp=50)
-
-        assert result is True
-
     # ========================================================================
     # Tests for is_print_active (#1890)
     # ========================================================================

+ 0 - 23
backend/tests/unit/services/test_slicer_api.py

@@ -571,29 +571,6 @@ class TestSliceOutputValidation:
             )
 
 
-class TestHealth:
-    @pytest.mark.asyncio
-    async def test_health_returns_body(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(
-                status_code=200,
-                json={"status": "healthy", "checks": {"orcaslicer": {"available": True}}},
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        body = await service.health()
-        assert body["status"] == "healthy"
-
-    @pytest.mark.asyncio
-    async def test_health_unreachable_raises(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            raise httpx.ConnectError("no route")
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerApiUnavailableError):
-            await service.health()
-
-
 class TestSliceWithProfilesProgress:
     """Live-progress wiring for slice_with_profiles.
 

+ 0 - 15
backend/tests/unit/services/test_smart_plug_manager.py

@@ -367,21 +367,6 @@ class TestSmartPlugManager:
         with patch.object(manager, "_mark_auto_off_pending", new_callable=AsyncMock):
             manager._cancel_pending_off(999)  # Non-existent plug ID
 
-    @pytest.mark.asyncio
-    async def test_cancel_all_pending(self, manager, mock_plug):
-        """Verify all pending tasks can be cancelled."""
-        mock_task1 = MagicMock()
-        mock_task2 = MagicMock()
-        manager._pending_off[1] = mock_task1
-        manager._pending_off[2] = mock_task2
-
-        with patch("asyncio.create_task"):
-            manager.cancel_all_pending()
-
-        assert len(manager._pending_off) == 0
-        mock_task1.cancel.assert_called_once()
-        mock_task2.cancel.assert_called_once()
-
     # ========================================================================
     # Tests for scheduler
     # ========================================================================

+ 0 - 47
backend/tests/unit/services/test_spoolman_service.py

@@ -478,22 +478,6 @@ class TestSpoolmanClient:
             assert result["id"] == 1
             mock_get.assert_called_once()  # Should call get_spools
 
-    @pytest.mark.asyncio
-    async def test_find_spools_by_location_prefix_with_cached_spools(self, client):
-        """Verify find_spools_by_location_prefix uses cached spools when provided."""
-        cached = [
-            {"id": 1, "location": "Printer1 - AMS A1"},
-            {"id": 2, "location": "Printer2 - AMS A1"},
-            {"id": 3, "location": "Printer1 - AMS A2"},
-        ]
-
-        with patch.object(client, "get_spools", AsyncMock()) as mock_get:
-            result = await client.find_spools_by_location_prefix("Printer1 - ", cached_spools=cached)
-            assert len(result) == 2
-            assert result[0]["id"] == 1
-            assert result[1]["id"] == 3
-            mock_get.assert_not_called()  # Should NOT call get_spools
-
     @pytest.mark.asyncio
     async def test_sync_ams_tray_with_cached_spools(self, client, sample_tray, existing_spool):
         """Verify sync_ams_tray passes cached_spools to find_spool_by_tag."""
@@ -506,37 +490,6 @@ class TestSpoolmanClient:
             await client.sync_ams_tray(sample_tray, "TestPrinter", _NoCatalog(), cached_spools=cached)
             mock_get.assert_not_called()  # Should NOT call get_spools
 
-    @pytest.mark.asyncio
-    async def test_clear_location_for_removed_spools_with_cached_spools(self, client):
-        """Verify clear_location_for_removed_spools uses cached spools."""
-        cached = [
-            {"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"A1B2C3D4E5F60718293A4B5C6D7E8F90"'}},
-            {"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"B1C2D3E4F5061728394A5B6C7D8E9F01"'}},
-            {"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"C1D2E3F40516273849A5B6C7D8E9F012"'}},
-        ]
-        # Tag 3 was cleared, so only tags 1 and 2 are current
-        current_tags = {
-            "A1B2C3D4E5F60718293A4B5C6D7E8F90",
-            "B1C2D3E4F5061728394A5B6C7D8E9F01",
-        }
-
-        with (
-            patch.object(client, "get_spools", AsyncMock()) as mock_get,
-            patch.object(client, "update_spool", AsyncMock(return_value={"id": 3})) as mock_update,
-        ):
-            cleared = await client.clear_location_for_removed_spools("Printer1", current_tags, cached_spools=cached)
-            assert cleared == 1
-            mock_get.assert_not_called()  # Should NOT call get_spools
-            mock_update.assert_called_once()
-            # Verify it cleared TAG3 (not in current_tags)
-            call_kwargs = mock_update.call_args.kwargs
-            assert call_kwargs["spool_id"] == 3
-            assert call_kwargs.get("clear_location") is True
-
-    # ========================================================================
-    # Tests for retry logic in get_spools
-    # ========================================================================
-
     @pytest.mark.asyncio
     async def test_get_spools_succeeds_on_first_attempt(self, client):
         """Verify get_spools succeeds immediately when no errors occur."""

+ 12 - 55
backend/tests/unit/services/test_virtual_printer.py

@@ -152,6 +152,18 @@ class TestVirtualPrinterInstance:
         assert status["running"] is False
         assert status["pending_files"] == 0
 
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("handler", ["_archive_file", "_queue_file", "_add_to_print_queue"])
+    async def test_a_file_that_cannot_be_handled_leaves_the_pending_count(self, instance, handler):
+        """Without a database the file is dropped, and the status stops counting it."""
+        file_path = Path("/tmp/test.3mf")  # nosec B108
+        instance._session_factory = None
+        instance._pending_files = {file_path.name: file_path}
+
+        await getattr(instance, handler)(file_path, "192.168.1.100")
+
+        assert instance.get_status()["pending_files"] == 0
+
     # ========================================================================
     # Tests for file handling
     # ========================================================================
@@ -3181,27 +3193,6 @@ class TestVirtualPrinterManager:
         assert status["model_name"] == "P1P"
         assert status["pending_files"] == 1
 
-    def test_manager_get_all_status(self, manager, tmp_path):
-        """Verify get_all_status returns status for all instances."""
-        from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
-
-        for i in range(1, 3):
-            inst = VirtualPrinterInstance(
-                vp_id=i,
-                name=f"VP{i}",
-                mode="archive",
-                model="C11",
-                access_code="12345678",
-                serial_suffix=f"39180000{i}",
-                base_dir=tmp_path,
-            )
-            manager._instances[i] = inst
-
-        statuses = manager.get_all_status()
-        assert len(statuses) == 2
-        assert statuses[0]["name"] == "VP1"
-        assert statuses[1]["name"] == "VP2"
-
     @pytest.mark.asyncio
     async def test_manager_stop_all(self, manager, tmp_path):
         """Verify stop_all removes all instances."""
@@ -3431,7 +3422,6 @@ class TestVirtualPrinterManager:
             model="C11",
             access_code="12345678",
             serial_suffix="391800001",
-            tailscale_disabled=False,
             base_dir=tmp_path,
         )
         inst.stop_server = AsyncMock()
@@ -3887,39 +3877,6 @@ class TestCertificateService:
         key_content = key_path.read_text()
         assert "BEGIN" in key_content and "KEY" in key_content
 
-    def test_certificates_reused_if_exist(self, cert_service):
-        """Verify existing certificates are reused."""
-        # First generation
-        cert_path1, key_path1 = cert_service.generate_certificates()
-        mtime1 = cert_path1.stat().st_mtime
-
-        # Second call should reuse (via ensure_certificates)
-        cert_path2, key_path2 = cert_service.ensure_certificates()
-        mtime2 = cert_path2.stat().st_mtime
-
-        assert mtime1 == mtime2  # File wasn't regenerated
-
-    def test_delete_certificates(self, cert_service):
-        """Verify certificates can be deleted."""
-        cert_service.generate_certificates()
-
-        assert cert_service.cert_path.exists()
-        assert cert_service.key_path.exists()
-
-        cert_service.delete_certificates()
-
-        assert not cert_service.cert_path.exists()
-        assert not cert_service.key_path.exists()
-
-    def test_ensure_creates_if_not_exist(self, cert_service):
-        """Verify ensure_certificates generates if not existing."""
-        assert not cert_service.cert_path.exists()
-
-        cert_path, key_path = cert_service.ensure_certificates()
-
-        assert cert_path.exists()
-        assert key_path.exists()
-
     # --- CA identity (#3014) ------------------------------------------------
     #
     # A slicer trust store is a flat list of certificates, and OpenSSL resolves

+ 7 - 5
backend/tests/unit/test_filament_type_equivalence.py

@@ -20,11 +20,13 @@ import pytest
 
 from backend.app.services.pipeline_eligibility import _canonical as eligibility_canonical
 from backend.app.services.print_scheduler import canonical_filament_type as scheduler_canonical
-from backend.app.utils.filament_types import (
-    FILAMENT_TYPE_GROUPS,
-    canonical_filament_type,
-    filament_types_compatible,
-)
+from backend.app.utils.filament_types import FILAMENT_TYPE_GROUPS, canonical_filament_type
+
+
+def filament_types_compatible(a: str | None, b: str | None) -> bool:
+    """The comparison the matcher and the pre-flight make."""
+    return canonical_filament_type(a) == canonical_filament_type(b)
+
 
 # Every type either module has ever had an opinion about, plus the shapes the
 # printer actually emits.

+ 1 - 2
backend/tests/unit/test_github_restore.py

@@ -594,11 +594,10 @@ class TestCompanionCredentials:
         rather than judged on availability. See
         ``test_the_whole_ldap_family_is_protected``.
         """
-        await self._restore(db_session, mqtt_enabled="true", virtual_printer_enabled="true")
+        await self._restore(db_session, mqtt_enabled="true")
 
         rows = await self._rows(db_session)
         assert rows["mqtt_enabled"] == "true"
-        assert rows["virtual_printer_enabled"] == "true"
 
     def test_every_exposure_toggle_is_a_companion_toggle(self):
         assert _COMPANION_EXPOSURE_TOGGLES.issubset(_COMPANION_CREDENTIALS)

+ 0 - 38
backend/tests/unit/test_permissions_stats_filter.py

@@ -88,41 +88,3 @@ class TestValidateUserFilterPermission:
         user.has_permission.return_value = False
         with pytest.raises(HTTPException):
             validate(user, -1)
-
-
-class TestApplyUserFilter:
-    """Test the _apply_user_filter helper."""
-
-    @pytest.fixture
-    def apply_filter(self):
-        from backend.app.api.routes.archives import _apply_user_filter
-
-        return _apply_user_filter
-
-    def test_none_does_nothing(self, apply_filter):
-        """When created_by_id is None, conditions list should not change."""
-        conditions = []
-        apply_filter(conditions, None)
-        assert len(conditions) == 0
-
-    def test_positive_id_adds_filter(self, apply_filter):
-        """A positive user ID should add an equality filter."""
-        conditions = []
-        apply_filter(conditions, 5)
-        assert len(conditions) == 1
-        # Check it's a SQLAlchemy comparison expression
-        assert str(conditions[0]) == "print_archives.created_by_id = :created_by_id_1"
-
-    def test_minus_one_adds_is_null(self, apply_filter):
-        """The sentinel value -1 should add an IS NULL filter."""
-        conditions = []
-        apply_filter(conditions, -1)
-        assert len(conditions) == 1
-        assert "IS NULL" in str(conditions[0]).upper()
-
-    def test_appends_to_existing_conditions(self, apply_filter):
-        """Filter should be appended to existing conditions."""
-        conditions = ["existing"]
-        apply_filter(conditions, 5)
-        assert len(conditions) == 2
-        assert conditions[0] == "existing"

+ 0 - 1
backend/tests/unit/test_printer_scope.py

@@ -22,7 +22,6 @@ class TestPrinterScope:
         scope = PrinterScope(frozenset({1, 2}))
         assert scope.allows(1)
         assert not scope.allows(3)
-        assert scope.filter_ids([3, 2, 1]) == [2, 1]
 
     def test_no_printer_is_always_in_scope(self):
         # Rows not bound to a printer (orphaned archives, model-based jobs)

+ 1 - 39
backend/tests/unit/test_spoolbuddy_schema_validation.py

@@ -1,4 +1,4 @@
-"""Unit tests for SpoolBuddy schema validation (security fixes H2, M2, M4).
+"""Unit tests for SpoolBuddy schema validation (security fixes M2, M4).
 
 Tests Pydantic model validation without requiring a running server or DB.
 """
@@ -10,47 +10,9 @@ from backend.app.schemas.spoolbuddy import (
     DeviceRegisterRequest,
     HeartbeatRequest,
     ScaleReadingRequest,
-    UpdateStatusRequest,
     WriteTagResultRequest,
 )
 
-# ---------------------------------------------------------------------------
-# H2 — UpdateStatusRequest: only valid Literal values accepted
-# ---------------------------------------------------------------------------
-
-
-class TestUpdateStatusRequestValidation:
-    def test_valid_status_updating(self):
-        req = UpdateStatusRequest(status="updating")
-        assert req.status == "updating"
-
-    def test_valid_status_complete(self):
-        req = UpdateStatusRequest(status="complete")
-        assert req.status == "complete"
-
-    def test_valid_status_error(self):
-        req = UpdateStatusRequest(status="error")
-        assert req.status == "error"
-
-    def test_invalid_status_rejected(self):
-        """Arbitrary status strings must be rejected (H2: prevents unbounded WS injection)."""
-        with pytest.raises(ValidationError):
-            UpdateStatusRequest(status="hacked")
-
-    def test_empty_status_rejected(self):
-        with pytest.raises(ValidationError):
-            UpdateStatusRequest(status="")
-
-    def test_message_max_length_enforced(self):
-        """message field must not exceed 255 chars."""
-        with pytest.raises(ValidationError):
-            UpdateStatusRequest(status="updating", message="x" * 256)
-
-    def test_message_at_max_length_accepted(self):
-        req = UpdateStatusRequest(status="complete", message="x" * 255)
-        assert len(req.message) == 255
-
-
 # ---------------------------------------------------------------------------
 # M2 — HeartbeatRequest: system_stats size limit (4096 bytes)
 # ---------------------------------------------------------------------------

+ 0 - 186
backend/tests/unit/test_spoolman_clear_location.py

@@ -1,186 +0,0 @@
-"""Unit tests for Spoolman location clearing when spools are removed from AMS.
-
-Tests the clear_location_for_removed_spools method to verify that stale
-Spoolman locations are cleared during both auto-sync and manual sync,
-preventing the "double-booked" slot bug (#921).
-"""
-
-from unittest.mock import AsyncMock, patch
-
-import pytest
-
-from backend.app.services.spoolman import SpoolmanClient
-
-BAMBU_UUID_A = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
-BAMBU_UUID_B = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
-BAMBU_UUID_C = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
-PRINTER_NAME = "My Printer"
-LOCATION_PREFIX = f"{PRINTER_NAME} - "
-
-
-def _make_spool(spool_id: int, location: str, tag: str = "", extra: dict | None = None) -> dict:
-    """Create a mock Spoolman spool dict."""
-    return {
-        "id": spool_id,
-        "location": location,
-        "extra": extra or {"tag": tag},
-    }
-
-
-@pytest.fixture
-def client():
-    """Create a SpoolmanClient without connecting."""
-    return SpoolmanClient("http://localhost:7912")
-
-
-class TestClearLocationForRemovedSpools:
-    """Test the clear_location_for_removed_spools method."""
-
-    @pytest.mark.asyncio
-    async def test_clears_spool_no_longer_in_ams(self, client):
-        """A spool whose UUID is not in current_tray_uuids should have its location cleared."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock, return_value=True) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME, current_tray_uuids=set(), cached_spools=cached_spools
-            )
-
-        assert cleared == 1
-        mock_update.assert_called_once_with(spool_id=1, clear_location=True)
-
-    @pytest.mark.asyncio
-    async def test_keeps_spool_still_in_ams(self, client):
-        """A spool whose UUID is in current_tray_uuids should not be cleared."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME, current_tray_uuids={BAMBU_UUID_A}, cached_spools=cached_spools
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_skips_non_bambu_spools(self, client):
-        """Spools without a 32-char RFID tag should not be cleared (non-Bambu / third-party)."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", "SHORT_TAG"),
-            _make_spool(2, f"{LOCATION_PREFIX}AMS A Slot 2", ""),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME, current_tray_uuids=set(), cached_spools=cached_spools
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_skips_spools_from_other_printers(self, client):
-        """Spools with locations for a different printer should not be touched."""
-        cached_spools = [
-            _make_spool(1, "Other Printer - AMS A Slot 1", BAMBU_UUID_A),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME, current_tray_uuids=set(), cached_spools=cached_spools
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_synced_spool_ids_protects_location_matched_spools(self, client):
-        """Spools in synced_spool_ids should not be cleared even if UUID doesn't match."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME,
-                current_tray_uuids=set(),
-                cached_spools=cached_spools,
-                synced_spool_ids={1},
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_clears_only_removed_spools_in_mixed_set(self, client):
-        """With multiple spools at a printer, only clear the one that was removed."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A),  # Still in AMS
-            _make_spool(2, f"{LOCATION_PREFIX}AMS A Slot 2", BAMBU_UUID_B),  # Removed
-            _make_spool(3, f"{LOCATION_PREFIX}AMS A Slot 3", BAMBU_UUID_C),  # Still in AMS
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock, return_value=True) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME,
-                current_tray_uuids={BAMBU_UUID_A, BAMBU_UUID_C},
-                cached_spools=cached_spools,
-            )
-
-        assert cleared == 1
-        mock_update.assert_called_once_with(spool_id=2, clear_location=True)
-
-    @pytest.mark.asyncio
-    async def test_uuid_comparison_is_case_insensitive(self, client):
-        """UUID matching should work regardless of case."""
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A.lower()),
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME,
-                current_tray_uuids={BAMBU_UUID_A},  # Uppercase
-                cached_spools=cached_spools,
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_returns_zero_when_no_spools_at_printer(self, client):
-        """When no spools have locations for this printer, nothing is cleared."""
-        with patch.object(client, "update_spool", new_callable=AsyncMock) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME, current_tray_uuids=set(), cached_spools=[]
-            )
-
-        assert cleared == 0
-        mock_update.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_double_booking_scenario(self, client):
-        """Reproduce #921: two spools assigned to the same printer location.
-
-        When SpoolA is removed and SpoolB takes its slot, SpoolA's old location
-        should be cleared because its UUID is no longer in current_tray_uuids.
-        """
-        cached_spools = [
-            _make_spool(1, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_A),  # OLD — was removed
-            _make_spool(2, f"{LOCATION_PREFIX}AMS A Slot 1", BAMBU_UUID_B),  # NEW — just inserted
-        ]
-
-        with patch.object(client, "update_spool", new_callable=AsyncMock, return_value=True) as mock_update:
-            cleared = await client.clear_location_for_removed_spools(
-                PRINTER_NAME,
-                current_tray_uuids={BAMBU_UUID_B},  # Only SpoolB is in AMS now
-                cached_spools=cached_spools,
-                synced_spool_ids={2},  # SpoolB was just synced
-            )
-
-        assert cleared == 1
-        mock_update.assert_called_once_with(spool_id=1, clear_location=True)

+ 0 - 40
backend/tests/unit/test_support_helpers.py

@@ -790,46 +790,6 @@ class TestParseObicoEnabledPrinters:
         assert _parse_obico_enabled_printers('{"1": true}') == set()
 
 
-class TestCheckUrlReachable:
-    """Tests for the slicer-API reachability ping."""
-
-    @pytest.mark.asyncio
-    async def test_empty_url_returns_none(self):
-        from backend.app.api.routes.support import _check_url_reachable
-
-        assert await _check_url_reachable("") is None
-        assert await _check_url_reachable("   ") is None
-
-    @pytest.mark.asyncio
-    async def test_successful_response_is_reachable_even_on_404(self):
-        # A 404 means the API is up; we want to separate network failure from
-        # configuration mistakes, so non-empty status counts as reachable.
-        from backend.app.api.routes.support import _check_url_reachable
-
-        with patch("httpx.AsyncClient") as mock_client_cls:
-            mock_client = AsyncMock()
-            mock_client_cls.return_value.__aenter__.return_value = mock_client
-            mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
-            mock_response = MagicMock()
-            mock_response.status_code = 404
-            mock_client.get = AsyncMock(return_value=mock_response)
-
-            result = await _check_url_reachable("http://localhost:3001/api")
-
-        assert result is True
-
-    @pytest.mark.asyncio
-    async def test_connection_error_returns_false(self):
-        from backend.app.api.routes.support import _check_url_reachable
-
-        with patch("httpx.AsyncClient") as mock_client_cls:
-            mock_client_cls.return_value.__aenter__.side_effect = ConnectionError("boom")
-
-            result = await _check_url_reachable("http://nowhere:9999")
-
-        assert result is False
-
-
 class TestFetchSlicerHealth:
     """Tests for the slicer-API health probe that extracts the bundled CLI
     version. Knowing the version in the support bundle lets the reviewer

+ 13 - 13
backend/tests/unit/test_threemf_tools.py

@@ -13,12 +13,12 @@ import pytest
 
 from backend.app.utils.threemf_tools import (
     expand_to_project_slots,
-    extract_bed_type_from_3mf,
     extract_embedded_presets_from_3mf,
     extract_filament_usage_from_3mf,
     extract_layer_filament_usage_from_3mf,
     extract_max_z_height_from_3mf,
     extract_plate_extruder_set_from_3mf,
+    extract_plate_metadata_from_3mf,
     extract_print_time_from_3mf,
     extract_project_filaments_from_3mf,
     extract_support_filament_slots_from_3mf,
@@ -816,7 +816,7 @@ class TestExtractEmbeddedPresetsFrom3mf:
 
 
 class TestExtractBedTypeFrom3mf:
-    """extract_bed_type_from_3mf reads per-plate `curr_bed_type` from
+    """extract_plate_metadata_from_3mf reads per-plate `curr_bed_type` from
     slice_info.config so the queue / print modal can show the right plate
     even on multi-plate 3MFs where different plates target different beds
     (#1281). archive.bed_type is one-value-per-archive (first plate's
@@ -835,7 +835,7 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path) == "Textured PEI Plate"
+        assert extract_plate_metadata_from_3mf(file_path).bed_type == "Textured PEI Plate"
 
     def test_multi_plate_returns_per_plate_value(self, tmp_path):
         # Reporter's case: a 3MF mixing PEI + Engineering across plates.
@@ -860,9 +860,9 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path, plate_id=1) == "Textured PEI Plate"
-        assert extract_bed_type_from_3mf(file_path, plate_id=2) == "Engineering Plate"
-        assert extract_bed_type_from_3mf(file_path, plate_id=3) == "Cool Plate"
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=1).bed_type == "Textured PEI Plate"
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=2).bed_type == "Engineering Plate"
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=3).bed_type == "Cool Plate"
 
     def test_no_plate_id_returns_first_plate(self, tmp_path):
         # The plate_id=None branch must match the archive-level capture
@@ -883,7 +883,7 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path) == "Cool Plate SuperTack"
+        assert extract_plate_metadata_from_3mf(file_path).bed_type == "Cool Plate SuperTack"
 
     def test_unknown_plate_id_returns_none(self, tmp_path):
         xml_content = """<?xml version="1.0" encoding="UTF-8"?>
@@ -897,7 +897,7 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path, plate_id=99) is None
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=99).bed_type is None
 
     def test_plate_without_bed_type_returns_none(self, tmp_path):
         # Older slicers may export a plate without curr_bed_type. The
@@ -917,8 +917,8 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path, plate_id=1) is None
-        assert extract_bed_type_from_3mf(file_path, plate_id=2) == "Engineering Plate"
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=1).bed_type is None
+        assert extract_plate_metadata_from_3mf(file_path, plate_id=2).bed_type == "Engineering Plate"
 
     def test_missing_slice_info_returns_none(self, tmp_path):
         buffer = io.BytesIO()
@@ -929,13 +929,13 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(buffer.read())
 
-        assert extract_bed_type_from_3mf(file_path) is None
+        assert extract_plate_metadata_from_3mf(file_path).bed_type is None
 
     def test_invalid_file_returns_none(self, tmp_path):
         file_path = tmp_path / "invalid.3mf"
         file_path.write_text("not a zip file")
 
-        assert extract_bed_type_from_3mf(file_path) is None
+        assert extract_plate_metadata_from_3mf(file_path).bed_type is None
 
     def test_whitespace_trimmed(self, tmp_path):
         # 3MF values sometimes carry surrounding whitespace from manual
@@ -952,7 +952,7 @@ class TestExtractBedTypeFrom3mf:
         file_path = tmp_path / "test.3mf"
         file_path.write_bytes(create_mock_3mf(xml_content).read())
 
-        assert extract_bed_type_from_3mf(file_path) == "Textured PEI Plate"
+        assert extract_plate_metadata_from_3mf(file_path).bed_type == "Textured PEI Plate"
 
 
 class TestExtractPrintTimeFrom3mf:

+ 0 - 76
backend/tests/unit/test_vp_certificate_rotation.py

@@ -1,76 +0,0 @@
-"""Tests for CertificateService.ensure_certificates' CA-rotation guard.
-
-When the shared CA is regenerated (e.g. its expiry crossed
-``CA_EXPIRY_THRESHOLD_DAYS``), any per-VP printer certificate that was
-signed by the OLD CA becomes orphaned: it still exists on disk and the
-old fallback ``cert_path.exists()`` check would happily reuse it. A
-slicer that imported the NEW CA then fails the TLS handshake because
-the printer cert's issuer doesn't match anything in its trust store.
-
-``_cert_matches_current_ca`` is the guard. It compares the on-disk
-printer cert's issuer against the on-disk CA cert's subject; on
-mismatch ``ensure_certificates`` regenerates the per-VP cert under the
-current CA.
-"""
-
-from backend.app.services.virtual_printer.certificate import CertificateService
-
-
-def test_ensure_certificates_reuses_cert_when_issuer_matches_ca(tmp_path):
-    """Happy path: a freshly-generated CA + per-VP cert pair shares
-    issuer/subject. ``ensure_certificates`` reads them back without
-    regenerating."""
-    svc = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
-
-    # First call: generates the CA + per-VP cert from scratch.
-    first_cert, first_key = svc.ensure_certificates()
-    first_cert_bytes = first_cert.read_bytes()
-
-    # Second call: cert + CA exist and the issuer matches. Should reuse.
-    second_cert, _ = svc.ensure_certificates()
-    assert second_cert.read_bytes() == first_cert_bytes
-
-
-def test_ensure_certificates_regenerates_when_ca_rotated(tmp_path):
-    """CA rotation scenario: the CA file is replaced with a different one
-    (e.g. previous expired and was regenerated). The per-VP cert on disk
-    was signed by the old CA, so its issuer no longer matches the new CA's
-    subject. ``ensure_certificates`` must regenerate the per-VP cert."""
-    # Build the first pair.
-    svc1 = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
-    orig_cert_bytes = svc1.ensure_certificates()[0].read_bytes()
-    orig_ca_bytes = svc1.ca_cert_path.read_bytes()
-
-    # Simulate CA rotation: build a SECOND CA in a different dir, then
-    # swap that CA's files into the original CA path. The per-VP cert
-    # still on disk was signed by the original CA — issuer mismatch now.
-    rotated_dir = tmp_path / "rotated"
-    rotated_dir.mkdir()
-    svc_rotated = CertificateService(cert_dir=rotated_dir, serial="01P00A391800002")
-    svc_rotated.ensure_certificates()
-
-    # Overwrite the original CA on disk with the rotated one.
-    svc1.ca_cert_path.write_bytes(svc_rotated.ca_cert_path.read_bytes())
-    svc1.ca_key_path.write_bytes(svc_rotated.ca_key_path.read_bytes())
-    assert svc1.ca_cert_path.read_bytes() != orig_ca_bytes  # confirm rotation
-
-    # Build a fresh service against the rotated CA, then ensure_certificates
-    # should detect the mismatch and regenerate the per-VP cert.
-    svc2 = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
-    new_cert, _ = svc2.ensure_certificates()
-    new_cert_bytes = new_cert.read_bytes()
-
-    # New per-VP cert must differ from the old (signed by a different CA now).
-    assert new_cert_bytes != orig_cert_bytes
-
-
-def test_cert_matches_current_ca_returns_false_when_no_ca(tmp_path):
-    """If the CA file is missing entirely, the match check must return
-    False so ``ensure_certificates`` falls through to ``generate_certificates``
-    instead of returning a per-VP cert that nothing can validate."""
-    svc = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
-    # Write a per-VP cert without a CA.
-    svc.cert_path.write_bytes(b"fake cert content")
-    svc.key_path.write_bytes(b"fake key content")
-    # No bbl_ca.crt on disk → match fails safely.
-    assert svc._cert_matches_current_ca() is False

+ 1 - 1
backend/tests/unit/test_vp_tls_ciphers.py

@@ -46,7 +46,7 @@ def _cert_pair(tmp_path: Path) -> tuple[Path, Path]:
     CertificateService. Returns ``(cert_path, key_path)`` suitable for
     ``load_cert_chain`` calls inside the VP services under test."""
     svc = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
-    return svc.ensure_certificates()
+    return svc.generate_certificates()
 
 
 def _assert_required_ciphers(ctx: ssl.SSLContext, where: str) -> None:

+ 0 - 3
frontend/src/__tests__/components/AdminDisableTwoFactor.test.tsx

@@ -26,16 +26,13 @@ vi.mock('../../contexts/AuthContext', async (importOriginal) => {
     ...actual,
     useAuth: () => ({
       ...mockAuth,
-      requiresSetup: false,
       loading: false,
       hasPermission: () => true,
       hasAnyPermission: () => true,
-      hasAllPermissions: () => true,
       canModify: () => true,
       login: vi.fn(),
       loginWithToken: vi.fn(),
       logout: vi.fn(),
-      refreshUser: vi.fn(),
       refreshAuth: vi.fn(),
     }),
   };

+ 0 - 88
frontend/src/__tests__/components/BackupModal.test.tsx

@@ -1,88 +0,0 @@
-/**
- * Tests for the BackupModal component.
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { render } from '../utils';
-import { BackupModal } from '../../components/BackupModal';
-import { http, HttpResponse } from 'msw';
-import { server } from '../mocks/server';
-
-describe('BackupModal', () => {
-  const mockOnClose = vi.fn();
-
-  beforeEach(() => {
-    vi.clearAllMocks();
-    server.use(
-      http.post('/api/v1/settings/backup', () => {
-        return new HttpResponse(
-          JSON.stringify({ success: true }),
-          {
-            headers: {
-              'Content-Type': 'application/json',
-            },
-          }
-        );
-      })
-    );
-  });
-
-  describe('rendering', () => {
-    it('renders the modal title', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByText(/backup/i)).toBeInTheDocument();
-    });
-
-    it('shows backup options', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByText(/settings/i)).toBeInTheDocument();
-    });
-
-    it('shows export button', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByRole('button', { name: /export/i })).toBeInTheDocument();
-    });
-
-    it('shows cancel button', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
-    });
-  });
-
-  describe('backup options', () => {
-    it('has checkbox for printers', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByText('Printers')).toBeInTheDocument();
-    });
-
-    it('has checkbox for archives', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByText(/archives/i)).toBeInTheDocument();
-    });
-
-    it('has checkbox for projects', () => {
-      render(<BackupModal onClose={mockOnClose} />);
-
-      expect(screen.getByText('Projects')).toBeInTheDocument();
-    });
-  });
-
-  describe('actions', () => {
-    it('calls onClose when cancel is clicked', async () => {
-      const user = userEvent.setup();
-      render(<BackupModal onClose={mockOnClose} />);
-
-      await user.click(screen.getByRole('button', { name: /cancel/i }));
-
-      expect(mockOnClose).toHaveBeenCalled();
-    });
-  });
-});

+ 0 - 3
frontend/src/__tests__/components/BulkPrinterToolbarWiring.test.tsx

@@ -18,17 +18,14 @@ const permissions = { granted: [] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/components/PrintModalBilling.test.tsx

@@ -17,17 +17,14 @@ const mockShowToast = vi.fn();
 const mockUseAuth = {
   user: { id: 1, username: 'finance-user', permissions: ['cost_centers:read_own', 'printers:control'] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permission === 'cost_centers:read_own' || permission === 'printers:control'),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 89
frontend/src/__tests__/components/RestoreModal.test.tsx

@@ -1,89 +0,0 @@
-/**
- * Tests for the RestoreModal component.
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { render } from '../utils';
-import { RestoreModal } from '../../components/RestoreModal';
-import { http, HttpResponse } from 'msw';
-import { server } from '../mocks/server';
-
-describe('RestoreModal', () => {
-  const mockOnClose = vi.fn();
-  const mockOnRestore = vi.fn();
-  const mockOnSuccess = vi.fn();
-
-  beforeEach(() => {
-    vi.clearAllMocks();
-    server.use(
-      http.post('/api/v1/settings/restore', () => {
-        return HttpResponse.json({ success: true });
-      })
-    );
-  });
-
-  describe('rendering', () => {
-    it('renders the modal title', () => {
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      // Title is "Restore Backup"
-      expect(screen.getByText('Restore Backup')).toBeInTheDocument();
-    });
-
-    it('shows file upload area', () => {
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      expect(screen.getByText(/select.*file/i)).toBeInTheDocument();
-    });
-
-    it('shows cancel button', () => {
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
-    });
-  });
-
-  describe('file input', () => {
-    it('accepts backup files', () => {
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      const fileInput = document.querySelector('input[type="file"]');
-      expect(fileInput).toBeInTheDocument();
-    });
-  });
-
-  describe('actions', () => {
-    it('calls onClose when cancel is clicked', async () => {
-      const user = userEvent.setup();
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      await user.click(screen.getByRole('button', { name: /cancel/i }));
-
-      expect(mockOnClose).toHaveBeenCalled();
-    });
-  });
-
-  describe('overwrite option', () => {
-    it('has overwrite toggle', () => {
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      // The modal has toggle for replacing existing data
-      expect(screen.getByText('Keep existing data')).toBeInTheDocument();
-    });
-
-    it('shows warning when overwrite is enabled', async () => {
-      const user = userEvent.setup();
-      render(<RestoreModal onClose={mockOnClose} onRestore={mockOnRestore} onSuccess={mockOnSuccess} />);
-
-      // Find and click the toggle (uses role="switch")
-      const toggle = screen.getByRole('switch');
-      await user.click(toggle);
-
-      await waitFor(() => {
-        expect(screen.getByText(/Caution/)).toBeInTheDocument();
-      });
-    });
-  });
-});

+ 0 - 3
frontend/src/__tests__/components/SkipObjectsModalWiring.test.tsx

@@ -17,17 +17,14 @@ const permissions = { granted: ['printers:control'] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

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

@@ -23,7 +23,6 @@ vi.mock('../../api/client', () => ({
     updateSpoolmanSettings: vi.fn(),
     getSpoolmanStatus: vi.fn(),
     connectSpoolman: vi.fn(),
-    disconnectSpoolman: vi.fn(),
     syncAllPrintersAms: vi.fn(),
     syncPrinterAms: vi.fn(),
     syncSpoolmanAmsWeights: vi.fn(),
@@ -62,7 +61,6 @@ describe('SpoolmanSettings', () => {
     });
     vi.mocked(api.getPrinters).mockResolvedValue([]);
     vi.mocked(api.connectSpoolman).mockResolvedValue({ success: true, message: 'Connected' });
-    vi.mocked(api.disconnectSpoolman).mockResolvedValue({ success: true, message: 'Disconnected' });
     vi.mocked(api.syncAllPrintersAms).mockResolvedValue({
       success: true,
       synced_count: 3,

+ 0 - 110
frontend/src/__tests__/components/TagDetectedModal.test.tsx

@@ -1,110 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, fireEvent } from '@testing-library/react';
-import { render } from '../utils';
-import { TagDetectedModal } from '../../components/spoolbuddy/TagDetectedModal';
-import type { MatchedSpool } from '../../hooks/useSpoolBuddyState';
-
-const mockUpdateSpoolWeight = vi.fn();
-
-vi.mock('../../api/client', () => ({
-  api: {
-    getSettings: vi.fn().mockResolvedValue({}),
-    getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
-  },
-  spoolbuddyApi: {
-    updateSpoolWeight: (...args: unknown[]) => mockUpdateSpoolWeight(...args),
-  },
-}));
-
-const mockSpool: MatchedSpool = {
-  id: 7,
-  tag_uid: 'AA11BB22CC33DD44',
-  material: 'PETG',
-  subtype: 'HF',
-  color_name: 'Orange',
-  rgba: 'FF6600FF',
-  brand: 'Overture',
-  label_weight: 1000,
-  core_weight: 250,
-  weight_used: 100,
-};
-
-const defaultProps = {
-  isOpen: true,
-  onClose: vi.fn(),
-  spool: mockSpool,
-  tagUid: 'AA11BB22CC33DD44',
-  scaleWeight: 950.0,
-  weightStable: true,
-  onSyncWeight: vi.fn(),
-  onAssignToAms: vi.fn(),
-  onLinkSpool: vi.fn(),
-  onAddToInventory: vi.fn(),
-};
-
-describe('TagDetectedModal', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-    mockUpdateSpoolWeight.mockResolvedValue({ status: 'ok', weight_used: 300 });
-  });
-
-  it('does not render when isOpen=false', () => {
-    render(<TagDetectedModal {...defaultProps} isOpen={false} />);
-    expect(screen.queryByText('Spool Detected')).not.toBeInTheDocument();
-  });
-
-  it('renders known spool view when spool provided', () => {
-    render(<TagDetectedModal {...defaultProps} />);
-
-    expect(screen.getByText('Spool Detected')).toBeInTheDocument();
-    expect(screen.getByText('Orange')).toBeInTheDocument();
-    expect(screen.getByText(/Overture/)).toBeInTheDocument();
-    expect(screen.getByText(/PETG/)).toBeInTheDocument();
-  });
-
-  it('renders unknown tag view when spool is null', () => {
-    render(
-      <TagDetectedModal
-        {...defaultProps}
-        spool={null}
-        tagUid="DEADBEEF11223344"
-      />
-    );
-
-    expect(screen.getByText('New Tag Detected')).toBeInTheDocument();
-    expect(screen.getByText('DEADBEEF11223344')).toBeInTheDocument();
-  });
-
-  it('closes on Escape key', () => {
-    const onClose = vi.fn();
-    render(<TagDetectedModal {...defaultProps} onClose={onClose} />);
-
-    fireEvent.keyDown(document, { key: 'Escape' });
-    expect(onClose).toHaveBeenCalledTimes(1);
-  });
-
-  it('shows weight from scale', () => {
-    // scaleWeight=950g, core=250g → remaining = 950-250 = 700g
-    render(<TagDetectedModal {...defaultProps} scaleWeight={950} />);
-
-    expect(screen.getByText('700g')).toBeInTheDocument();
-  });
-
-  it('shows action buttons (Assign to AMS, Sync Weight)', () => {
-    const onAssign = vi.fn();
-    const onSync = vi.fn();
-    render(
-      <TagDetectedModal
-        {...defaultProps}
-        onAssignToAms={onAssign}
-        onSyncWeight={onSync}
-      />
-    );
-
-    expect(screen.getByText('Assign to AMS')).toBeInTheDocument();
-    expect(screen.getByText('Sync Weight')).toBeInTheDocument();
-
-    fireEvent.click(screen.getByText('Assign to AMS'));
-    expect(onAssign).toHaveBeenCalledTimes(1);
-  });
-});

+ 0 - 680
frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx

@@ -1,680 +0,0 @@
-/**
- * Tests for the VirtualPrinterSettings component.
- *
- * Tests the virtual printer configuration UI including:
- * - Enable/disable toggle
- * - Access code management
- * - Archive mode selection
- * - Status display
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { render } from '../utils';
-import { VirtualPrinterSettings } from '../../components/VirtualPrinterSettings';
-
-// Mock the API client
-vi.mock('../../api/client', () => ({
-  api: {
-    getSettings: vi.fn().mockResolvedValue({}),
-    updateSettings: vi.fn().mockResolvedValue({}),
-    getPrinters: vi.fn().mockResolvedValue([]),
-    getNetworkInterfaces: vi.fn().mockResolvedValue({ interfaces: [] }),
-  },
-  virtualPrinterApi: {
-    getSettings: vi.fn(),
-    updateSettings: vi.fn(),
-    getModels: vi.fn().mockResolvedValue({
-      models: {
-        'BL-P001': 'X1C',
-        'C12': 'P1S',
-        'N7': 'P2S',
-      },
-    }),
-  },
-}));
-
-// Import mocked module
-import { virtualPrinterApi } from '../../api/client';
-
-// Mock data factory
-const createMockSettings = (overrides = {}) => ({
-  enabled: false,
-  access_code_set: false,
-  mode: 'archive' as const,
-  model: 'BL-P001',
-  target_printer_id: null as number | null,
-  remote_interface_ip: null as string | null,
-  status: {
-    enabled: false,
-    running: false,
-    mode: 'archive',
-    name: 'Bambuddy',
-    serial: '00M00A391800001',
-    model: 'BL-P001',
-    model_name: 'X1C',
-    pending_files: 0,
-  },
-  ...overrides,
-});
-
-describe('VirtualPrinterSettings', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-    // Default mock implementation
-    vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(createMockSettings());
-    vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(createMockSettings());
-  });
-
-  describe('rendering', () => {
-    it('renders loading state initially', () => {
-      // Delay the API response to catch loading state
-      vi.mocked(virtualPrinterApi.getSettings).mockImplementation(
-        () => new Promise(() => {}) // Never resolves
-      );
-      render(<VirtualPrinterSettings />);
-
-      // Should show loading spinner
-      expect(document.querySelector('.animate-spin')).toBeInTheDocument();
-    });
-
-    it('renders component title', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Virtual Printer')).toBeInTheDocument();
-      });
-    });
-
-    it('renders enable toggle', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
-      });
-    });
-
-    it('renders access code section', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Access Code')).toBeInTheDocument();
-      });
-    });
-
-    it('renders mode section', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Mode')).toBeInTheDocument();
-      });
-    });
-
-    it('renders how it works info', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('How it works:')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('status indicator', () => {
-    it('shows Stopped when not running', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ status: { ...createMockSettings().status, running: false } })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Stopped')).toBeInTheDocument();
-      });
-    });
-
-    it('shows Running when active', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({
-          enabled: true,
-          status: { ...createMockSettings().status, enabled: true, running: true },
-        })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Running')).toBeInTheDocument();
-      });
-    });
-
-    it('shows status details when running', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({
-          enabled: true,
-          status: {
-            enabled: true,
-            running: true,
-            mode: 'archive',
-            name: 'Bambuddy',
-            serial: '00M00A391800001',
-            model: 'BL-P001',
-            model_name: 'X1C',
-            pending_files: 0,
-          },
-        })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Status Details')).toBeInTheDocument();
-        expect(screen.getByText('Bambuddy')).toBeInTheDocument();
-        expect(screen.getByText('00M00A391800001')).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('access code', () => {
-    it('shows warning when access code not set', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ access_code_set: false })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('No access code set - required to enable')).toBeInTheDocument();
-      });
-    });
-
-    it('shows success when access code is set', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ access_code_set: true })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Access code is set')).toBeInTheDocument();
-      });
-    });
-
-    it('shows character count while typing', async () => {
-      const user = userEvent.setup();
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Access Code')).toBeInTheDocument();
-      });
-
-      const input = screen.getByPlaceholderText('Enter 8-char code');
-      await user.type(input, '1234');
-
-      expect(screen.getByText('(4/8)')).toBeInTheDocument();
-    });
-
-    it('saves access code on button click', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ access_code_set: true })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Access Code')).toBeInTheDocument();
-      });
-
-      const input = screen.getByPlaceholderText('Enter 8-char code');
-      await user.type(input, '12345678');
-
-      const saveButton = screen.getByRole('button', { name: 'Save' });
-      await user.click(saveButton);
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({
-          access_code: '12345678',
-        });
-      });
-    });
-
-    it('toggles password visibility', async () => {
-      const user = userEvent.setup();
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Access Code')).toBeInTheDocument();
-      });
-
-      const input = screen.getByPlaceholderText('Enter 8-char code');
-      expect(input).toHaveAttribute('type', 'password');
-
-      // Find and click the visibility toggle (eye icon button)
-      const toggleButtons = screen.getAllByRole('button');
-      const visibilityToggle = toggleButtons.find(
-        (btn) => btn.querySelector('svg') && btn.className.includes('absolute')
-      );
-
-      if (visibilityToggle) {
-        await user.click(visibilityToggle);
-        expect(input).toHaveAttribute('type', 'text');
-      }
-    });
-  });
-
-  describe('mode selection', () => {
-    it('renders Archive mode option', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Archive')).toBeInTheDocument();
-        expect(screen.getByText('Archive files immediately')).toBeInTheDocument();
-      });
-    });
-
-    it('renders Review mode option', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Review')).toBeInTheDocument();
-        expect(screen.getByText('Review before archiving')).toBeInTheDocument();
-      });
-    });
-
-    it('renders Queue mode option', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Queue')).toBeInTheDocument();
-        expect(screen.getByText('Archive and add to queue')).toBeInTheDocument();
-      });
-    });
-
-    it('highlights current mode (review)', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ mode: 'review' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        const reviewButton = screen.getByText('Review').closest('button');
-        expect(reviewButton?.className).toContain('border-bambu-green');
-      });
-    });
-
-    it('highlights current mode (legacy immediate maps to archive button)', async () => {
-      // Pre-#1429 the wire value was `immediate` but the UI button was
-      // labeled "Archive". Backend migration rewrites stored rows, but a
-      // stale-cached settings payload may still carry the legacy value —
-      // the component normalises it client-side so the right button lights up.
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ mode: 'immediate' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        const archiveButton = screen.getByText('Archive').closest('button');
-        expect(archiveButton?.className).toContain('border-bambu-green');
-      });
-    });
-
-    it('highlights current mode (legacy print_queue maps to queue button)', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ mode: 'print_queue' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        const queueButton = screen.getByText('Queue').closest('button');
-        expect(queueButton?.className).toContain('border-bambu-green');
-      });
-    });
-
-    it('changes mode to review on click', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ mode: 'review' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Review')).toBeInTheDocument();
-      });
-
-      const reviewButton = screen.getByText('Review').closest('button');
-      if (reviewButton) {
-        await user.click(reviewButton);
-      }
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'review' });
-      });
-    });
-
-    it('changes mode to queue on click', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ mode: 'queue' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Queue')).toBeInTheDocument();
-      });
-
-      const queueButton = screen.getByText('Queue').closest('button');
-      if (queueButton) {
-        await user.click(queueButton);
-      }
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'queue' });
-      });
-    });
-  });
-
-  describe('enable/disable toggle', () => {
-    it('cannot enable without access code', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: false, access_code_set: false })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
-      });
-
-      // Find the toggle switch (it's a button with relative class containing the slider)
-      const allButtons = screen.getAllByRole('button');
-      const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
-
-      if (toggle) {
-        await user.click(toggle);
-      }
-
-      // Should not call update API (no access code set)
-      expect(virtualPrinterApi.updateSettings).not.toHaveBeenCalled();
-    });
-
-    it('can enable when access code is set', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: false, access_code_set: true })
-      );
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, access_code_set: true })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
-      });
-
-      // Find the toggle switch (it's a button with rounded-full and w-12 classes)
-      const allButtons = screen.getAllByRole('button');
-      const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
-
-      expect(toggle).toBeDefined();
-      if (toggle) {
-        await user.click(toggle);
-      }
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith(
-          expect.objectContaining({ enabled: true })
-        );
-      });
-    });
-
-    it('can disable when enabled', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, access_code_set: true })
-      );
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ enabled: false, access_code_set: true })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
-      });
-
-      // Find the toggle switch
-      const allButtons = screen.getAllByRole('button');
-      const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
-
-      expect(toggle).toBeDefined();
-      if (toggle) {
-        await user.click(toggle);
-      }
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith(
-          expect.objectContaining({ enabled: false })
-        );
-      });
-    });
-  });
-
-  describe('info section', () => {
-    it('shows setup required warning', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Setup Required')).toBeInTheDocument();
-      });
-    });
-
-    it('shows link to setup guide', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Read the setup guide before enabling')).toBeInTheDocument();
-      });
-    });
-
-    it('shows how it works section', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('How it works:')).toBeInTheDocument();
-        expect(screen.getByText(/virtual printers appear in your slicer/)).toBeInTheDocument();
-      });
-    });
-  });
-
-  describe('proxy mode', () => {
-    it('renders Proxy mode option', async () => {
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Proxy')).toBeInTheDocument();
-        expect(screen.getByText('Relay to real printer')).toBeInTheDocument();
-      });
-    });
-
-    it('highlights proxy mode when selected', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ mode: 'proxy', target_printer_id: 1 })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        const proxyButton = screen.getByText('Proxy').closest('button');
-        expect(proxyButton?.className).toContain('border-blue-500');
-      });
-    });
-
-    it('shows proxy status details when running in proxy mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({
-          enabled: true,
-          mode: 'proxy',
-          target_printer_id: 1,
-          status: {
-            enabled: true,
-            running: true,
-            mode: 'proxy',
-            name: 'Bambuddy (Proxy)',
-            serial: '00M00A391800001',
-            model: 'BL-P001',
-            model_name: 'X1C',
-            pending_files: 0,
-            proxy: {
-              running: true,
-              target_host: '192.168.1.100',
-              ftp_port: 990,  // Privileged port for Bambu Studio compatibility
-              mqtt_port: 8883,
-              ftp_connections: 1,
-              mqtt_connections: 2,
-            },
-          },
-        })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Running')).toBeInTheDocument();
-        expect(screen.getByText('Status Details')).toBeInTheDocument();
-        // IP address appears in multiple places (target printer and status details)
-        expect(screen.getAllByText('192.168.1.100').length).toBeGreaterThan(0);
-      });
-    });
-
-    it('shows target printer dropdown in proxy mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ mode: 'proxy' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Target Printer')).toBeInTheDocument();
-        expect(screen.getByText('Select a printer...')).toBeInTheDocument();
-      });
-    });
-
-    it('changes mode to proxy on click', async () => {
-      const user = userEvent.setup();
-      vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
-        createMockSettings({ mode: 'proxy' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Proxy')).toBeInTheDocument();
-      });
-
-      const proxyButton = screen.getByText('Proxy').closest('button');
-      if (proxyButton) {
-        await user.click(proxyButton);
-      }
-
-      await waitFor(() => {
-        expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'proxy' });
-      });
-    });
-  });
-
-  describe('network interface override', () => {
-    it('shows interface dropdown when enabled in immediate mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'archive' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Network Interface Override')).toBeInTheDocument();
-      });
-    });
-
-    it('shows interface dropdown when enabled in review mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'review' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Network Interface Override')).toBeInTheDocument();
-      });
-    });
-
-    it('shows interface dropdown when enabled in print_queue mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'queue' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Network Interface Override')).toBeInTheDocument();
-      });
-    });
-
-    it('shows interface dropdown when enabled in proxy mode', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'proxy', target_printer_id: 1 })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Network Interface Override')).toBeInTheDocument();
-      });
-    });
-
-    it('hides interface dropdown when disabled', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: false, mode: 'archive' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Mode')).toBeInTheDocument();
-      });
-
-      expect(screen.queryByText('Network Interface Override')).not.toBeInTheDocument();
-    });
-
-    it('shows configured status when interface is set', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'archive', remote_interface_ip: '10.0.0.50' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText('Interface override active')).toBeInTheDocument();
-      });
-    });
-
-    it('shows optional hint when no interface is set', async () => {
-      vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
-        createMockSettings({ enabled: true, mode: 'archive', remote_interface_ip: '' })
-      );
-
-      render(<VirtualPrinterSettings />);
-
-      await waitFor(() => {
-        expect(screen.getByText(/Optional.*auto-detected IP/)).toBeInTheDocument();
-      });
-    });
-  });
-});

+ 0 - 80
frontend/src/__tests__/components/WeightDisplay.test.tsx

@@ -1,80 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, fireEvent, waitFor } from '@testing-library/react';
-import { render } from '../utils';
-import { WeightDisplay } from '../../components/spoolbuddy/WeightDisplay';
-
-const mockTare = vi.fn();
-
-vi.mock('../../api/client', () => ({
-  api: {
-    getSettings: vi.fn().mockResolvedValue({}),
-    getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
-  },
-  spoolbuddyApi: {
-    tare: (...args: unknown[]) => mockTare(...args),
-  },
-}));
-
-const defaultProps = {
-  weight: 823.4,
-  weightStable: true,
-  deviceOnline: true,
-  deviceId: 'sb-0001',
-};
-
-describe('WeightDisplay', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-    mockTare.mockResolvedValue({ status: 'ok' });
-  });
-
-  it('renders weight value with 1 decimal place', () => {
-    render(<WeightDisplay {...defaultProps} weight={823.456} />);
-    expect(screen.getByText('823.5')).toBeInTheDocument();
-  });
-
-  it('shows green dot when stable and online', () => {
-    const { container } = render(
-      <WeightDisplay {...defaultProps} weightStable={true} deviceOnline={true} />
-    );
-    const dot = container.querySelector('.bg-green-500');
-    expect(dot).toBeInTheDocument();
-    expect(screen.getByText('Stable')).toBeInTheDocument();
-  });
-
-  it('shows amber dot when unstable', () => {
-    const { container } = render(
-      <WeightDisplay {...defaultProps} weightStable={false} deviceOnline={true} />
-    );
-    const dot = container.querySelector('.bg-amber-500');
-    expect(dot).toBeInTheDocument();
-    expect(screen.getByText('Measuring...')).toBeInTheDocument();
-  });
-
-  it('shows gray dot when offline', () => {
-    const { container } = render(
-      <WeightDisplay {...defaultProps} deviceOnline={false} />
-    );
-    const dot = container.querySelector('.bg-zinc-600');
-    expect(dot).toBeInTheDocument();
-    expect(screen.getByText('No reading')).toBeInTheDocument();
-  });
-
-  it('tare button calls spoolbuddyApi.tare(deviceId)', async () => {
-    render(<WeightDisplay {...defaultProps} />);
-
-    const tareButton = screen.getByText('Tare');
-    fireEvent.click(tareButton);
-
-    await waitFor(() => {
-      expect(mockTare).toHaveBeenCalledWith('sb-0001');
-    });
-  });
-
-  it('tare button is disabled when no deviceId', () => {
-    render(<WeightDisplay {...defaultProps} deviceId={null} />);
-
-    const tareButton = screen.getByText('Tare');
-    expect(tareButton).toBeDisabled();
-  });
-});

+ 0 - 1
frontend/src/__tests__/components/spool-form/PAProfileSectionNozzle.test.tsx

@@ -35,7 +35,6 @@ describe('PAProfileSection nozzle-specific profiles (#2618)', () => {
       <I18nextProvider i18n={i18n}>
         <PAProfileSection
           formData={{ ...defaultFormData, material: 'PAHT-CF', slicer_filament: 'GFN05' }}
-          updateField={vi.fn()}
           printersWithCalibrations={printers}
           selectedProfiles={new Set()}
           setSelectedProfiles={vi.fn()}

+ 7 - 23
frontend/src/__tests__/components/spoolbuddy/SpoolBuddyColorName3090.test.tsx

@@ -2,7 +2,7 @@
  * The kiosk shows the colour the rest of Bambuddy shows (#3090).
  *
  * Reported by @Sawtaytoes: scan a Bambu RFID spool that Bambuddy's inventory
- * calls "Candy Red" and SpoolBuddy's dialog says "Unknown color" — with the
+ * calls "Candy Red" and SpoolBuddy says "Unknown color" — with the
  * right red swatch beside it. The name was never in the spool record. Bambu's tags frequently carry none, so Bambuddy resolves the swatch's
  * own hex against the colour catalog; the kiosk was printing the empty column.
  *
@@ -14,7 +14,6 @@
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { screen } from '@testing-library/react';
 import { render } from '../../utils';
-import { TagDetectedModal } from '../../../components/spoolbuddy/TagDetectedModal';
 import { SpoolInfoCard } from '../../../components/spoolbuddy/SpoolInfoCard';
 import type { MatchedSpool } from '../../../hooks/useSpoolBuddyState';
 import { setColorCatalog, __resetColorCatalogForTests } from '../../../utils/colors';
@@ -44,17 +43,10 @@ const candyRed: MatchedSpool = {
   weight_used: 0,
 };
 
-const modalProps = {
-  isOpen: true,
-  onClose: vi.fn(),
+const cardProps = {
   spool: candyRed,
-  tagUid: '00000100',
   scaleWeight: 1000,
-  weightStable: true,
   onSyncWeight: vi.fn(),
-  onAssignToAms: vi.fn(),
-  onLinkSpool: vi.fn(),
-  onAddToInventory: vi.fn(),
 };
 
 describe('SpoolBuddy colour names (#3090)', () => {
@@ -64,26 +56,18 @@ describe('SpoolBuddy colour names (#3090)', () => {
   });
 
   it('names the scanned spool from the catalog when the tag carried no name', () => {
-    render(<TagDetectedModal {...modalProps} />);
+    render(<SpoolInfoCard {...cardProps} />);
 
     expect(screen.getByText('Candy Red')).toBeInTheDocument();
     expect(screen.queryByText('Unknown color')).not.toBeInTheDocument();
   });
 
-  it('names it on the dashboard card too, not only in the scan dialog', () => {
-    // Four components render a scanned spool. Fixing the dialog alone leaves
-    // the same spool nameless on the card behind it.
-    render(<SpoolInfoCard spool={candyRed} scaleWeight={1000} weightStable onSyncWeight={vi.fn()} />);
-
-    expect(screen.getByText('Candy Red')).toBeInTheDocument();
-  });
-
   it('prefers the catalog over a name Spoolman never stored', () => {
     // Spoolman has no colour-name field, so the backend sends the subtype with
     // color_name_is_synthesized set. "Silk+" is not a colour.
     render(
-      <TagDetectedModal
-        {...modalProps}
+      <SpoolInfoCard
+        {...cardProps}
         spool={{ ...candyRed, color_name: 'Silk+', color_name_is_synthesized: true }}
       />,
     );
@@ -96,7 +80,7 @@ describe('SpoolBuddy colour names (#3090)', () => {
     // The catalog is the fallback, not an override — a user who typed
     // "My Favourite Red" sees it, whatever the hex says.
     render(
-      <TagDetectedModal {...modalProps} spool={{ ...candyRed, color_name: 'My Favourite Red' }} />,
+      <SpoolInfoCard {...cardProps} spool={{ ...candyRed, color_name: 'My Favourite Red' }} />,
     );
 
     expect(screen.getByText('My Favourite Red')).toBeInTheDocument();
@@ -107,7 +91,7 @@ describe('SpoolBuddy colour names (#3090)', () => {
     // Unknown hex, no stored name: the string has to come from i18n, because
     // the kiosk is used in every language Bambuddy ships.
     render(
-      <TagDetectedModal {...modalProps} spool={{ ...candyRed, rgba: '123456FF' }} />,
+      <SpoolInfoCard {...cardProps} spool={{ ...candyRed, rgba: '123456FF' }} />,
     );
 
     expect(screen.getByText('Unknown color')).toBeInTheDocument();

+ 27 - 45
frontend/src/__tests__/contexts/AuthContext.test.tsx

@@ -147,42 +147,42 @@ describe('AuthContext', () => {
         result.current.hasAnyPermission('printers:read' as Permission, 'settings:update' as Permission)
       ).toBe(true);
     });
+  });
 
-    it('hasAllPermissions returns true for any permissions when auth disabled', async () => {
-      const { result } = renderHook(() => useAuth(), {
-        wrapper: createWrapper(),
-      });
-
-      await waitFor(() => {
-        expect(result.current.authEnabled).toBe(false);
-      });
+  describe('first-run setup', () => {
+    const realLocation = window.location;
 
-      expect(
-        result.current.hasAllPermissions('printers:read' as Permission, 'settings:update' as Permission)
-      ).toBe(true);
-    });
-  });
+    function stubLocation(pathname: string) {
+      const { origin, protocol, host, hostname, port } = realLocation;
+      const location = { origin, protocol, host, hostname, port, pathname, href: origin + pathname, search: '', hash: '' };
+      Object.defineProperty(window, 'location', { configurable: true, value: location });
+      return location;
+    }
 
-  describe('when auth requires setup', () => {
     beforeEach(() => {
       server.use(
-        http.get('/api/v1/auth/status', () => {
-          return HttpResponse.json({
-            auth_enabled: false,
-            requires_setup: true,
-          });
-        })
+        http.get('/api/v1/auth/status', () => HttpResponse.json({ auth_enabled: false, requires_setup: true }))
       );
     });
 
-    it('requiresSetup is true', async () => {
-      const { result } = renderHook(() => useAuth(), {
-        wrapper: createWrapper(),
-      });
+    afterEach(() => {
+      Object.defineProperty(window, 'location', { configurable: true, value: realLocation });
+    });
 
-      await waitFor(() => {
-        expect(result.current.requiresSetup).toBe(true);
-      });
+    it('sends a new install to the setup page', async () => {
+      const location = stubLocation('/');
+
+      renderHook(() => useAuth(), { wrapper: createWrapper() });
+
+      await waitFor(() => expect(location.href).toBe('/setup'));
+    });
+
+    it('leaves a camera page where it is', async () => {
+      const location = stubLocation('/camera/1');
+      const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
+
+      await waitFor(() => expect(result.current.loading).toBe(false));
+      expect(location.href).toBe(`${realLocation.origin}/camera/1`);
     });
   });
 
@@ -303,24 +303,6 @@ describe('AuthContext', () => {
         )
       ).toBe(true);
     });
-
-    it('hasAllPermissions returns true for any combination', async () => {
-      const { result } = renderHook(() => useAuth(), {
-        wrapper: createWrapper(),
-      });
-
-      await waitFor(() => {
-        expect(result.current.loading).toBe(false);
-      });
-
-      expect(
-        result.current.hasAllPermissions(
-          'settings:update' as Permission,
-          'api_keys:create' as Permission,
-          'groups:delete' as Permission
-        )
-      ).toBe(true);
-    });
   });
 
   describe('auth:expired event (#1698)', () => {

+ 0 - 161
frontend/src/__tests__/hooks/useLongPress.test.ts

@@ -1,161 +0,0 @@
-/**
- * Tests for the useLongPress hook.
- */
-
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { renderHook, act } from '@testing-library/react';
-import { useLongPress } from '../../hooks/useLongPress';
-
-describe('useLongPress', () => {
-  beforeEach(() => {
-    vi.useFakeTimers();
-  });
-
-  afterEach(() => {
-    vi.useRealTimers();
-  });
-
-  it('calls onLongPress after delay', () => {
-    const onLongPress = vi.fn();
-    const onClick = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress, onClick, delay: 500 })
-    );
-
-    // Simulate mouse down
-    act(() => {
-      result.current.onMouseDown({} as React.MouseEvent);
-    });
-
-    // Fast forward past the delay
-    act(() => {
-      vi.advanceTimersByTime(600);
-    });
-
-    // Should trigger long press
-    expect(onLongPress).toHaveBeenCalled();
-    expect(onClick).not.toHaveBeenCalled();
-  });
-
-  it('calls onClick for short press', () => {
-    const onLongPress = vi.fn();
-    const onClick = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress, onClick, delay: 500 })
-    );
-
-    // Simulate mouse down
-    act(() => {
-      result.current.onMouseDown({} as React.MouseEvent);
-    });
-
-    // Release before delay
-    act(() => {
-      vi.advanceTimersByTime(200);
-      result.current.onMouseUp({} as React.MouseEvent);
-    });
-
-    // Should trigger click, not long press
-    expect(onClick).toHaveBeenCalled();
-    expect(onLongPress).not.toHaveBeenCalled();
-  });
-
-  it('cancels on mouse leave', () => {
-    const onLongPress = vi.fn();
-    const onClick = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress, onClick, delay: 500 })
-    );
-
-    // Simulate mouse down
-    act(() => {
-      result.current.onMouseDown({} as React.MouseEvent);
-    });
-
-    // Mouse leaves before delay
-    act(() => {
-      vi.advanceTimersByTime(200);
-      result.current.onMouseLeave({} as React.MouseEvent);
-    });
-
-    // Continue past delay
-    act(() => {
-      vi.advanceTimersByTime(400);
-    });
-
-    // Neither should be called
-    expect(onLongPress).not.toHaveBeenCalled();
-    expect(onClick).not.toHaveBeenCalled();
-  });
-
-  it('uses default delay of 500ms', () => {
-    const onLongPress = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress })
-    );
-
-    // Simulate mouse down
-    act(() => {
-      result.current.onMouseDown({} as React.MouseEvent);
-    });
-
-    // Just before default delay
-    act(() => {
-      vi.advanceTimersByTime(450);
-    });
-    expect(onLongPress).not.toHaveBeenCalled();
-
-    // After default delay
-    act(() => {
-      vi.advanceTimersByTime(100);
-    });
-    expect(onLongPress).toHaveBeenCalled();
-  });
-
-  it('handles touch events', () => {
-    const onLongPress = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress, delay: 500 })
-    );
-
-    // Simulate touch start
-    act(() => {
-      result.current.onTouchStart({} as React.TouchEvent);
-    });
-
-    // Fast forward past the delay
-    act(() => {
-      vi.advanceTimersByTime(600);
-    });
-
-    expect(onLongPress).toHaveBeenCalled();
-  });
-
-  it('cancels on touch end', () => {
-    const onLongPress = vi.fn();
-    const onClick = vi.fn();
-
-    const { result } = renderHook(() =>
-      useLongPress({ onLongPress, onClick, delay: 500 })
-    );
-
-    // Simulate touch start
-    act(() => {
-      result.current.onTouchStart({} as React.TouchEvent);
-    });
-
-    // End touch before delay
-    act(() => {
-      vi.advanceTimersByTime(200);
-      result.current.onTouchEnd({} as React.TouchEvent);
-    });
-
-    expect(onClick).toHaveBeenCalled();
-    expect(onLongPress).not.toHaveBeenCalled();
-  });
-});

+ 0 - 20
frontend/src/__tests__/hooks/useSpoolBuddyState.test.ts

@@ -50,26 +50,6 @@ describe('useSpoolBuddyState', () => {
     expect(result.current.deviceId).toBe('dev-1');
   });
 
-  it('WEIGHT_UPDATE handles nested data format', () => {
-    const { result } = renderHook(() => useSpoolBuddyState());
-
-    act(() => {
-      dispatchCustomEvent('spoolbuddy-weight', {
-        data: {
-          weight_grams: 100,
-          stable: false,
-          raw_adc: 9999,
-          device_id: 'dev-2',
-        },
-      });
-    });
-
-    expect(result.current.weight).toBe(100);
-    expect(result.current.weightStable).toBe(false);
-    expect(result.current.rawAdc).toBe(9999);
-    expect(result.current.deviceId).toBe('dev-2');
-  });
-
   it('TAG_MATCHED sets matchedSpool and clears unknownTagUid', () => {
     const { result } = renderHook(() => useSpoolBuddyState());
 

+ 1 - 66
frontend/src/__tests__/hooks/useWebSocket.test.ts

@@ -6,7 +6,7 @@
  */
 
 import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { renderHook, waitFor, act, screen } from '@testing-library/react';
+import { renderHook, act, screen } from '@testing-library/react';
 import React from 'react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { ToastProvider } from '../../contexts/ToastContext';
@@ -250,27 +250,6 @@ describe('useWebSocket hook', () => {
       // GHSA-r2qv: the ws-token mint result is appended as ?token=...
       expect(ws.url).toContain('token=test-ws-token');
     });
-
-    it('reports connected state when WebSocket opens', async () => {
-      const { useWebSocket } = await import('../../hooks/useWebSocket');
-
-      const { result } = renderHook(() => useWebSocket(), {
-        wrapper: createWrapper(queryClient),
-      });
-
-      // Initially not connected
-      expect(result.current.isConnected).toBe(false);
-
-      // Simulate connection opening
-      const ws = await waitForWs();
-      act(() => {
-        ws.open();
-      });
-
-      await waitFor(() => {
-        expect(result.current.isConnected).toBe(true);
-      });
-    });
   });
 
   describe('message handling', () => {
@@ -769,7 +748,6 @@ describe('useWebSocket hook', () => {
             printer_name: 'Printer A',
             slot: 'A1',
             verified: false,
-            saw_tray: true,
           });
         });
       }).not.toThrow();
@@ -1046,49 +1024,6 @@ describe('useWebSocket hook', () => {
     });
   });
 
-  describe('sendMessage', () => {
-    it('sends JSON message when connected', async () => {
-      const { useWebSocket } = await import('../../hooks/useWebSocket');
-
-      const { result } = renderHook(() => useWebSocket(), {
-        wrapper: createWrapper(queryClient),
-      });
-
-      const ws = await waitForWs();
-
-      // Open connection
-      act(() => {
-        ws.open();
-      });
-
-      act(() => {
-        result.current.sendMessage({ type: 'test', data: 'hello' });
-      });
-
-      expect(ws.send).toHaveBeenCalledWith(
-        JSON.stringify({ type: 'test', data: 'hello' })
-      );
-    });
-
-    it('does not send when disconnected', async () => {
-      const { useWebSocket } = await import('../../hooks/useWebSocket');
-
-      const { result } = renderHook(() => useWebSocket(), {
-        wrapper: createWrapper(queryClient),
-      });
-
-      const ws = await waitForWs();
-
-      // Don't open connection - still in CONNECTING state
-
-      act(() => {
-        result.current.sendMessage({ type: 'test' });
-      });
-
-      expect(ws.send).not.toHaveBeenCalled();
-    });
-  });
-
   describe('reconnection', () => {
     it('reconnects after connection closes', async () => {
       vi.useFakeTimers();

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageArchivedConsumed.test.tsx

@@ -52,9 +52,6 @@ const baseSettings = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageCopyButton.test.tsx

@@ -88,9 +88,6 @@ const MOCK_SETTINGS = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageDeepLink.test.tsx

@@ -76,9 +76,6 @@ const MOCK_SETTINGS = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageGroupRestore.test.tsx

@@ -37,9 +37,6 @@ const baseSettings = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageLowStock.test.tsx

@@ -44,9 +44,6 @@ const mockSettings = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPagePrintLabels.test.tsx

@@ -79,9 +79,6 @@ const MOCK_SETTINGS = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx

@@ -43,9 +43,6 @@ const mockSettings = {
   date_format: 'system',
   time_format: 'system',
   default_printer_id: null,
-  virtual_printer_enabled: false,
-  virtual_printer_access_code: '',
-  virtual_printer_mode: 'archive',
   dark_style: 'classic',
   dark_background: 'neutral',
   dark_accent: 'green',

+ 0 - 3
frontend/src/__tests__/pages/PrinterLocationsPage.test.tsx

@@ -20,17 +20,14 @@ const permissions = { granted: ['printers:read', 'printers:update'] as string[]
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

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

@@ -20,17 +20,14 @@ const permissions = { denied: new Set<string>() };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => !permissions.denied.has(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/pages/PrintersPageBulkActionsWiring.test.tsx

@@ -17,17 +17,14 @@ const permissions = { granted: [] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/pages/PrintersPageCameraSplitButton.test.tsx

@@ -20,17 +20,14 @@ const permissions = { granted: ['camera:view', 'settings:update'] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/pages/PrintersPageDeletePowerWiring.test.tsx

@@ -18,17 +18,14 @@ const permissions = { denied: new Set<string>() };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => !permissions.denied.has(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/pages/PrintersPageDropPermission.test.tsx

@@ -20,17 +20,14 @@ const permissions = { granted: ['library:upload', 'queue:create'] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

+ 0 - 3
frontend/src/__tests__/pages/PrintersPageFirmwareModalWiring.test.tsx

@@ -19,17 +19,14 @@ const permissions = { granted: [] as string[] };
 const mockUseAuth = {
   user: { id: 1, username: 'operator', permissions: [] as string[] },
   authEnabled: true,
-  requiresSetup: false,
   loading: false,
   isAdmin: false,
   login: vi.fn(),
   loginWithToken: vi.fn(),
   logout: vi.fn(),
-  refreshUser: vi.fn(),
   refreshAuth: vi.fn(),
   hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
   hasAnyPermission: vi.fn(() => true),
-  hasAllPermissions: vi.fn(() => true),
   canModify: vi.fn(() => true),
 };
 

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio