Bläddra i källkod

remove(slicer): drop bundle import; fix cloud preset type/from for CLI (#1712)

  Bundle import never delivered what it implied: BambuStudio's .bbscfg export
  strips system processes/filaments, so importing a bundle left users without
  process presets and slicing fell back to embedded settings on STL. Bundle
  mode also hid the standard tier behind a constrained dropdown, the actual
  trap reported here.

  Removed end-to-end:
  - backend: POST/GET/DELETE /slicer/bundles*, SliceRequest.bundle,
    SliceBundleSpec, dispatch fork in library.py, bundle-context params on
    the filament-requirements endpoints, bundle-fingerprint cache key in
    slice_preview.py, SlicerApiService.{import,list,get,delete}_bundle and
    slice_with_bundle, BundleSummary / BundleNotFoundError.
  - frontend: BundlePicker + BundleStringDropdown, isBundleMode + every
    branch, bundle state/queries/dispatch in SliceModal.tsx, SlicerBundle /
    SliceBundleSpec types, three bundle API methods. buildCompatibilityIndex
    loses its bundle path; presetCompatibility keeps compatible_printers
    plus the @BBL fallback.
  - SlicerBundlesPanel turns into a permanent static notice explaining the
    removal, alternative import paths, and the new slice-time lookup order
    (Imported > Orca Cloud > Bambu Cloud > Standard sidecar fallback).
  - i18n: slicerBundlesRemoved.{title,description,alternatives,lookupOrder}
    translated across all 11 locales; slice.bundle*, slicerBundles.* keys
    removed.

  Fixed (surfaced by removing bundle mode):
  - _resolve_cloud and _resolve_orca_cloud now force type per slot and pin
    from: "system" on the payload before json.dumps. Bambu Cloud ships
    type as "printer"/"print" and routinely empty `from`; the BS CLI's
    --load-settings parser rejects both with return -5 / "input preset
    file invalid". Standard tier already did this; cloud paths now match.
maziggy 2 månader sedan
förälder
incheckning
1c42a9f1fd
36 ändrade filer med 403 tillägg och 3078 borttagningar
  1. 1 0
      CHANGELOG.md
  2. 0 31
      backend/app/api/routes/archives.py
  3. 47 150
      backend/app/api/routes/library.py
  4. 1 158
      backend/app/api/routes/slicer_presets.py
  5. 0 46
      backend/app/schemas/slicer.py
  6. 26 0
      backend/app/services/preset_resolver.py
  7. 23 88
      backend/app/services/slice_preview.py
  8. 0 240
      backend/app/services/slicer_api.py
  9. 14 258
      backend/tests/integration/test_library_slice_api.py
  10. 128 2
      backend/tests/unit/services/test_preset_resolver.py
  11. 0 182
      backend/tests/unit/services/test_slice_preview.py
  12. 0 273
      backend/tests/unit/services/test_slicer_api.py
  13. 6 74
      backend/tests/unit/test_slice_request_schema.py
  14. 0 211
      backend/tests/unit/test_slicer_presets.py
  15. 3 175
      frontend/src/__tests__/components/SliceModal.test.tsx
  16. 0 214
      frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx
  17. 16 141
      frontend/src/__tests__/utils/slicerPrinterMatch.test.ts
  18. 1 87
      frontend/src/api/client.ts
  19. 36 294
      frontend/src/components/SliceModal.tsx
  20. 26 178
      frontend/src/components/SlicerBundlesPanel.tsx
  21. 5 18
      frontend/src/i18n/locales/de.ts
  22. 5 18
      frontend/src/i18n/locales/en.ts
  23. 5 18
      frontend/src/i18n/locales/es.ts
  24. 5 18
      frontend/src/i18n/locales/fr.ts
  25. 5 18
      frontend/src/i18n/locales/it.ts
  26. 5 18
      frontend/src/i18n/locales/ja.ts
  27. 5 18
      frontend/src/i18n/locales/ko.ts
  28. 5 18
      frontend/src/i18n/locales/pt-BR.ts
  29. 5 18
      frontend/src/i18n/locales/tr.ts
  30. 5 18
      frontend/src/i18n/locales/zh-CN.ts
  31. 5 18
      frontend/src/i18n/locales/zh-TW.ts
  32. 18 76
      frontend/src/utils/slicerPrinterMatch.ts
  33. 0 0
      static/assets/index-45eedLWT.css
  34. 0 0
      static/assets/index-7s3X35pi.css
  35. 0 0
      static/assets/index-VyNhPxaj.js
  36. 2 2
      static/index.html

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
CHANGELOG.md


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

@@ -3498,20 +3498,12 @@ async def _try_preview_slice_filaments(
     plate_id: int,
     file_path: Path,
     request_id: str | None = None,
-    bundle_id: str | None = None,
-    printer_name: str | None = None,
-    process_name: str | None = None,
-    filament_names: list[str] | None = None,
 ) -> list[dict] | None:
     """Run a preview slice via the user's configured sidecar so the filament
     list endpoint can return real per-plate filaments for unsliced project
     files. Returns ``None`` on any failure — the caller falls back to the
     painted-face heuristic. ``request_id`` flows through to the sidecar
     for live progress on the SliceModal's inline spinner + toast.
-
-    Bundle context (id + preset names) is forwarded to the preview helper
-    so the preview can mirror the real-print profile triplet when supplied
-    — see ``slice_preview.get_preview_filaments`` for the full contract.
     """
     from backend.app.api.routes.settings import get_setting
     from backend.app.services.slice_preview import get_preview_filaments
@@ -3540,10 +3532,6 @@ async def _try_preview_slice_filaments(
         file_name=file_path.name,
         api_url=api_url,
         request_id=request_id,
-        bundle_id=bundle_id,
-        printer_name=printer_name,
-        process_name=process_name,
-        filament_names=filament_names,
     )
 
 
@@ -3552,10 +3540,6 @@ async def get_filament_requirements(
     archive_id: int,
     plate_id: int | None = None,
     request_id: str | None = None,
-    bundle_id: str | None = None,
-    printer_name: str | None = None,
-    process_name: str | None = None,
-    filament_names: str | None = None,
     db: AsyncSession = Depends(get_db),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
 ):
@@ -3567,12 +3551,6 @@ async def get_filament_requirements(
     Args:
         archive_id: The archive ID
         plate_id: Optional plate index to filter filaments for (for multi-plate files)
-        bundle_id / printer_name / process_name / filament_names: Optional
-            bundle context. When all four are supplied, the preview slice
-            (run for unsliced project files) uses ``slice_with_bundle``
-            against the named preset triplet instead of the embedded-
-            settings fallback. ``filament_names`` is comma- or semicolon-
-            separated.
     """
     import defusedxml.ElementTree as ET
 
@@ -3676,11 +3654,6 @@ async def get_filament_requirements(
                 project_filaments = extract_project_filaments_from_3mf(zf)
                 used_slot_ids: set[int] = set()
                 if project_filaments and plate_id is not None:
-                    parsed_filament_names: list[str] | None = None
-                    if filament_names:
-                        parsed_filament_names = [
-                            n.strip() for n in filament_names.replace(";", ",").split(",") if n.strip()
-                        ] or None
                     preview = await _try_preview_slice_filaments(
                         db,
                         kind="archive",
@@ -3688,10 +3661,6 @@ async def get_filament_requirements(
                         plate_id=plate_id,
                         file_path=file_path,
                         request_id=request_id,
-                        bundle_id=bundle_id,
-                        printer_name=printer_name,
-                        process_name=process_name,
-                        filament_names=parsed_filament_names,
                     )
                     if preview is not None:
                         used_slot_ids = {f["slot_id"] for f in preview}

+ 47 - 150
backend/app/api/routes/library.py

@@ -2701,10 +2701,6 @@ async def _try_preview_slice_filaments(
     plate_id: int,
     file_path: Path,
     request_id: str | None = None,
-    bundle_id: str | None = None,
-    printer_name: str | None = None,
-    process_name: str | None = None,
-    filament_names: list[str] | None = None,
 ) -> list[dict] | None:
     """Run a preview slice via the user's configured sidecar. Same shape as
     the matching helper in archives.py — see that module for rationale.
@@ -2712,13 +2708,6 @@ async def _try_preview_slice_filaments(
     ``request_id``: when supplied, forwarded to the sidecar so the
     SliceModal's inline spinner + toast can poll the matching progress
     endpoint and show "Generating G-code (45%)" for the preview as well.
-
-    ``bundle_id`` / ``printer_name`` / ``process_name`` / ``filament_names``:
-    when all are supplied, the preview uses ``slice_with_bundle`` against
-    the named bundle's preset triplet so the preview's gram numbers reflect
-    the same profiles the real print will use. Partial context falls back
-    to the embedded-settings path so a half-completed Bundle-tier selection
-    in the modal doesn't error out.
     """
     from backend.app.api.routes.settings import get_setting
     from backend.app.services.slice_preview import get_preview_filaments
@@ -2747,10 +2736,6 @@ async def _try_preview_slice_filaments(
         file_name=file_path.name,
         api_url=api_url,
         request_id=request_id,
-        bundle_id=bundle_id,
-        printer_name=printer_name,
-        process_name=process_name,
-        filament_names=filament_names,
     )
 
 
@@ -2759,10 +2744,6 @@ async def get_library_file_filament_requirements(
     file_id: int,
     plate_id: int | None = None,
     request_id: str | None = None,
-    bundle_id: str | None = None,
-    printer_name: str | None = None,
-    process_name: str | None = None,
-    filament_names: str | None = None,
     db: AsyncSession = Depends(get_db),
     _: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
 ):
@@ -2774,12 +2755,6 @@ async def get_library_file_filament_requirements(
     Args:
         file_id: The library file ID
         plate_id: Optional plate index to get filaments for a specific plate
-        bundle_id / printer_name / process_name / filament_names: Optional
-            bundle context. When all four are supplied, the preview slice
-            (run for unsliced project files) uses ``slice_with_bundle``
-            against the named preset triplet instead of the embedded-
-            settings fallback. ``filament_names`` is comma- or semicolon-
-            separated to mirror the slice route's multi-color form.
     """
     import defusedxml.ElementTree as ET
 
@@ -2899,14 +2874,6 @@ async def get_library_file_filament_requirements(
                 project_filaments = extract_project_filaments_from_3mf(zf)
                 used_slot_ids: set[int] = set()
                 if project_filaments and plate_id is not None:
-                    # Bundle context flows through optional query params so
-                    # callers without a Bundle-tier selection (the common
-                    # case) hit the same path as before.
-                    parsed_filament_names: list[str] | None = None
-                    if filament_names:
-                        parsed_filament_names = [
-                            n.strip() for n in filament_names.replace(";", ",").split(",") if n.strip()
-                        ] or None
                     preview = await _try_preview_slice_filaments(
                         db,
                         kind="library_file",
@@ -2914,10 +2881,6 @@ async def get_library_file_filament_requirements(
                         plate_id=plate_id,
                         file_path=file_path,
                         request_id=request_id,
-                        bundle_id=bundle_id,
-                        printer_name=printer_name,
-                        process_name=process_name,
-                        filament_names=parsed_filament_names,
                     )
                     if preview is not None:
                         used_slot_ids = {f["slot_id"] for f in preview}
@@ -3171,48 +3134,38 @@ async def _run_slicer_with_fallback(
         SlicerInputError,
     )
 
-    # Bundle dispatch path: when SliceRequest.bundle is set, the schema
-    # validator short-circuited the presets-required check, so the
-    # PresetRef fields may all be None. Skip resolve_preset_ref entirely
-    # — the sidecar will materialise the per-category JSONs from the
-    # bundle's extracted directory at slice time.
-    use_bundle = request.bundle is not None
-
     user: User | None = None
     presets: dict[str, str] = {}
     filament_jsons: list[str] = []
-    if not use_bundle:
-        # Resolve each slot via the source-aware resolver. The schema
-        # validator has already normalised legacy `*_preset_id: int`
-        # fields into `PresetRef(source='local', id=str(int))`, so all
-        # three are guaranteed non-None here.
-        if current_user_id is not None:
-            user = await db.get(User, current_user_id)
-
-        refs = {
-            "printer": request.printer_preset,
-            "process": request.process_preset,
-        }
-        for slot, ref in refs.items():
-            assert ref is not None, "schema validator guarantees PresetRef is set"
-            presets[slot] = await resolve_preset_ref(db, user, ref, slot)
-        # Multi-color: resolve each filament slot in plate order. The schema
-        # validator backfilled `filament_presets` from the legacy `filament_preset`
-        # field for single-color callers, so this list is always non-empty.
-        for ref in request.filament_presets:
-            assert ref is not None, "schema validator guarantees filament list is non-None"
-            filament_jsons.append(await resolve_preset_ref(db, user, ref, "filament"))
-
-        # Bed-type override (#1337): patch curr_bed_type onto the resolved
-        # process JSON so the slicer's StaticPrintConfig pass picks up the
-        # user's pick instead of whatever the process preset defaults to.
-        # Without this, slicing an STL of ABS onto a process preset whose
-        # default is "Cool Plate" fails with "Plate 1: Cool Plate does not
-        # support filament 1" — the reporter's exact scenario. Only applies
-        # to the resolved-preset path; bundle mode would need a sidecar-side
-        # mechanism to patch presets it materialises from disk.
-        if request.bed_type:
-            presets["process"] = _patch_process_bed_type(presets["process"], request.bed_type)
+    # Resolve each slot via the source-aware resolver. The schema
+    # validator has already normalised legacy `*_preset_id: int`
+    # fields into `PresetRef(source='local', id=str(int))`, so all
+    # three are guaranteed non-None here.
+    if current_user_id is not None:
+        user = await db.get(User, current_user_id)
+
+    refs = {
+        "printer": request.printer_preset,
+        "process": request.process_preset,
+    }
+    for slot, ref in refs.items():
+        assert ref is not None, "schema validator guarantees PresetRef is set"
+        presets[slot] = await resolve_preset_ref(db, user, ref, slot)
+    # Multi-color: resolve each filament slot in plate order. The schema
+    # validator backfilled `filament_presets` from the legacy `filament_preset`
+    # field for single-color callers, so this list is always non-empty.
+    for ref in request.filament_presets:
+        assert ref is not None, "schema validator guarantees filament list is non-None"
+        filament_jsons.append(await resolve_preset_ref(db, user, ref, "filament"))
+
+    # Bed-type override (#1337): patch curr_bed_type onto the resolved
+    # process JSON so the slicer's StaticPrintConfig pass picks up the
+    # user's pick instead of whatever the process preset defaults to.
+    # Without this, slicing an STL of ABS onto a process preset whose
+    # default is "Cool Plate" fails with "Plate 1: Cool Plate does not
+    # support filament 1" — the reporter's exact scenario.
+    if request.bed_type:
+        presets["process"] = _patch_process_bed_type(presets["process"], request.bed_type)
 
     # Slicer routing — pick the sidecar URL by preferred_slicer.
     # The per-install URL setting (Settings UI → Slicer card) wins; an
@@ -3286,11 +3239,10 @@ async def _run_slicer_with_fallback(
         target_model = await _resolve_target_printer_model(db, user, request)
         if source_model and target_model and is_dual_nozzle_model(source_model) != is_dual_nozzle_model(target_model):
             logger.info(
-                "Cross-nozzle-class re-slice (%s -> %s, %s): enabling --arrange so BS reconciles "
+                "Cross-nozzle-class re-slice (%s -> %s): enabling --arrange so BS reconciles "
                 "the embedded project layout against the target printer",
                 source_model,
                 target_model,
-                "bundle" if use_bundle else "presets",
             )
             cross_class_arrange = True
     # When this slice is dispatcher-tracked, generate a request_id so
@@ -3319,17 +3271,10 @@ async def _run_slicer_with_fallback(
     # never touches the unused slot. Replace unused-slot entries with the
     # slot-1 selection before the real slice so the loaded-filament set
     # is materially homogeneous.
-    bundle_filament_names: list[str] | None = None
     if is_3mf and request.plate is not None:
         from backend.app.services.slicer_3mf_convert import substitute_unused_plate_filaments
 
-        if use_bundle:
-            assert request.bundle is not None
-            bundle_filament_names = substitute_unused_plate_filaments(
-                primary_bytes, request.plate, list(request.bundle.filament_names)
-            )
-        else:
-            filament_jsons = substitute_unused_plate_filaments(primary_bytes, request.plate, filament_jsons)
+        filament_jsons = substitute_unused_plate_filaments(primary_bytes, request.plate, filament_jsons)
 
     # Cross-class slice-all loop (#1493): when the user asks for
     # ``plate=0`` (all plates) AND the source's nozzle class differs from
@@ -3392,39 +3337,18 @@ async def _run_slicer_with_fallback(
 
                 for plate_num in range(1, plate_count + 1):
                     plate_cb = _wrap_progress_for_plate(plate_num, plate_count)
-                    if use_bundle:
-                        assert request.bundle is not None
-                        per_plate = await service.slice_with_bundle(
-                            model_bytes=primary_bytes,
-                            model_filename=model_filename,
-                            bundle_id=request.bundle.bundle_id,
-                            printer_name=request.bundle.printer_name,
-                            process_name=request.bundle.process_name,
-                            filament_names=(
-                                bundle_filament_names
-                                if bundle_filament_names is not None
-                                else request.bundle.filament_names
-                            ),
-                            plate=plate_num,
-                            export_3mf=True,
-                            arrange=True,
-                            bed_type=request.bed_type,
-                            request_id=progress_request_id,
-                            on_progress=plate_cb,
-                        )
-                    else:
-                        per_plate = await service.slice_with_profiles(
-                            model_bytes=primary_bytes,
-                            model_filename=model_filename,
-                            printer_profile_json=presets["printer"],
-                            process_profile_json=presets["process"],
-                            filament_profile_jsons=filament_jsons,
-                            plate=plate_num,
-                            export_3mf=True,
-                            arrange=True,
-                            request_id=progress_request_id,
-                            on_progress=plate_cb,
-                        )
+                    per_plate = await service.slice_with_profiles(
+                        model_bytes=primary_bytes,
+                        model_filename=model_filename,
+                        printer_profile_json=presets["printer"],
+                        process_profile_json=presets["process"],
+                        filament_profile_jsons=filament_jsons,
+                        plate=plate_num,
+                        export_3mf=True,
+                        arrange=True,
+                        request_id=progress_request_id,
+                        on_progress=plate_cb,
+                    )
                     per_plate_results.append((plate_num, per_plate))
 
                 # Merge the N single-plate 3MFs into one multi-plate 3MF.
@@ -3445,27 +3369,6 @@ async def _run_slicer_with_fallback(
                     filament_used_g=sum(r.filament_used_g for _, r in per_plate_results),
                     filament_used_mm=sum(r.filament_used_mm for _, r in per_plate_results),
                 )
-            elif use_bundle:
-                # Bundle dispatch: sidecar materialises the JSON triplet
-                # from the stored .bbscfg by name. ``request.bundle`` is
-                # guaranteed non-None here by the use_bundle branch above.
-                assert request.bundle is not None
-                result = await service.slice_with_bundle(
-                    model_bytes=primary_bytes,
-                    model_filename=model_filename,
-                    bundle_id=request.bundle.bundle_id,
-                    printer_name=request.bundle.printer_name,
-                    process_name=request.bundle.process_name,
-                    filament_names=bundle_filament_names
-                    if bundle_filament_names is not None
-                    else request.bundle.filament_names,
-                    plate=request.plate,
-                    export_3mf=request.export_3mf,
-                    arrange=cross_class_arrange,
-                    bed_type=request.bed_type,
-                    request_id=progress_request_id,
-                    on_progress=progress_callback,
-                )
             else:
                 result = await service.slice_with_profiles(
                     model_bytes=primary_bytes,
@@ -3502,11 +3405,8 @@ async def _run_slicer_with_fallback(
             # bytes — the embedded-settings path also reads the same
             # project_settings.config and the same range validator runs
             # there too, so without sanitisation the fallback would die
-            # on the same sentinel error (#1201). Same fallback applies
-            # to the bundle path: if the resolved triplet crashes the CLI,
-            # embedded settings give the user *something* rather than a
-            # hard failure (the SliceModal flags the difference via
-            # used_embedded_settings).
+            # on the same sentinel error (#1201). The SliceModal flags
+            # the difference to the user via used_embedded_settings.
             result = await service.slice_without_profiles(
                 model_bytes=primary_bytes,
                 model_filename=model_filename,
@@ -3555,8 +3455,6 @@ async def _resolve_target_printer_model(db: AsyncSession, user: User | None, req
     """
     from backend.app.services.preset_resolver import resolve_preset_ref
 
-    if request.bundle is not None:
-        return _canonical_printer_model(request.bundle.printer_name)
     if request.printer_preset is None:
         return None
     try:
@@ -3578,11 +3476,10 @@ async def guard_nozzle_class_reslice(
 
     Cross-nozzle-class re-slicing is handled by ``_run_slicer_with_fallback``'s
     two-pass conversion (#1493): a 1mm cube is sliced with the target triplet
-    (via either ``slice_with_profiles`` or ``slice_with_bundle``, whichever
-    dispatch mode the caller is using) to produce a fresh target-shaped
+    via ``slice_with_profiles`` to produce a fresh target-shaped
     ``Metadata/project_settings.config``, which is then spliced into the
     source 3MF before the real slice. So this guard never needs to block
-    anymore — both preset and bundle paths are covered.
+    anymore.
 
     The function and its call sites in ``archives.py`` / the library re-slice
     route are kept so external pinned-version forks and downstream patches

+ 1 - 158
backend/app/api/routes/slicer_presets.py

@@ -16,7 +16,7 @@ import json
 import logging
 import time
 
-from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
+from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -47,12 +47,8 @@ from backend.app.services.orca_cloud import (
     OrcaCloudError,
 )
 from backend.app.services.slicer_api import (
-    BundleNotFoundError,
-    BundleSummary,
     SlicerApiError,
     SlicerApiService,
-    SlicerApiUnavailableError,
-    SlicerInputError,
 )
 from backend.app.utils.printer_models import PRINTER_MODEL_MAP
 
@@ -535,159 +531,6 @@ async def list_unified_presets(
     )
 
 
-def _bundle_summary_to_dict(b: BundleSummary) -> dict:
-    """Serialize a BundleSummary for the JSON response. The frontend uses
-    these arrays to populate the preset dropdowns when a user picks the
-    bundle as the slice source.
-    """
-    return {
-        "id": b.id,
-        "printer_preset_name": b.printer_preset_name,
-        "printer": b.printer,
-        "process": b.process,
-        "filament": b.filament,
-        "version": b.version,
-    }
-
-
-@router.post("/bundles", status_code=201)
-async def import_slicer_bundle(
-    file: UploadFile = File(...),
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.LIBRARY_UPLOAD),
-):
-    """Forward a BambuStudio Printer Preset Bundle (.bbscfg) to the sidecar.
-
-    The user exports their printer's preset bundle from BambuStudio (File
-    -> Export -> Export Preset Bundle, "Printer preset bundle" option).
-    Uploading it here unpacks the bundle on the sidecar and exposes its
-    inner printer / process / filament presets to subsequent slice
-    requests via the bundle-id selector.
-
-    Idempotent: re-uploading the same file yields the same id (sidecar
-    hashes the zip content), so duplicate uploads collapse rather than
-    accumulate.
-    """
-    api_url = await _resolve_slicer_api_url(db)
-    if not api_url:
-        raise HTTPException(status_code=503, detail="No slicer sidecar configured")
-
-    # Multer on the sidecar caps bundle uploads at 50MB. We don't enforce
-    # that here — let the sidecar's filter own the limit so it stays in
-    # one place — but we do reject empty / huge files at the FastAPI
-    # layer to avoid pointlessly streaming them to the sidecar first.
-    contents = await file.read()
-    if not contents:
-        raise HTTPException(status_code=400, detail="Bundle file is empty")
-    filename = file.filename or "bundle.bbscfg"
-
-    try:
-        async with SlicerApiService(base_url=api_url) as svc:
-            summary = await svc.import_bundle(contents, filename=filename)
-    except SlicerInputError as e:
-        # Sidecar's 4xx — most likely a non-.bbscfg upload, a corrupt zip,
-        # or a path-traversal entry that the manifest validator caught.
-        # Log the detail so it lands in the support bundle: the FE-only
-        # toast was leaving us blind during triage (#1312).
-        logger.warning(
-            "Bundle import rejected by sidecar (%s, %d bytes): %s",
-            filename,
-            len(contents),
-            e,
-        )
-        raise HTTPException(status_code=400, detail=str(e)) from e
-    except SlicerApiUnavailableError as e:
-        logger.warning("Bundle import: sidecar unreachable (%s): %s", api_url, e)
-        raise HTTPException(status_code=503, detail=str(e)) from e
-    except SlicerApiError as e:
-        logger.warning(
-            "Bundle import: sidecar server error (%s, %d bytes): %s",
-            filename,
-            len(contents),
-            e,
-        )
-        # 5xx from the sidecar's import path is rare — usually a disk
-        # write failure inside DATA_PATH/bundles. 502 (bad gateway) is
-        # closer to the truth than 500 here, since we're proxying.
-        raise HTTPException(status_code=502, detail=str(e)) from e
-    return _bundle_summary_to_dict(summary)
-
-
-@router.get("/bundles")
-async def list_slicer_bundles(
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.LIBRARY_UPLOAD),
-):
-    """List every Printer Preset Bundle currently stored on the sidecar.
-
-    Drives the SliceModal's "Bundle" tier and a Settings panel where
-    users can review / delete imported bundles. Returns ``[]`` when the
-    sidecar has no bundles imported yet.
-    """
-    api_url = await _resolve_slicer_api_url(db)
-    if not api_url:
-        # No sidecar configured: empty list rather than 503 so the modal
-        # renders cleanly. Same shape as the bundled-presets fallback.
-        return []
-    try:
-        async with SlicerApiService(base_url=api_url) as svc:
-            bundles = await svc.list_bundles()
-    except SlicerApiUnavailableError as e:
-        # Sidecar offline: surface as 503 so the frontend can show a
-        # banner. Differs from the bundled-tier behaviour because that
-        # path also has cloud + local fallbacks; bundles is the only
-        # source for its tier.
-        raise HTTPException(status_code=503, detail=str(e)) from e
-    except SlicerApiError as e:
-        raise HTTPException(status_code=502, detail=str(e)) from e
-    return [_bundle_summary_to_dict(b) for b in bundles]
-
-
-@router.get("/bundles/{bundle_id}")
-async def get_slicer_bundle(
-    bundle_id: str,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.LIBRARY_UPLOAD),
-):
-    """Return one bundle by id. 404 if it doesn't exist on the sidecar."""
-    api_url = await _resolve_slicer_api_url(db)
-    if not api_url:
-        raise HTTPException(status_code=503, detail="No slicer sidecar configured")
-    try:
-        async with SlicerApiService(base_url=api_url) as svc:
-            summary = await svc.get_bundle(bundle_id)
-    except BundleNotFoundError as e:
-        raise HTTPException(status_code=404, detail=str(e)) from e
-    except SlicerApiUnavailableError as e:
-        raise HTTPException(status_code=503, detail=str(e)) from e
-    except SlicerApiError as e:
-        raise HTTPException(status_code=502, detail=str(e)) from e
-    return _bundle_summary_to_dict(summary)
-
-
-@router.delete("/bundles/{bundle_id}", status_code=204)
-async def delete_slicer_bundle(
-    bundle_id: str,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.LIBRARY_UPLOAD),
-):
-    """Remove a stored bundle from the sidecar. Future slice requests
-    referencing this id will fail with 404 from the sidecar.
-    """
-    api_url = await _resolve_slicer_api_url(db)
-    if not api_url:
-        raise HTTPException(status_code=503, detail="No slicer sidecar configured")
-    try:
-        async with SlicerApiService(base_url=api_url) as svc:
-            await svc.delete_bundle(bundle_id)
-    except BundleNotFoundError as e:
-        raise HTTPException(status_code=404, detail=str(e)) from e
-    except SlicerApiUnavailableError as e:
-        raise HTTPException(status_code=503, detail=str(e)) from e
-    except SlicerApiError as e:
-        raise HTTPException(status_code=502, detail=str(e)) from e
-
-
 @router.get("/preview-progress/{request_id}")
 async def get_preview_slice_progress(
     request_id: str,

+ 0 - 46
backend/app/schemas/slicer.py

@@ -24,37 +24,6 @@ class PresetRef(BaseModel):
     )
 
 
-class SliceBundleSpec(BaseModel):
-    """Per-request reference to a Printer Preset Bundle stored on the slicer
-    sidecar. When SliceRequest.bundle is set, the dispatch skips PresetRef
-    resolution entirely and asks the sidecar to pick its inner JSON triplet
-    by name from the bundle's extracted directory — much faster than
-    re-uploading three profile JSONs every slice and matches the preset
-    triplet the user actually slices with in BambuStudio.
-    """
-
-    bundle_id: str = Field(
-        ...,
-        min_length=1,
-        description="Sidecar-side bundle id from POST /api/v1/slicer/bundles.",
-    )
-    printer_name: str = Field(
-        ...,
-        min_length=1,
-        description="Preset name within the bundle's printer/ directory (with or without the BambuStudio '# ' prefix).",
-    )
-    process_name: str = Field(
-        ...,
-        min_length=1,
-        description="Preset name within the bundle's process/ directory.",
-    )
-    filament_names: list[str] = Field(
-        ...,
-        min_length=1,
-        description="Per-slot filament preset names within the bundle's filament/ directory. Index 0 = slot 1.",
-    )
-
-
 class SliceRequest(BaseModel):
     """Body for `POST /library/files/{file_id}/slice`.
 
@@ -98,15 +67,6 @@ class SliceRequest(BaseModel):
     # is empty so older clients keep working.
     filament_presets: list[PresetRef] = Field(default_factory=list)
 
-    # Bundle dispatch alternative — when set, presets above are ignored and
-    # the slicer dispatch picks per-category JSONs from a previously-imported
-    # .bbscfg on the sidecar. Validator below short-circuits the
-    # presets-required check when this is non-None.
-    bundle: SliceBundleSpec | None = Field(
-        default=None,
-        description="When set, slice via a sidecar-side bundle instead of resolved preset refs.",
-    )
-
     plate: int | None = Field(
         default=None,
         ge=0,
@@ -142,13 +102,7 @@ class SliceRequest(BaseModel):
         ``filament_presets`` list satisfies the requirement on its own; an
         empty list falls back to the singular fields, which then promote
         into a one-element list.
-
-        When ``bundle`` is set, the dispatch picks the JSON triplet from
-        the sidecar bundle directly so PresetRef resolution is skipped —
-        return early before the presets-required checks below.
         """
-        if self.bundle is not None:
-            return self
         for slot, ref_attr, legacy_attr in (
             ("printer", "printer_preset", "printer_preset_id"),
             ("process", "process_preset", "process_preset_id"),

+ 26 - 0
backend/app/services/preset_resolver.py

@@ -164,6 +164,22 @@ async def _resolve_cloud(db: AsyncSession, user: User | None, ref: PresetRef, sl
             slot,
         )
         payload = detail
+    if isinstance(payload, dict):
+        # Bambu Cloud labels presets with `type: "printer"` / `"print"` /
+        # `"filament"`, but the BS / Orca CLI's `--load-settings` parser only
+        # accepts `"machine"` / `"process"` / `"filament"`. Without this
+        # rewrite the CLI exits -5 with `operator(): unknown config type`
+        # and the sidecar surfaces a generic "The input preset file is
+        # invalid and can not be parsed" — see preset_resolver header
+        # comment for the silent-fail history. `from` gets the same
+        # treatment: Bambu Cloud's filament details routinely arrive with
+        # `from: ""` (or no `from` at all) and the CLI rejects either with
+        # `operator(): ... from  unsupported` (same -5 exit). The standard
+        # tier already pins `from: "system"` for exactly this reason; the
+        # cloud tier needs the same pin because it lands at the same `--load-
+        # settings` parser. The sidecar's `normalizeFromField` only rewrites
+        # the `"User"` / `"System"` casings, not empty / missing values.
+        payload = {**payload, "type": _SLOT_TO_PROFILE_TYPE[slot], "from": "system"}
     return json.dumps(payload)
 
 
@@ -222,6 +238,16 @@ async def _resolve_orca_cloud(db: AsyncSession, user: User | None, ref: PresetRe
             slot,
         )
         content = profile
+    if isinstance(content, dict):
+        # Orca natively uses `machine` / `process` / `filament` for `type`,
+        # which is what the CLI wants — but Bambu-imported profiles synced
+        # through Orca Cloud can carry `printer` / `print` instead, and the
+        # CLI's `--load-settings` parser rejects those the same way it does
+        # for the Bambu Cloud tier. Force the slot-appropriate value so the
+        # source tier doesn't decide whether slicing works. `from` gets the
+        # same forced pin to `"system"` for the same reason — see the
+        # Bambu Cloud branch above.
+        content = {**content, "type": _SLOT_TO_PROFILE_TYPE[slot], "from": "system"}
     return json.dumps(content)
 
 

+ 23 - 88
backend/app/services/slice_preview.py

@@ -8,22 +8,12 @@ Bambu Studio applies its own pruning to painted-face data at slice time.
 
 This module wraps the sidecar's slice call so the endpoint can run a preview
 slice, parse the result's slice_info, and return the actual filament list.
-Two slice modes are supported:
-
-  * "embedded settings" mode (default) — calls ``slice_without_profiles`` so
-    the slicer falls back on the file's own ``Metadata/project_settings.config``.
-    Used when the SliceModal opens before the user has picked a profile
-    triplet and we just want the slot-mapping (which is a model property,
-    independent of process settings).
-
-  * "bundle" mode — when the caller passes a bundle id + per-category preset
-    names, calls ``slice_with_bundle`` so the preview reflects the same
-    triplet the real print will use. More accurate gram numbers; same slot
-    mapping. Used after the SliceModal's Bundle tier resolves.
-
-Results are cached by ``(kind, source_id, plate_id, content_hash, bundle_key)``
-so different bundle picks on the same file don't collide and repeat opens
-on the same plate + same bundle are instant. LRU eviction keeps the cache
+The preview always uses the file's embedded settings (``slice_without_profiles``):
+the slot-mapping is a model property, independent of process settings, so
+we don't need to thread the user's profile triplet through here.
+
+Results are cached by ``(kind, source_id, plate_id, content_hash)`` so
+repeat opens on the same plate are instant. LRU eviction keeps the cache
 bounded. Hash invalidation handles in-place file replacement; no TTL is
 used because preview-slice output is deterministic for a given input.
 """
@@ -47,19 +37,15 @@ from backend.app.services.slicer_api import (
 logger = logging.getLogger(__name__)
 
 _PREVIEW_CACHE_MAX = 256
-# Cache key includes a bundle-context fingerprint (or "" when no bundle was
-# supplied) so a "preview without profiles" result and a "preview with
-# bundle X" result for the same file/plate occupy distinct entries instead
-# of clobbering each other.
-_PreviewCacheKey = tuple[str, int, int, str, str]
+_PreviewCacheKey = tuple[str, int, int, str]
 # Cache values: list[dict] on success, [] on parsed-but-empty (slicer
 # returned a 3MF without filament data for this plate — caching the negative
 # avoids burning 30s+ per modal open on a known-bad input).
 _preview_cache: OrderedDict[_PreviewCacheKey, list[dict]] = OrderedDict()
-# Per-key locks prevent N concurrent modal opens on the same (file, plate,
-# bundle) from launching N redundant preview slices — only the first one
-# runs, the rest wait and read from the cache. Locks are evicted alongside
-# cache entries to keep the dict bounded; we do NOT cache transient sidecar
+# Per-key locks prevent N concurrent modal opens on the same (file, plate)
+# from launching N redundant preview slices — only the first one runs, the
+# rest wait and read from the cache. Locks are evicted alongside cache
+# entries to keep the dict bounded; we do NOT cache transient sidecar
 # failures (network errors etc.) so those retry naturally on next request.
 _preview_locks: dict[_PreviewCacheKey, asyncio.Lock] = {}
 
@@ -68,25 +54,6 @@ def _content_hash(file_bytes: bytes) -> str:
     return hashlib.sha256(file_bytes).hexdigest()[:16]
 
 
-def _bundle_context_fingerprint(
-    bundle_id: str | None,
-    printer_name: str | None,
-    process_name: str | None,
-    filament_names: list[str] | None,
-) -> str:
-    """Derive a stable cache-key fragment for the bundle context. Empty
-    string when no bundle is supplied — preserves cache compatibility with
-    the no-bundle ("embedded settings") path so existing entries remain
-    valid. SHA-256 prefix keeps the key short while collision-resistant
-    enough for a 256-entry LRU.
-    """
-    if not (bundle_id and printer_name and process_name and filament_names):
-        return ""
-    parts = [bundle_id, printer_name, process_name, *filament_names]
-    raw = "\x1f".join(parts).encode("utf-8")
-    return hashlib.sha256(raw).hexdigest()[:12]
-
-
 async def get_preview_filaments(
     *,
     kind: str,
@@ -96,33 +63,20 @@ async def get_preview_filaments(
     file_name: str,
     api_url: str,
     request_id: str | None = None,
-    bundle_id: str | None = None,
-    printer_name: str | None = None,
-    process_name: str | None = None,
-    filament_names: list[str] | None = None,
 ) -> list[dict] | None:
     """Run a preview slice for ``plate_id``, parse the resulting slice_info,
     and return the per-plate filament list.
 
-    By default uses the file's embedded settings (``slice_without_profiles``).
-    When all four ``bundle_*`` params are provided, uses ``slice_with_bundle``
-    so the preview matches the profile triplet the real print will use —
-    same slot mapping, more-accurate gram numbers. Partial bundle context
-    (e.g. id without preset names) falls back to the embedded path rather
-    than failing, so an in-progress modal selection doesn't surface errors.
+    Uses the file's embedded settings (``slice_without_profiles``) since the
+    slot mapping is a model property, independent of any user-picked profile
+    triplet.
 
     Returns ``None`` when the preview slice fails — the caller should fall
     back to whatever heuristic it has (typically the project_filaments +
     painted-face approach in ``threemf_tools``).
     """
     h = _content_hash(file_bytes)
-    bundle_fp = _bundle_context_fingerprint(
-        bundle_id,
-        printer_name,
-        process_name,
-        filament_names,
-    )
-    key: _PreviewCacheKey = (kind, source_id, plate_id, h, bundle_fp)
+    key: _PreviewCacheKey = (kind, source_id, plate_id, h)
     cached = _preview_cache.get(key)
     if cached is not None:
         _preview_cache.move_to_end(key)
@@ -139,38 +93,19 @@ async def get_preview_filaments(
 
         try:
             async with SlicerApiService(base_url=api_url) as svc:
-                if bundle_fp:
-                    # All four bundle params present (guaranteed non-None by
-                    # _bundle_context_fingerprint returning non-empty);
-                    # the type-checker can't see that, so assert for narrowing.
-                    assert bundle_id and printer_name and process_name
-                    assert filament_names is not None
-                    result = await svc.slice_with_bundle(
-                        model_bytes=file_bytes,
-                        model_filename=file_name,
-                        bundle_id=bundle_id,
-                        printer_name=printer_name,
-                        process_name=process_name,
-                        filament_names=filament_names,
-                        plate=plate_id,
-                        export_3mf=True,
-                        request_id=request_id,
-                    )
-                else:
-                    result = await svc.slice_without_profiles(
-                        model_bytes=file_bytes,
-                        model_filename=file_name,
-                        plate=plate_id,
-                        export_3mf=True,
-                        request_id=request_id,
-                    )
+                result = await svc.slice_without_profiles(
+                    model_bytes=file_bytes,
+                    model_filename=file_name,
+                    plate=plate_id,
+                    export_3mf=True,
+                    request_id=request_id,
+                )
         except SlicerApiError as e:
             logger.warning(
-                "Preview slice failed for %s/%s plate %s (bundle=%s): %s",
+                "Preview slice failed for %s/%s plate %s: %s",
                 kind,
                 source_id,
                 plate_id,
-                bundle_id or "-",
                 e,
             )
             return None

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

@@ -47,42 +47,6 @@ class SliceResult(NamedTuple):
     filament_used_mm: float
 
 
-class BundleSummary(NamedTuple):
-    """Sidecar's view of a stored Printer Preset Bundle (.bbscfg).
-
-    Mirrors the JSON shape returned by `/profiles/bundle(s)` on the
-    sidecar — `printer`, `process`, `filament` are each a list of preset
-    names available within the bundle (without the `.json` extension and
-    without the BambuStudio "# " user-clone prefix; the sidecar accepts
-    both forms when looking them up at slice time).
-    """
-
-    id: str
-    printer_preset_name: str
-    printer: list[str]
-    process: list[str]
-    filament: list[str]
-    version: str | None
-
-
-class BundleNotFoundError(SlicerApiError):
-    """Sidecar returned 404 for the bundle id (deleted, never imported)."""
-
-
-def _parse_bundle_summary(payload: dict) -> BundleSummary:
-    """Build a BundleSummary from the sidecar's JSON. Tolerant of missing
-    optional fields so a sidecar that adds keys later doesn't break parsing.
-    """
-    return BundleSummary(
-        id=str(payload.get("id") or ""),
-        printer_preset_name=str(payload.get("printer_preset_name") or ""),
-        printer=list(payload.get("printer") or []),
-        process=list(payload.get("process") or []),
-        filament=list(payload.get("filament") or []),
-        version=payload.get("version"),
-    )
-
-
 _shared_http_client: httpx.AsyncClient | None = None
 
 
@@ -191,102 +155,6 @@ class SlicerApiService:
             raise SlicerApiUnavailableError(f"Slicer sidecar /profiles/bundled returned {response.status_code}")
         return response.json()
 
-    async def import_bundle(
-        self,
-        zip_bytes: bytes,
-        *,
-        filename: str = "bundle.bbscfg",
-    ) -> BundleSummary:
-        """POST /profiles/bundle — upload a BambuStudio Printer Preset Bundle.
-
-        Idempotent on the sidecar side: re-uploading the same file yields the
-        same id (deterministic SHA-256 prefix of the zip content) and the
-        sidecar reuses its existing extracted directory, so re-importing is
-        always safe.
-
-        Raises:
-            SlicerInputError: 4xx — bundle isn't a valid .bbscfg, or fails the
-                sidecar's path-traversal / manifest validation.
-            SlicerApiUnavailableError: connection error or 5xx.
-        """
-        files = {"file": (filename, zip_bytes, "application/zip")}
-        try:
-            response = await self._client.post(
-                f"{self.base_url}/profiles/bundle",
-                files=files,
-                timeout=60.0,
-            )
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
-        if response.status_code >= 500:
-            raise SlicerApiServerError(
-                f"Slicer sidecar /profiles/bundle failed ({response.status_code}): {_format_sidecar_error(response)}",
-            )
-        if response.status_code >= 400:
-            raise SlicerInputError(
-                f"Slicer sidecar rejected bundle ({response.status_code}): {_format_sidecar_error(response)}",
-            )
-        return _parse_bundle_summary(response.json())
-
-    async def list_bundles(self) -> list[BundleSummary]:
-        """GET /profiles/bundles — list every imported bundle and its presets.
-
-        Returns an empty list when the sidecar's bundle store is empty (the
-        sidecar returns ``[]`` rather than 404 in that case). Network errors
-        and 5xx surface as ``SlicerApiUnavailableError`` so callers can
-        decide whether to render an empty UI or a "sidecar offline" banner.
-        """
-        try:
-            response = await self._client.get(f"{self.base_url}/profiles/bundles", timeout=10.0)
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
-        if response.status_code >= 400:
-            raise SlicerApiUnavailableError(
-                f"Slicer sidecar /profiles/bundles returned {response.status_code}",
-            )
-        payload = response.json()
-        if not isinstance(payload, list):
-            raise SlicerApiServerError("Slicer sidecar returned non-array bundle list")
-        return [_parse_bundle_summary(b) for b in payload if isinstance(b, dict)]
-
-    async def get_bundle(self, bundle_id: str) -> BundleSummary:
-        """GET /profiles/bundles/<id> — single bundle summary.
-
-        Raises:
-            BundleNotFoundError: 404 — id does not exist on the sidecar.
-            SlicerApiUnavailableError: connection error or 5xx.
-        """
-        try:
-            response = await self._client.get(
-                f"{self.base_url}/profiles/bundles/{bundle_id}",
-                timeout=10.0,
-            )
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
-        if response.status_code == 404:
-            raise BundleNotFoundError(f"Bundle {bundle_id!r} not found on sidecar")
-        if response.status_code >= 400:
-            raise SlicerApiUnavailableError(
-                f"Slicer sidecar /profiles/bundles/{bundle_id} returned {response.status_code}",
-            )
-        return _parse_bundle_summary(response.json())
-
-    async def delete_bundle(self, bundle_id: str) -> None:
-        """DELETE /profiles/bundles/<id> — remove a stored bundle."""
-        try:
-            response = await self._client.delete(
-                f"{self.base_url}/profiles/bundles/{bundle_id}",
-                timeout=10.0,
-            )
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
-        if response.status_code == 404:
-            raise BundleNotFoundError(f"Bundle {bundle_id!r} not found on sidecar")
-        if response.status_code >= 400:
-            raise SlicerApiUnavailableError(
-                f"Slicer sidecar DELETE /profiles/bundles/{bundle_id} returned {response.status_code}",
-            )
-
     async def _poll_progress(
         self,
         request_id: str,
@@ -438,114 +306,6 @@ class SlicerApiService:
             filament_used_mm=_safe_float(response.headers.get("x-filament-used-mm")),
         )
 
-    async def slice_with_bundle(
-        self,
-        *,
-        model_bytes: bytes,
-        model_filename: str,
-        bundle_id: str,
-        printer_name: str,
-        process_name: str,
-        filament_names: list[str],
-        plate: int | None = None,
-        export_3mf: bool = False,
-        arrange: bool = False,
-        bed_type: str | None = None,
-        request_id: str | None = None,
-        on_progress: Callable[[dict], None] | None = None,
-    ) -> SliceResult:
-        """POST /slice with bundle id + per-category preset names.
-
-        Asks the sidecar to materialize the printer / process / filament
-        JSONs from a previously-imported `.bbscfg`, instead of accepting
-        them as multipart attachments. Equivalent to
-        ``slice_with_profiles`` from the user's perspective — same return
-        shape, same 4xx/5xx semantics, same progress-poll wiring — but
-        the sidecar saves the round-trip of re-uploading the JSONs every
-        time a user kicks off a slice with the same bundle.
-
-        ``filament_names`` is plate-slot-ordered: index 0 is slot 1, etc.
-        Single-color callers pass a one-element list. The sidecar joins
-        them as semicolon-separated `--load-filaments` for the CLI.
-
-        Raises:
-            SlicerInputError: 4xx — bundle / preset name not found, etc.
-            SlicerApiServerError: sidecar 5xx (CLI failure on resolved
-                triplet — same conditions that fail slice_with_profiles).
-            SlicerApiUnavailableError: connection error.
-        """
-        files = {
-            "file": (model_filename, model_bytes, _guess_model_content_type(model_filename)),
-        }
-        data: dict[str, str | list[str]] = {
-            "bundle": bundle_id,
-            "printerName": printer_name,
-            "processName": process_name,
-        }
-        # The sidecar's SlicingSettings supports both `filamentName` (single
-        # legacy field, kept for clients that pre-date multi-color) and
-        # `filamentNames` (semicolon/comma-separated, matches multi-color
-        # uploads). Always send the array form so a single-slot case still
-        # ends up in the same code path on the sidecar.
-        data["filamentNames"] = ";".join(filament_names)
-        if plate is not None:
-            data["plate"] = str(plate)
-        if export_3mf:
-            data["exportType"] = "3mf"
-        if arrange:
-            # See slice_with_profiles for the rationale: cross-class re-slices
-            # (#1493) need --arrange so BS repositions objects for the target
-            # bed instead of inheriting the source printer's coordinate layout.
-            data["arrange"] = "true"
-        if bed_type is not None:
-            # #1337: bed-plate override flows through to the sidecar as a
-            # standalone field. The sidecar wraps this as --curr_bed_type on
-            # the CLI invocation, overriding whatever the bundle's process
-            # JSON specifies. Bambuddy can't patch the bundle's JSON locally
-            # (the sidecar materialises it from disk), so this round-trip is
-            # the only path. Silently no-ops on sidecar versions that don't
-            # yet recognise the field — the user's slice still runs with the
-            # bundle's default plate, no crash.
-            data["bedType"] = bed_type
-        if request_id is not None:
-            data["requestId"] = request_id
-
-        progress_task: asyncio.Task | None = None
-        if request_id is not None and on_progress is not None:
-            progress_task = asyncio.create_task(
-                self._poll_progress(request_id, on_progress),
-                name=f"slicer-progress-{request_id}",
-            )
-
-        try:
-            response = await self._client.post(
-                f"{self.base_url}/slice",
-                files=files,
-                data=data,
-                timeout=self.timeout_seconds,
-            )
-        except httpx.RequestError as exc:
-            raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
-        finally:
-            if progress_task is not None:
-                progress_task.cancel()
-                try:
-                    await progress_task
-                except (asyncio.CancelledError, Exception):
-                    pass
-
-        if response.status_code >= 500:
-            raise SlicerApiServerError(f"Slicer CLI failed ({response.status_code}): {_format_sidecar_error(response)}")
-        if response.status_code >= 400:
-            raise SlicerInputError(f"Slicer rejected input ({response.status_code}): {_format_sidecar_error(response)}")
-
-        return SliceResult(
-            content=response.content,
-            print_time_seconds=_safe_int(response.headers.get("x-print-time-seconds")),
-            filament_used_g=_safe_float(response.headers.get("x-filament-used-g")),
-            filament_used_mm=_safe_float(response.headers.get("x-filament-used-mm")),
-        )
-
     async def slice_without_profiles(
         self,
         *,

+ 14 - 258
backend/tests/integration/test_library_slice_api.py

@@ -535,234 +535,6 @@ class TestSliceLibraryFile:
         assert "3D/3dmodel.model" in names
 
 
-class TestSliceWithBundle:
-    """Bundle dispatch path: when SliceRequest.bundle is set, the dispatch
-    forwards bundle id + per-category preset names to the sidecar instead
-    of resolving cloud/local/standard PresetRefs. Same fallback semantics
-    apply for 3MF inputs whose CLI run fails."""
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_bundle_dispatch_forwards_form_fields(self, async_client: AsyncClient, slice_test_setup):
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["body"] = request.content
-            return httpx.Response(
-                status_code=200,
-                content=b"PK\x03\x04 fake-3mf",
-                headers={
-                    "x-print-time-seconds": "200",
-                    "x-filament-used-g": "1.5",
-                    "x-filament-used-mm": "150",
-                },
-            )
-
-        _install_mock_sidecar(handler)
-        response = await async_client.post(
-            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
-            json={
-                "bundle": {
-                    "bundle_id": "abc123def456abcd",
-                    "printer_name": "# Bambu Lab H2D 0.4 nozzle",
-                    "process_name": "# 0.20mm Standard @BBL H2D",
-                    "filament_names": [
-                        "# Bambu PLA Basic @BBL H2D",
-                        "# Bambu PETG HF @BBL H2D 0.4 nozzle",
-                    ],
-                },
-            },
-        )
-        assert response.status_code == 202, response.text
-        final = await _wait_for_job(async_client, response.json()["job_id"])
-        assert final["status"] == "completed", final
-
-        # Multipart form body should carry the bundle selectors instead of
-        # the JSON profile attachments. Quick string-level check is enough
-        # to confirm the dispatch picked the bundle branch.
-        body = captured["body"]
-        assert b'name="bundle"' in body
-        assert b"abc123def456abcd" in body
-        assert b'name="printerName"' in body
-        assert b'name="processName"' in body
-        assert b'name="filamentNames"' in body
-        # Multi-color filament list joined with ';' on the wire.
-        assert b"# Bambu PLA Basic @BBL H2D;# Bambu PETG HF @BBL H2D 0.4 nozzle" in body
-        # Profile attachments must NOT be present — bundle dispatch skips
-        # PresetRef resolution entirely.
-        assert b'name="printerProfile"' not in body
-        assert b'name="presetProfile"' not in body
-        assert b'name="filamentProfile"' not in body
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_bundle_dispatch_forwards_bed_type_when_set(self, async_client: AsyncClient, slice_test_setup):
-        """#1337 follow-up: bed-type override flows through the bundle path
-        as a `bedType` form field so the sidecar can pass
-        `--curr_bed_type` to the CLI. Bambuddy can't patch the bundle's
-        process JSON locally — the sidecar materialises it from the stored
-        .bbscfg — so the form field is the only handle."""
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["body"] = bytes(request.content)
-            return httpx.Response(
-                status_code=200,
-                content=b"PK\x03\x04 fake",
-                headers={
-                    "x-print-time-seconds": "10",
-                    "x-filament-used-g": "0.1",
-                    "x-filament-used-mm": "1.0",
-                },
-            )
-
-        _install_mock_sidecar(handler)
-        response = await async_client.post(
-            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
-            json={
-                "bundle": {
-                    "bundle_id": "abc",
-                    "printer_name": "# X1C",
-                    "process_name": "# 0.20mm",
-                    "filament_names": ["# Bambu PLA"],
-                },
-                "bed_type": "Engineering Plate",
-            },
-        )
-        assert response.status_code == 202
-        final = await _wait_for_job(async_client, response.json()["job_id"])
-        assert final["status"] == "completed", final
-        body = captured["body"]
-        assert b'name="bedType"' in body
-        assert b"Engineering Plate" in body
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_bundle_dispatch_omits_bed_type_when_unset(self, async_client: AsyncClient, slice_test_setup):
-        """Companion test: no bed_type ⇒ no bedType form field, so the
-        bundle's own curr_bed_type is preserved end-to-end."""
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["body"] = bytes(request.content)
-            return httpx.Response(
-                status_code=200,
-                content=b"PK\x03\x04 fake",
-                headers={
-                    "x-print-time-seconds": "10",
-                    "x-filament-used-g": "0.1",
-                    "x-filament-used-mm": "1.0",
-                },
-            )
-
-        _install_mock_sidecar(handler)
-        response = await async_client.post(
-            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
-            json={
-                "bundle": {
-                    "bundle_id": "abc",
-                    "printer_name": "# X1C",
-                    "process_name": "# 0.20mm",
-                    "filament_names": ["# Bambu PLA"],
-                },
-            },
-        )
-        assert response.status_code == 202
-        final = await _wait_for_job(async_client, response.json()["job_id"])
-        assert final["status"] == "completed", final
-        assert b'name="bedType"' not in captured["body"]
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_bundle_dispatch_3mf_falls_back_to_embedded_on_5xx(
-        self, async_client: AsyncClient, db_session, slice_test_setup
-    ):
-        # Same fallback as the preset-based path: if the resolved bundle
-        # triplet crashes the CLI on a 3MF, retry with embedded settings
-        # so the user gets *something* rather than a hard failure.
-        src_3mf_path = slice_test_setup["tmp_path"] / "library" / "files" / "complex_bundle.3mf"
-        src_3mf_path.write_bytes(_make_3mf_with_settings({"prime_tower_brim_width": "-1"}))
-        threemf = LibraryFile(
-            filename="complex_bundle.3mf",
-            file_path=str(src_3mf_path.relative_to(slice_test_setup["tmp_path"])),
-            file_type="3mf",
-            file_size=src_3mf_path.stat().st_size,
-        )
-        db_session.add(threemf)
-        await db_session.commit()
-        await db_session.refresh(threemf)
-
-        call_count = {"n": 0}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            call_count["n"] += 1
-            # First call: bundle path → simulate CLI 5xx
-            if call_count["n"] == 1:
-                return httpx.Response(
-                    status_code=500,
-                    json={"message": "Failed to slice the model"},
-                )
-            # Retry: no profiles / no bundle → succeed with embedded settings
-            return httpx.Response(
-                status_code=200,
-                content=b"PK\x03\x04 fake-3mf",
-                headers={
-                    "x-print-time-seconds": "100",
-                    "x-filament-used-g": "1.0",
-                    "x-filament-used-mm": "100",
-                },
-            )
-
-        _install_mock_sidecar(handler)
-        response = await async_client.post(
-            f"/api/v1/library/files/{threemf.id}/slice",
-            json={
-                "bundle": {
-                    "bundle_id": "abc",
-                    "printer_name": "P",
-                    "process_name": "Q",
-                    "filament_names": ["F"],
-                },
-            },
-        )
-        assert response.status_code == 202
-
-        final = await _wait_for_job(async_client, response.json()["job_id"])
-        assert final["status"] == "completed", final
-        assert final["result"]["used_embedded_settings"] is True
-        assert call_count["n"] == 2  # bundle attempt + embedded fallback
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_bundle_dispatch_404_surfaces_as_400(self, async_client: AsyncClient, slice_test_setup):
-        # Sidecar returns 404 when the bundle / preset name isn't found —
-        # the slicer client classifies this as user-correctable input
-        # error so the dispatch returns 400 to the caller, not 502.
-        def handler(_: httpx.Request) -> httpx.Response:
-            return httpx.Response(
-                status_code=404,
-                json={"message": 'process preset "Imaginary" not found in bundle "abc"'},
-            )
-
-        _install_mock_sidecar(handler)
-        response = await async_client.post(
-            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
-            json={
-                "bundle": {
-                    "bundle_id": "abc",
-                    "printer_name": "P",
-                    "process_name": "Imaginary",
-                    "filament_names": ["F"],
-                },
-            },
-        )
-        assert response.status_code == 202
-        final = await _wait_for_job(async_client, response.json()["job_id"])
-        assert final["status"] == "failed"
-        assert final["error_status"] == 400
-        assert "imaginary" in (final["error_detail"] or "").lower()
-
-
 # ---------------------------------------------------------------------------
 # GET /slice-jobs/{id}
 # ---------------------------------------------------------------------------
@@ -1603,24 +1375,19 @@ class TestCanonicalPrinterModel:
 
 class TestNozzleClassGuard:
     """guard_nozzle_class_reslice is now a no-op (#1493). Cross-class re-slicing
-    is handled by the two-pass conversion in _run_slicer_with_fallback for
-    both preset and bundle dispatch — so the guard never blocks. The function
-    is kept (and these tests with it) so external forks / pinned versions
-    that call it still link, and so a future regression that re-introduces a
-    raise inside the helper gets caught here."""
+    is handled by the two-pass conversion in _run_slicer_with_fallback — so the
+    guard never blocks. The function is kept (and these tests with it) so
+    external forks / pinned versions that call it still link, and so a future
+    regression that re-introduces a raise inside the helper gets caught here."""
 
     @staticmethod
-    def _bundle_request() -> object:
-        return type("_Req", (), {"bundle": object()})()
-
-    @staticmethod
-    def _preset_request() -> object:
-        return type("_Req", (), {"bundle": None})()
+    def _request() -> object:
+        return type("_Req", (), {})()
 
     @pytest.mark.asyncio
-    async def test_single_to_dual_bundle_is_allowed(self, monkeypatch):
-        """Bundle-mode cross-class: handled by the two-pass converter via
-        slice_with_bundle on the cube, so the guard does NOT raise."""
+    async def test_single_to_dual_is_allowed(self, monkeypatch):
+        """Cross-class re-slice: handled by the two-pass converter, so the
+        guard does NOT raise."""
         import backend.app.api.routes.library as lib
 
         async def _target(_db, _user, _request):
@@ -1628,28 +1395,17 @@ class TestNozzleClassGuard:
 
         monkeypatch.setattr(lib, "_resolve_target_printer_model", _target)
         # No raise — the converter handles this case now.
-        await guard_nozzle_class_reslice(None, None, self._bundle_request(), "X1C")
+        await guard_nozzle_class_reslice(None, None, self._request(), "X1C")
 
     @pytest.mark.asyncio
-    async def test_dual_to_single_bundle_is_allowed(self, monkeypatch):
+    async def test_dual_to_single_is_allowed(self, monkeypatch):
         import backend.app.api.routes.library as lib
 
         async def _target(_db, _user, _request):
             return "X1C"
 
         monkeypatch.setattr(lib, "_resolve_target_printer_model", _target)
-        await guard_nozzle_class_reslice(None, None, self._bundle_request(), "H2D")
-
-    @pytest.mark.asyncio
-    async def test_preset_path_is_not_blocked(self, monkeypatch):
-        """Preset path cross-class is also handled by the two-pass converter."""
-        import backend.app.api.routes.library as lib
-
-        async def _target(_db, _user, _request):
-            return "H2D"
-
-        monkeypatch.setattr(lib, "_resolve_target_printer_model", _target)
-        await guard_nozzle_class_reslice(None, None, self._preset_request(), "X1C")
+        await guard_nozzle_class_reslice(None, None, self._request(), "H2D")
 
     @pytest.mark.asyncio
     async def test_same_nozzle_class_is_allowed(self, monkeypatch):
@@ -1659,7 +1415,7 @@ class TestNozzleClassGuard:
             return "P1S"
 
         monkeypatch.setattr(lib, "_resolve_target_printer_model", _target)
-        await guard_nozzle_class_reslice(None, None, self._bundle_request(), "X1C")
+        await guard_nozzle_class_reslice(None, None, self._request(), "X1C")
 
     @pytest.mark.asyncio
     async def test_no_source_model_is_a_noop(self, monkeypatch):
@@ -1669,7 +1425,7 @@ class TestNozzleClassGuard:
             return "H2D"
 
         monkeypatch.setattr(lib, "_resolve_target_printer_model", _target)
-        await guard_nozzle_class_reslice(None, None, self._bundle_request(), None)
+        await guard_nozzle_class_reslice(None, None, self._request(), None)
 
     @pytest.mark.asyncio
     async def test_null_request_is_a_noop(self):

+ 128 - 2
backend/tests/unit/services/test_preset_resolver.py

@@ -157,7 +157,16 @@ async def test_cloud_unwraps_setting_envelope():
     ):
         out = await preset_resolver._resolve_cloud(db, user, PresetRef(source="cloud", id="PFU123"), slot="printer")
     payload = json.loads(out)
-    assert payload == {"name": "X1C Custom", "nozzle_diameter": [0.4]}
+    # Resolver rewrites the `type` field to the CLI-expected value AND pins
+    # `from: "system"` (#1712 follow-up: Bambu Cloud labels printers as
+    # "printer" and filaments routinely ship with empty `from`; the CLI
+    # rejects either with the same -5 "input preset invalid" surface).
+    assert payload == {
+        "name": "X1C Custom",
+        "nozzle_diameter": [0.4],
+        "type": "machine",
+        "from": "system",
+    }
     cloud_mock.close.assert_awaited_once()
 
 
@@ -186,6 +195,115 @@ async def test_cloud_falls_back_to_top_level_when_no_envelope():
     assert "name" in payload
 
 
+@pytest.mark.parametrize(
+    "slot, source_type, expected_type",
+    [
+        # Bambu Cloud's wire shape: `printer` / `print` / `filament`. The CLI
+        # only accepts `machine` / `process` / `filament`. Without rewrite
+        # the CLI exits -5 with `operator(): unknown config type` and the
+        # sidecar surfaces "The input preset file is invalid and can not be
+        # parsed" (#1712 follow-up, reported by maziggy on Mecha Mewtwo).
+        ("printer", "printer", "machine"),
+        ("process", "print", "process"),
+        ("filament", "filament", "filament"),
+        # Cloud-side already CLI-shaped: still gets overwritten to the
+        # canonical value — idempotent, no harm.
+        ("printer", "machine", "machine"),
+        ("process", "process", "process"),
+        # Missing type field on the source payload: synthesise it.
+        ("printer", None, "machine"),
+        ("process", None, "process"),
+    ],
+)
+@pytest.mark.asyncio
+async def test_cloud_rewrites_type_field_for_cli(slot, source_type, expected_type):
+    db = MagicMock()
+    user = MagicMock()
+    user.has_permission = MagicMock(return_value=True)
+    setting: dict = {"name": "P"}
+    if source_type is not None:
+        setting["type"] = source_type
+    cloud_mock = MagicMock()
+    cloud_mock.set_token = MagicMock()
+    cloud_mock.get_setting_detail = AsyncMock(return_value={"setting": setting})
+    cloud_mock.close = AsyncMock()
+    with (
+        patch.object(
+            preset_resolver,
+            "get_stored_token",
+            AsyncMock(return_value=("tok", None, "global")),
+        ),
+        patch.object(preset_resolver, "BambuCloudService", return_value=cloud_mock),
+    ):
+        out = await preset_resolver._resolve_cloud(db, user, PresetRef(source="cloud", id="X"), slot=slot)
+    assert json.loads(out)["type"] == expected_type
+
+
+@pytest.mark.parametrize(
+    "source_from",
+    [
+        # The actual failing case (#1712 follow-up): Bambu Cloud's filament
+        # detail endpoint routinely returns presets with no `from` field or
+        # `from: ""`. The CLI rejects either with
+        # `operator(): ... from  unsupported` (note the double space — that's
+        # the literal stderr from the sidecar log on the Mecha Mewtwo slice).
+        "",
+        # Cloud-side already CLI-friendly: still gets pinned to "system" —
+        # idempotent, no harm, matches the standard-tier convention.
+        "system",
+        # GUI-exported values that the sidecar's normalizeFromField also
+        # maps to "system" for the same reason — we beat it to the punch.
+        "User",
+        "System",
+    ],
+)
+@pytest.mark.asyncio
+async def test_cloud_pins_from_field_to_system(source_from):
+    db = MagicMock()
+    user = MagicMock()
+    user.has_permission = MagicMock(return_value=True)
+    setting: dict = {"name": "F", "type": "filament", "from": source_from}
+    cloud_mock = MagicMock()
+    cloud_mock.set_token = MagicMock()
+    cloud_mock.get_setting_detail = AsyncMock(return_value={"setting": setting})
+    cloud_mock.close = AsyncMock()
+    with (
+        patch.object(
+            preset_resolver,
+            "get_stored_token",
+            AsyncMock(return_value=("tok", None, "global")),
+        ),
+        patch.object(preset_resolver, "BambuCloudService", return_value=cloud_mock),
+    ):
+        out = await preset_resolver._resolve_cloud(db, user, PresetRef(source="cloud", id="X"), slot="filament")
+    assert json.loads(out)["from"] == "system"
+
+
+@pytest.mark.asyncio
+async def test_cloud_synthesises_from_field_when_missing():
+    """The original failing payload had no `from` field at all (sidecar
+    error: `from  unsupported` — double space = empty value). The resolver
+    must still emit a usable `from` instead of forwarding the gap."""
+    db = MagicMock()
+    user = MagicMock()
+    user.has_permission = MagicMock(return_value=True)
+    setting = {"name": "F", "type": "filament"}  # NB: no `from`
+    cloud_mock = MagicMock()
+    cloud_mock.set_token = MagicMock()
+    cloud_mock.get_setting_detail = AsyncMock(return_value={"setting": setting})
+    cloud_mock.close = AsyncMock()
+    with (
+        patch.object(
+            preset_resolver,
+            "get_stored_token",
+            AsyncMock(return_value=("tok", None, "global")),
+        ),
+        patch.object(preset_resolver, "BambuCloudService", return_value=cloud_mock),
+    ):
+        out = await preset_resolver._resolve_cloud(db, user, PresetRef(source="cloud", id="X"), slot="filament")
+    assert json.loads(out)["from"] == "system"
+
+
 @pytest.mark.asyncio
 async def test_cloud_auth_error_returns_401():
     db = MagicMock()
@@ -246,7 +364,15 @@ async def test_orca_cloud_unwraps_content():
             db, user, PresetRef(source="orca_cloud", id="abc"), slot="printer"
         )
     payload = json.loads(out)
-    assert payload == {"name": "X1C Custom", "nozzle_diameter": [0.4]}
+    # Resolver rewrites `type` to the CLI-expected value AND pins
+    # `from: "system"` (#1712 follow-up). Orca natively uses "machine" but
+    # Bambu-sourced syncs can carry "printer" and either empty/missing `from`.
+    assert payload == {
+        "name": "X1C Custom",
+        "nozzle_diameter": [0.4],
+        "type": "machine",
+        "from": "system",
+    }
     svc_mock.close.assert_awaited_once()
 
 

+ 0 - 182
backend/tests/unit/services/test_slice_preview.py

@@ -82,17 +82,6 @@ class _StubService:
             filament_used_mm=0.0,
         )
 
-    async def slice_with_bundle(self, **kw):
-        self.calls.append({"method": "slice_with_bundle", **kw})
-        if self.raise_exc is not None:
-            raise self.raise_exc
-        return SliceResult(
-            content=self.response_bytes or b"",
-            print_time_seconds=0,
-            filament_used_g=0.0,
-            filament_used_mm=0.0,
-        )
-
 
 # ---------------------------------------------------------------------------
 # _parse_filaments_from_sliced_3mf — pure-function parsing tests.
@@ -265,174 +254,3 @@ class TestGetPreviewFilaments:
         assert len(slice_preview._preview_cache) == _PREVIEW_CACHE_MAX
         # Lock dict is also pruned (no leak): same size as cache.
         assert len(slice_preview._preview_locks) == _PREVIEW_CACHE_MAX
-
-
-# ---------------------------------------------------------------------------
-# Bundle-aware preview path — when bundle context is supplied, the preview
-# routes through `slice_with_bundle` so its gram numbers reflect the same
-# triplet the real print will use. Cache must distinguish between bundle
-# picks so a fresh selection doesn't re-serve a prior preview's output.
-# ---------------------------------------------------------------------------
-
-
-class TestBundleAwarePreview:
-    @pytest.mark.asyncio
-    async def test_full_bundle_context_uses_slice_with_bundle(self):
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            result = await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="abc123",
-                printer_name="# Bambu Lab H2D 0.4 nozzle",
-                process_name="# 0.20mm Standard @BBL H2D",
-                filament_names=["# Bambu PLA Basic @BBL H2D"],
-            )
-        assert result is not None
-        assert result[0]["slot_id"] == 1
-        # The bundle path engaged — slice_with_bundle was called, not the
-        # embedded-settings fallback.
-        assert len(stub.calls) == 1
-        assert stub.calls[0]["method"] == "slice_with_bundle"
-        assert stub.calls[0]["bundle_id"] == "abc123"
-        assert stub.calls[0]["filament_names"] == ["# Bambu PLA Basic @BBL H2D"]
-
-    @pytest.mark.asyncio
-    async def test_partial_bundle_context_falls_back_to_embedded(self):
-        # Modal-in-progress case: user picked a bundle id but hasn't yet
-        # picked the filament. Falling back to embedded settings keeps
-        # the preview's slot mapping fresh while gram numbers will firm
-        # up once the selection completes.
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="abc123",
-                printer_name="# Bambu Lab H2D 0.4 nozzle",
-                process_name="# 0.20mm Standard @BBL H2D",
-                # filament_names missing
-            )
-        assert len(stub.calls) == 1
-        assert stub.calls[0]["method"] == "slice_without_profiles"
-
-    @pytest.mark.asyncio
-    async def test_empty_filament_names_list_falls_back(self):
-        # Empty list (vs None) is treated as "incomplete context" since
-        # passing `[]` to slice_with_bundle would yield no
-        # --load-filaments arg and confuse the CLI.
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="abc123",
-                printer_name="P",
-                process_name="Q",
-                filament_names=[],
-            )
-        assert stub.calls[0]["method"] == "slice_without_profiles"
-
-    @pytest.mark.asyncio
-    async def test_cache_separates_bundle_picks(self):
-        # Same file/plate, two different bundle picks → two distinct cache
-        # entries → two slices run. Without the bundle-fingerprint cache key,
-        # the second call would erroneously serve the first's output.
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="bundleA",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["F"],
-            )
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="bundleB",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["F"],
-            )
-        assert len(stub.calls) == 2
-        assert stub.calls[0]["bundle_id"] == "bundleA"
-        assert stub.calls[1]["bundle_id"] == "bundleB"
-
-    @pytest.mark.asyncio
-    async def test_cache_separates_bundle_vs_embedded(self):
-        # Same file/plate, one call without bundle and one with bundle →
-        # both must run. The embedded-settings cache entry must NOT be
-        # served as the bundle-picked result (gram numbers would be wrong).
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-            )
-            await get_preview_filaments(
-                kind="library_file",
-                source_id=42,
-                plate_id=1,
-                file_bytes=b"abc",
-                file_name="x.3mf",
-                api_url="http://sidecar",
-                bundle_id="bundleA",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["F"],
-            )
-        methods = [c["method"] for c in stub.calls]
-        assert methods == ["slice_without_profiles", "slice_with_bundle"]
-
-    @pytest.mark.asyncio
-    async def test_bundle_repeat_call_hits_cache(self):
-        # Sanity check that the new cache key is otherwise stable: same
-        # bundle pick on the same file → cache hit on second call.
-        body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}])
-        stub = _StubService(response_bytes=body)
-        with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub):
-            for _ in range(2):
-                await get_preview_filaments(
-                    kind="library_file",
-                    source_id=42,
-                    plate_id=1,
-                    file_bytes=b"abc",
-                    file_name="x.3mf",
-                    api_url="http://sidecar",
-                    bundle_id="bundleA",
-                    printer_name="P",
-                    process_name="Q",
-                    filament_names=["F"],
-                )
-        assert len(stub.calls) == 1

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

@@ -8,8 +8,6 @@ import httpx
 import pytest
 
 from backend.app.services.slicer_api import (
-    BundleNotFoundError,
-    BundleSummary,
     SlicerApiServerError,
     SlicerApiService,
     SlicerApiUnavailableError,
@@ -537,274 +535,3 @@ class TestSliceWithProfilesProgress:
         assert result is not None
         # Sustained 404 → no snapshots ever forwarded.
         assert snapshots == []
-
-
-# ── BundleSummary parsing + bundle CRUD client methods ─────────────────────
-
-
-class TestBundleClientMethods:
-    """Coverage for import_bundle / list_bundles / get_bundle / delete_bundle.
-
-    Mirrors the existing SlicerApiService tests' mock-transport pattern. The
-    bundle endpoints are simple JSON CRUD on the sidecar, but the response
-    parsing has to remain forgiving (newer sidecars may add fields, older
-    ones may omit some) and the failure modes have to map cleanly to our
-    typed exceptions so route handlers can pick the right HTTP status.
-    """
-
-    SAMPLE_SUMMARY = {
-        "id": "2bd8722dd20a837e",
-        "printer_preset_name": "# Bambu Lab H2D 0.4 nozzle",
-        "printer": ["# Bambu Lab H2D 0.4 nozzle"],
-        "process": ["# 0.20mm Standard @BBL H2D"],
-        "filament": ["# Bambu PLA Basic @BBL H2D"],
-        "version": "02.06.00.50",
-    }
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_happy_path(self):
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["url"] = str(request.url)
-            captured["method"] = request.method
-            captured["content_type"] = request.headers.get("content-type", "")
-            return httpx.Response(status_code=201, json=self.SAMPLE_SUMMARY)
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        summary = await service.import_bundle(b"PK\x03\x04zip-bytes", filename="H2D.bbscfg")
-
-        assert isinstance(summary, BundleSummary)
-        assert summary.id == "2bd8722dd20a837e"
-        assert summary.printer == ["# Bambu Lab H2D 0.4 nozzle"]
-        assert summary.process == ["# 0.20mm Standard @BBL H2D"]
-        assert summary.filament == ["# Bambu PLA Basic @BBL H2D"]
-        assert captured["method"] == "POST"
-        assert captured["url"].endswith("/profiles/bundle")
-        assert captured["content_type"].startswith("multipart/form-data")
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_400_raises_input_error(self):
-        # Non-.bbscfg uploads, corrupt zips, malicious entry names — all
-        # rejected by the sidecar with 4xx so the user can fix and retry.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(
-                status_code=400,
-                json={"message": "Bundle is missing bundle_structure.json"},
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerInputError) as exc_info:
-            await service.import_bundle(b"not a zip")
-        assert "missing bundle_structure" in str(exc_info.value)
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_5xx_raises_server_error(self):
-        # Disk-write failure on DATA_PATH — rare but observable when /data
-        # is a tmpfs that filled up.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(status_code=500, json={"message": "ENOSPC"})
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerApiServerError):
-            await service.import_bundle(b"x")
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_connection_error(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            raise httpx.ConnectError("connection refused")
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerApiUnavailableError):
-            await service.import_bundle(b"x")
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_returns_summaries(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            assert request.url.path == "/profiles/bundles"
-            return httpx.Response(status_code=200, json=[self.SAMPLE_SUMMARY])
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        bundles = await service.list_bundles()
-        assert len(bundles) == 1
-        assert bundles[0].id == self.SAMPLE_SUMMARY["id"]
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_empty_array(self):
-        # Sidecar returns [] when no bundles imported yet — must not raise.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(status_code=200, json=[])
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        assert await service.list_bundles() == []
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_non_array_raises(self):
-        # Older / mis-configured sidecar returning {} instead of []. Surface
-        # the bug with a clear server error rather than silently treating
-        # malformed payload as empty.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(status_code=200, json={"unexpected": "shape"})
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerApiServerError):
-            await service.list_bundles()
-
-    @pytest.mark.asyncio
-    async def test_get_bundle_404_raises_not_found(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(status_code=404, json={"message": "not found"})
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(BundleNotFoundError):
-            await service.get_bundle("deadbeef00000000")
-
-    @pytest.mark.asyncio
-    async def test_get_bundle_happy_path(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            assert request.url.path == "/profiles/bundles/2bd8722dd20a837e"
-            return httpx.Response(status_code=200, json=self.SAMPLE_SUMMARY)
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        summary = await service.get_bundle("2bd8722dd20a837e")
-        assert summary.id == "2bd8722dd20a837e"
-
-    @pytest.mark.asyncio
-    async def test_delete_bundle_204_succeeds_silently(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            assert request.method == "DELETE"
-            return httpx.Response(status_code=204)
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        # Should not raise.
-        await service.delete_bundle("2bd8722dd20a837e")
-
-    @pytest.mark.asyncio
-    async def test_delete_bundle_404_raises_not_found(self):
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(status_code=404, json={"message": "not found"})
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(BundleNotFoundError):
-            await service.delete_bundle("missing")
-
-
-class TestSliceWithBundle:
-    """The bundle slice path takes the same model upload but replaces the
-    profile-attachment fields with bundle-id + preset-name form fields.
-    Coverage for the form shape, the multi-filament join, and the same
-    4xx/5xx mapping as slice_with_profiles."""
-
-    @pytest.mark.asyncio
-    async def test_form_fields_and_filament_join(self):
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["url"] = str(request.url)
-            captured["body"] = request.content
-            captured["content_type"] = request.headers.get("content-type", "")
-            return httpx.Response(
-                status_code=200,
-                content=b"; G-CODE",
-                headers={
-                    "x-print-time-seconds": "60",
-                    "x-filament-used-g": "1.0",
-                    "x-filament-used-mm": "100.0",
-                },
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        result = await service.slice_with_bundle(
-            model_bytes=b"solid Cube\n",
-            model_filename="Cube.stl",
-            bundle_id="2bd8722dd20a837e",
-            printer_name="# Bambu Lab H2D 0.4 nozzle",
-            process_name="# 0.20mm Standard @BBL H2D",
-            filament_names=["# Bambu PLA Basic @BBL H2D", "# Bambu PETG HF @BBL H2D"],
-        )
-
-        assert isinstance(result, SliceResult)
-        assert result.print_time_seconds == 60
-        assert captured["url"].endswith("/slice")
-        assert captured["content_type"].startswith("multipart/form-data")
-        # Multi-filament joined with ';' — the sidecar's parser splits on
-        # both ';' and ',' so the wire format is the more-explicit ';'.
-        body = captured["body"]
-        assert b"# Bambu PLA Basic @BBL H2D;# Bambu PETG HF @BBL H2D" in body
-        # Each form field appears in the multipart body.
-        assert b'name="bundle"' in body
-        assert b'name="printerName"' in body
-        assert b'name="processName"' in body
-        assert b'name="filamentNames"' in body
-        # Bundle id round-trips on the wire.
-        assert b"2bd8722dd20a837e" in body
-
-    @pytest.mark.asyncio
-    async def test_arrange_true_emits_form_field(self):
-        """#1493: bundle dispatch also forwards arrange=True so cross-class
-        slices via .bbscfg bundles get the same BS auto-arrange behaviour
-        as the preset path."""
-        captured: dict = {}
-
-        def handler(request: httpx.Request) -> httpx.Response:
-            captured["body"] = request.content
-            return httpx.Response(
-                status_code=200,
-                content=b"3MF",
-                headers={"x-print-time-seconds": "0", "x-filament-used-g": "0", "x-filament-used-mm": "0"},
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        await service.slice_with_bundle(
-            model_bytes=b"x",
-            model_filename="Cube.3mf",
-            bundle_id="abc",
-            printer_name="p",
-            process_name="pr",
-            filament_names=["f"],
-            arrange=True,
-        )
-
-        assert b'name="arrange"' in captured["body"]
-
-    @pytest.mark.asyncio
-    async def test_404_unknown_preset_maps_to_input_error(self):
-        # Sidecar returns 404 when bundle exists but preset name doesn't.
-        # The slice route classifies this as user-correctable input error,
-        # not server failure.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(
-                status_code=404,
-                json={"message": 'process preset "Imaginary" not found in bundle "abc"'},
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerInputError):
-            await service.slice_with_bundle(
-                model_bytes=b"x",
-                model_filename="Cube.stl",
-                bundle_id="abc",
-                printer_name="p",
-                process_name="Imaginary",
-                filament_names=["f"],
-            )
-
-    @pytest.mark.asyncio
-    async def test_5xx_maps_to_server_error(self):
-        # CLI segfault on the resolved triplet — same handling as slice_with_profiles.
-        def handler(request: httpx.Request) -> httpx.Response:
-            return httpx.Response(
-                status_code=500,
-                json={"message": "Slicer process failed (signal SIGSEGV)"},
-            )
-
-        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
-        with pytest.raises(SlicerApiServerError):
-            await service.slice_with_bundle(
-                model_bytes=b"x",
-                model_filename="Cube.3mf",
-                bundle_id="abc",
-                printer_name="p",
-                process_name="pr",
-                filament_names=["f"],
-            )

+ 6 - 74
backend/tests/unit/test_slice_request_schema.py

@@ -6,7 +6,7 @@ normalisation that lets the route handler ignore the difference.
 import pytest
 from pydantic import ValidationError
 
-from backend.app.schemas.slicer import PresetRef, SliceBundleSpec, SliceRequest
+from backend.app.schemas.slicer import PresetRef, SliceRequest
 
 
 class TestLegacyBareIntegerShape:
@@ -144,79 +144,11 @@ class TestFilamentPresetsList:
         assert [r.id for r in req.filament_presets] == ["slot1", "slot2", "slot3"]
 
 
-class TestBundleDispatchShape:
-    """When SliceRequest.bundle is set, the dispatcher picks the JSON
-    triplet from a sidecar-side bundle by name and PresetRef resolution
-    is skipped entirely. Validator must accept "bundle alone" without
-    flagging missing presets."""
+class TestPresetsRequired:
+    """Without preset refs (and no legacy integer ids), the validator must
+    reject the request. Preset selection is mandatory now that bundle mode
+    is gone."""
 
-    def test_bundle_alone_validates(self):
-        req = SliceRequest(
-            bundle=SliceBundleSpec(
-                bundle_id="abc123def456abcd",
-                printer_name="# Bambu Lab H2D 0.4 nozzle",
-                process_name="# 0.20mm Standard @BBL H2D",
-                filament_names=["# Bambu PLA Basic @BBL H2D"],
-            ),
-        )
-        # PresetRef fields are absent; that's fine in bundle mode.
-        assert req.bundle is not None
-        assert req.printer_preset is None
-        assert req.process_preset is None
-        assert req.filament_presets == []
-
-    def test_bundle_with_filament_list_preserves_order(self):
-        req = SliceRequest(
-            bundle=SliceBundleSpec(
-                bundle_id="abc",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["red", "blue", "green"],
-            ),
-        )
-        assert req.bundle.filament_names == ["red", "blue", "green"]
-
-    def test_bundle_rejects_empty_filament_list(self):
-        with pytest.raises(ValidationError):
-            SliceBundleSpec(
-                bundle_id="abc",
-                printer_name="P",
-                process_name="Q",
-                filament_names=[],
-            )
-
-    def test_bundle_rejects_empty_id(self):
-        with pytest.raises(ValidationError):
-            SliceBundleSpec(
-                bundle_id="",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["F"],
-            )
-
-    def test_no_bundle_no_presets_still_rejected(self):
-        # Dropping the bundle escape-hatch must not bypass the existing
-        # presets-required check.
+    def test_empty_request_rejected(self):
         with pytest.raises(ValidationError):
             SliceRequest()
-
-    def test_bundle_with_presets_keeps_both_fields(self):
-        # Sending both is allowed (validator accepts the bundle and skips
-        # preset normalisation) — the dispatch picks bundle on the route
-        # side. Confirms the validator doesn't reject overlapping intent
-        # so a future client that wants to record the legacy presets
-        # alongside doesn't fail validation.
-        req = SliceRequest(
-            printer_preset=PresetRef(source="standard", id="X1C"),
-            process_preset=PresetRef(source="standard", id="0.20"),
-            filament_presets=[PresetRef(source="standard", id="PLA")],
-            bundle=SliceBundleSpec(
-                bundle_id="abc",
-                printer_name="P",
-                process_name="Q",
-                filament_names=["F"],
-            ),
-        )
-        assert req.bundle is not None
-        # Presets stay populated; dispatch ignores them when bundle is set.
-        assert req.printer_preset is not None

+ 0 - 211
backend/tests/unit/test_slicer_presets.py

@@ -704,217 +704,6 @@ class TestResolveSlicerApiUrl:
         assert url is None
 
 
-class TestBundleRoutes:
-    """Route-level coverage for the bundle proxy endpoints. Each route
-    resolves the sidecar URL via _resolve_slicer_api_url, then proxies the
-    operation through SlicerApiService. We mock both pieces so we can pin
-    the HTTP-status mapping (sidecar input error → 400, BundleNotFoundError
-    → 404, unreachable → 503) without spinning up a sidecar.
-    """
-
-    SAMPLE_SUMMARY = sp.BundleSummary(
-        id="abc123def456abcd",
-        printer_preset_name="# Bambu Lab H2D 0.4 nozzle",
-        printer=["# Bambu Lab H2D 0.4 nozzle"],
-        process=["# 0.20mm Standard @BBL H2D"],
-        filament=["# Bambu PLA Basic @BBL H2D"],
-        version="02.06.00.50",
-    )
-
-    def _patched_service(self, **methods) -> MagicMock:
-        """Build a SlicerApiService mock that supports `async with` and
-        exposes the bundle methods via AsyncMock per the override dict."""
-        svc = MagicMock()
-        svc.__aenter__ = AsyncMock(return_value=svc)
-        svc.__aexit__ = AsyncMock(return_value=False)
-        for name, mock in methods.items():
-            setattr(svc, name, mock)
-        return svc
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_happy_path(self):
-        from io import BytesIO
-
-        from fastapi import UploadFile
-
-        svc = self._patched_service(
-            import_bundle=AsyncMock(return_value=self.SAMPLE_SUMMARY),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-        ):
-            file = UploadFile(filename="H2D.bbscfg", file=BytesIO(b"PK\x03\x04"))
-            result = await sp.import_slicer_bundle(file=file, db=MagicMock(), _=None)
-        assert result["id"] == "abc123def456abcd"
-        assert result["printer"] == ["# Bambu Lab H2D 0.4 nozzle"]
-        svc.import_bundle.assert_awaited_once()
-        kwargs = svc.import_bundle.await_args.kwargs
-        assert kwargs["filename"] == "H2D.bbscfg"
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_no_sidecar_returns_503(self):
-        from io import BytesIO
-
-        from fastapi import HTTPException, UploadFile
-
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value=None)),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.import_slicer_bundle(
-                file=UploadFile(filename="x.bbscfg", file=BytesIO(b"x")),
-                db=MagicMock(),
-                _=None,
-            )
-        assert exc.value.status_code == 503
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_empty_file_returns_400(self):
-        from io import BytesIO
-
-        from fastapi import HTTPException, UploadFile
-
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.import_slicer_bundle(
-                file=UploadFile(filename="x.bbscfg", file=BytesIO(b"")),
-                db=MagicMock(),
-                _=None,
-            )
-        assert exc.value.status_code == 400
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_sidecar_400_passes_through(self, caplog):
-        from io import BytesIO
-
-        from fastapi import HTTPException, UploadFile
-
-        svc = self._patched_service(
-            import_bundle=AsyncMock(side_effect=sp.SlicerInputError("bad zip")),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-            caplog.at_level("WARNING", logger="backend.app.api.routes.slicer_presets"),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.import_slicer_bundle(
-                file=UploadFile(filename="x.bbscfg", file=BytesIO(b"x")),
-                db=MagicMock(),
-                _=None,
-            )
-        assert exc.value.status_code == 400
-        # #1312: the sidecar's reject reason MUST land in the log so it
-        # ends up in support bundles without us having to ask reporters
-        # to copy the FE toast.
-        assert any("bad zip" in r.message for r in caplog.records)
-        assert any("x.bbscfg" in r.message for r in caplog.records)
-
-    @pytest.mark.asyncio
-    async def test_import_bundle_sidecar_unreachable_returns_503(self):
-        from io import BytesIO
-
-        from fastapi import HTTPException, UploadFile
-
-        svc = self._patched_service(
-            import_bundle=AsyncMock(side_effect=sp.SlicerApiUnavailableError("offline")),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.import_slicer_bundle(
-                file=UploadFile(filename="x.bbscfg", file=BytesIO(b"x")),
-                db=MagicMock(),
-                _=None,
-            )
-        assert exc.value.status_code == 503
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_happy_path(self):
-        svc = self._patched_service(
-            list_bundles=AsyncMock(return_value=[self.SAMPLE_SUMMARY]),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-        ):
-            result = await sp.list_slicer_bundles(db=MagicMock(), _=None)
-        assert len(result) == 1
-        assert result[0]["id"] == "abc123def456abcd"
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_no_sidecar_returns_empty(self):
-        # Differs from import: list returns [] instead of 503 so the
-        # SliceModal still renders cleanly when no sidecar is configured
-        # (matches bundled-tier behaviour above).
-        with patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value=None)):
-            result = await sp.list_slicer_bundles(db=MagicMock(), _=None)
-        assert result == []
-
-    @pytest.mark.asyncio
-    async def test_list_bundles_sidecar_unreachable_returns_503(self):
-        from fastapi import HTTPException
-
-        svc = self._patched_service(
-            list_bundles=AsyncMock(side_effect=sp.SlicerApiUnavailableError("offline")),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.list_slicer_bundles(db=MagicMock(), _=None)
-        assert exc.value.status_code == 503
-
-    @pytest.mark.asyncio
-    async def test_get_bundle_404(self):
-        from fastapi import HTTPException
-
-        svc = self._patched_service(
-            get_bundle=AsyncMock(side_effect=sp.BundleNotFoundError("not found")),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.get_slicer_bundle("missing", db=MagicMock(), _=None)
-        assert exc.value.status_code == 404
-
-    @pytest.mark.asyncio
-    async def test_delete_bundle_204(self):
-        # delete returns None on success; FastAPI sends 204 because the route
-        # declares status_code=204.
-        svc = self._patched_service(delete_bundle=AsyncMock(return_value=None))
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-        ):
-            result = await sp.delete_slicer_bundle("abc", db=MagicMock(), _=None)
-        assert result is None
-        svc.delete_bundle.assert_awaited_once_with("abc")
-
-    @pytest.mark.asyncio
-    async def test_delete_bundle_404(self):
-        from fastapi import HTTPException
-
-        svc = self._patched_service(
-            delete_bundle=AsyncMock(side_effect=sp.BundleNotFoundError("not found")),
-        )
-        with (
-            patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
-            patch.object(sp, "SlicerApiService", return_value=svc),
-            pytest.raises(HTTPException) as exc,
-        ):
-            await sp.delete_slicer_bundle("missing", db=MagicMock(), _=None)
-        assert exc.value.status_code == 404
-
-
 class TestParseCompatiblePrinters:
     """``compatible_printers`` exposed for local process / filament presets so
     the SliceModal can filter the dropdowns by the selected printer (#1325)."""

+ 3 - 175
frontend/src/__tests__/components/SliceModal.test.tsx

@@ -26,7 +26,6 @@ vi.mock('../../api/client', () => ({
     getArchivePlates: vi.fn(),
     getLibraryFileFilamentRequirements: vi.fn(),
     getArchiveFilamentRequirements: vi.fn(),
-    listSlicerBundles: vi.fn(),
     getSettings: vi.fn().mockResolvedValue({}),
     updateSettings: vi.fn().mockResolvedValue({}),
   },
@@ -41,7 +40,6 @@ const mockApi = api as unknown as {
   getArchivePlates: ReturnType<typeof vi.fn>;
   getLibraryFileFilamentRequirements: ReturnType<typeof vi.fn>;
   getArchiveFilamentRequirements: ReturnType<typeof vi.fn>;
-  listSlicerBundles: ReturnType<typeof vi.fn>;
 };
 
 function makeUnified(overrides: Partial<UnifiedPresetsResponse> = {}): UnifiedPresetsResponse {
@@ -123,10 +121,6 @@ describe('SliceModal', () => {
       plate_id: 1,
       filaments: [],
     });
-    // Default: no bundles imported. Bundle-tier tests override this with a
-    // populated array; everything else inherits the empty default so the
-    // modal renders the original (preset-only) layout.
-    mockApi.listSlicerBundles.mockResolvedValue([]);
   });
 
   it('auto-selects the highest-priority tier per slot on first load', async () => {
@@ -836,9 +830,9 @@ describe('SliceModal', () => {
 
   // Cross-printer re-slicing is a normal, supported operation as of
   // 2026-05-20 (Step 0 empirical test: sidecar overrides printer / process
-  // / bed / kinematics from the picked bundle, producing valid target-
-  // printer G-code). No banner, no warning — the picker UI already shows
-  // which printer the user picked, and that's enough.
+  // / bed / kinematics from the picked profile triplet, producing valid
+  // target-printer G-code). No banner, no warning — the picker UI already
+  // shows which printer the user picked, and that's enough.
   it('does not surface any cross-printer banner and keeps Slice enabled when models differ', async () => {
     mockApi.getLibraryFilePlates.mockResolvedValue({
       file_id: 100,
@@ -1024,170 +1018,4 @@ describe('SliceModal', () => {
     });
   });
 
-  // -------------------------------------------------------------------------
-  // Bundle tier — picking an imported .bbscfg replaces the cloud/local/standard
-  // dropdown set with bundle-scoped pickers and routes the slice through the
-  // backend's bundle dispatch shape (no PresetRefs in the body).
-  // -------------------------------------------------------------------------
-
-  describe('Bundle tier', () => {
-    const sampleBundle = {
-      id: 'abc123def456abcd',
-      printer_preset_name: '# Bambu Lab H2D 0.4 nozzle',
-      printer: ['# Bambu Lab H2D 0.4 nozzle'],
-      process: [
-        '# 0.20mm Standard @BBL H2D',
-        '# 0.16mm Standard @BBL H2D',
-      ],
-      filament: [
-        '# Bambu PLA Basic @BBL H2D',
-        '# Bambu PETG HF @BBL H2D 0.4 nozzle',
-      ],
-      version: '02.06.00.50',
-    };
-
-    it('hides the bundle picker when no bundles are imported', async () => {
-      // Default beforeEach already returns []; assert the picker isn't
-      // rendered so users without bundles see the original layout.
-      renderWithTracker({
-        source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
-        onClose: vi.fn(),
-      });
-      await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
-      expect(screen.queryByText(/slicer bundle/i)).toBeNull();
-    });
-
-    it('renders the bundle picker when at least one bundle is imported', async () => {
-      mockApi.listSlicerBundles.mockResolvedValue([sampleBundle]);
-      renderWithTracker({
-        source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
-        onClose: vi.fn(),
-      });
-      await waitFor(() =>
-        expect(screen.getByText(/slicer bundle/i)).toBeDefined(),
-      );
-      // The bundle option is in the dropdown.
-      const bundleSelect = screen.getAllByRole('combobox')[0] as HTMLSelectElement;
-      expect(
-        Array.from(bundleSelect.options).map((o) => o.textContent),
-      ).toContain('# Bambu Lab H2D 0.4 nozzle');
-    });
-
-    it('replaces preset dropdowns with bundle-scoped pickers when a bundle is selected', async () => {
-      mockApi.listSlicerBundles.mockResolvedValue([sampleBundle]);
-      renderWithTracker({
-        source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
-        onClose: vi.fn(),
-      });
-      await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
-
-      const user = userEvent.setup();
-      const selects = screen.getAllByRole('combobox') as HTMLSelectElement[];
-      // First select is the bundle picker (new top-of-modal dropdown).
-      await user.selectOptions(selects[0], sampleBundle.id);
-
-      // Wait for the bundle-mode UI to take over: process options should
-      // now reflect the bundle's process names.
-      await waitFor(() => {
-        expect(
-          screen.getByText('# 0.20mm Standard @BBL H2D'),
-        ).toBeDefined();
-      });
-
-      // The static printer label shows the bundle's printer. Both the
-      // <option> in the bundle picker and the read-only <div> below
-      // contain this text, so use getAllByText.
-      const printerNameMatches = screen.getAllByText('# Bambu Lab H2D 0.4 nozzle');
-      expect(printerNameMatches.length).toBeGreaterThanOrEqual(2);
-
-      // Cloud/local/standard preset names from the original tier no longer
-      // appear in the visible dropdowns (the bundle replaced them).
-      const visibleSelects = screen.getAllByRole('combobox') as HTMLSelectElement[];
-      const allOptionTexts = visibleSelects.flatMap((sel) =>
-        Array.from(sel.options).map((o) => o.textContent ?? ''),
-      );
-      // Cloud printer name shouldn't be in any visible dropdown anymore.
-      expect(allOptionTexts).not.toContain('My Custom X1C');
-    });
-
-    it('submits bundle dispatch shape (no PresetRefs) when a bundle is selected', async () => {
-      mockApi.listSlicerBundles.mockResolvedValue([sampleBundle]);
-      mockApi.sliceLibraryFile.mockResolvedValue({
-        job_id: 99,
-        status: 'pending',
-        status_url: '/api/v1/slice-jobs/99',
-      });
-
-      renderWithTracker({
-        source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
-        onClose: vi.fn(),
-      });
-      await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
-
-      const user = userEvent.setup();
-      const selects = screen.getAllByRole('combobox') as HTMLSelectElement[];
-      await user.selectOptions(selects[0], sampleBundle.id);
-
-      // Wait for bundle-mode dropdowns to render.
-      await waitFor(() =>
-        expect(screen.getByText('# 0.20mm Standard @BBL H2D')).toBeDefined(),
-      );
-      await user.click(screen.getByRole('button', { name: /^Slice$/ }));
-
-      await waitFor(() => {
-        const [fileId, body] = mockApi.sliceLibraryFile.mock.calls[0];
-        expect(fileId).toBe(100);
-        expect(body.bundle).toEqual({
-          bundle_id: sampleBundle.id,
-          printer_name: '# Bambu Lab H2D 0.4 nozzle',
-          process_name: '# 0.20mm Standard @BBL H2D',
-          filament_names: ['# Bambu PLA Basic @BBL H2D'],
-        });
-        // The preset triplet must NOT be in the body — bundle dispatch
-        // skips PresetRef resolution entirely on the backend.
-        expect(body.printer_preset).toBeUndefined();
-        expect(body.process_preset).toBeUndefined();
-        expect(body.filament_presets).toBeUndefined();
-      });
-    });
-
-    it('switching back to "None" restores the preset triplet path', async () => {
-      mockApi.listSlicerBundles.mockResolvedValue([sampleBundle]);
-      mockApi.sliceLibraryFile.mockResolvedValue({
-        job_id: 100,
-        status: 'pending',
-        status_url: '/api/v1/slice-jobs/100',
-      });
-
-      renderWithTracker({
-        source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
-        onClose: vi.fn(),
-      });
-      await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
-
-      const user = userEvent.setup();
-      const bundleSelect = screen.getAllByRole('combobox')[0] as HTMLSelectElement;
-      await user.selectOptions(bundleSelect, sampleBundle.id);
-      await waitFor(() =>
-        expect(screen.getByText('# 0.20mm Standard @BBL H2D')).toBeDefined(),
-      );
-
-      // Flip back to None.
-      await user.selectOptions(bundleSelect, '');
-      await waitFor(() => {
-        const selects = screen.getAllByRole('combobox') as HTMLSelectElement[];
-        // After de-selecting bundle, the printer dropdown's first option
-        // should be one of the original cloud/local/standard names.
-        const printerOptions = Array.from(selects[1].options).map((o) => o.textContent);
-        expect(printerOptions).toContain('My Custom X1C');
-      });
-
-      await user.click(screen.getByRole('button', { name: /^Slice$/ }));
-      await waitFor(() => {
-        const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
-        expect(body.bundle).toBeUndefined();
-        expect(body.printer_preset).toBeDefined();
-      });
-    });
-  });
 });

+ 0 - 214
frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx

@@ -1,214 +0,0 @@
-/**
- * Tests for the SlicerBundlesPanel — Settings panel for managing
- * BambuStudio Printer Preset Bundles (.bbscfg) on the slicer sidecar.
- *
- * Coverage:
- *  - Empty state when the sidecar has no bundles imported yet.
- *  - List rendering with summary line (process / filament counts).
- *  - Upload happy path → success toast + list invalidation.
- *  - Upload error → error toast.
- *  - Delete with confirmation → success toast + list invalidation.
- *  - Delete error → error toast.
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, fireEvent, waitFor } from '@testing-library/react';
-import { render } from '../utils';
-import { api } from '../../api/client';
-import { SlicerBundlesPanel } from '../../components/SlicerBundlesPanel';
-
-vi.mock('../../api/client', async () => {
-  const actual: typeof import('../../api/client') = await vi.importActual(
-    '../../api/client',
-  );
-  return {
-    ...actual,
-    api: {
-      ...actual.api,
-      listSlicerBundles: vi.fn(),
-      importSlicerBundle: vi.fn(),
-      deleteSlicerBundle: vi.fn(),
-    },
-    getAuthToken: vi.fn(() => null),
-  };
-});
-
-const SAMPLE_BUNDLE = {
-  id: 'abc123def456abcd',
-  printer_preset_name: '# Bambu Lab H2D 0.4 nozzle',
-  printer: ['# Bambu Lab H2D 0.4 nozzle'],
-  process: [
-    '# 0.20mm Standard @BBL H2D',
-    '# 0.16mm Standard @BBL H2D',
-  ],
-  filament: [
-    '# Bambu PLA Basic @BBL H2D',
-    '# Bambu PETG HF @BBL H2D 0.4 nozzle',
-    '# Bambu ABS @BBL H2D',
-  ],
-  version: '02.06.00.50',
-};
-
-beforeEach(() => {
-  vi.clearAllMocks();
-});
-
-describe('SlicerBundlesPanel — empty state', () => {
-  it('renders the empty-state message when no bundles exist', async () => {
-    vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([]);
-
-    render(<SlicerBundlesPanel />);
-
-    await waitFor(() =>
-      expect(api.listSlicerBundles).toHaveBeenCalled(),
-    );
-    expect(
-      await screen.findByText(/no bundles imported yet/i),
-    ).toBeInTheDocument();
-  });
-});
-
-describe('SlicerBundlesPanel — list rendering', () => {
-  it('renders bundle name + summary (process and filament counts)', async () => {
-    vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([SAMPLE_BUNDLE]);
-
-    render(<SlicerBundlesPanel />);
-
-    expect(
-      await screen.findByText('# Bambu Lab H2D 0.4 nozzle'),
-    ).toBeInTheDocument();
-    // Summary should reflect 2 process + 3 filament from the fixture.
-    expect(
-      await screen.findByText(/2 process · 3 filament/i),
-    ).toBeInTheDocument();
-    // Version suffix appended after the summary.
-    expect(screen.getByText(/v02\.06\.00\.50/)).toBeInTheDocument();
-  });
-});
-
-describe('SlicerBundlesPanel — upload flow', () => {
-  it('imports a selected file and refreshes the list on success', async () => {
-    // First listing call returns empty so the test can detect the post-import
-    // re-fetch (second call) returning the new bundle.
-    vi.mocked(api.listSlicerBundles)
-      .mockResolvedValueOnce([])
-      .mockResolvedValueOnce([SAMPLE_BUNDLE]);
-    vi.mocked(api.importSlicerBundle).mockResolvedValueOnce(SAMPLE_BUNDLE);
-
-    const { container } = render(<SlicerBundlesPanel />);
-
-    // The file input is hidden (display: none for styling); grab it directly.
-    const fileInput = container.querySelector(
-      'input[type="file"]',
-    ) as HTMLInputElement;
-    expect(fileInput).toBeTruthy();
-
-    const file = new File([new Uint8Array([0x50, 0x4b, 0x03, 0x04])], 'H2D.bbscfg', {
-      type: 'application/zip',
-    });
-
-    fireEvent.change(fileInput, { target: { files: [file] } });
-
-    await waitFor(() =>
-      expect(api.importSlicerBundle).toHaveBeenCalledWith(file),
-    );
-    // After the success, the list call should fire a second time (cache
-    // invalidation by react-query).
-    await waitFor(() =>
-      expect(api.listSlicerBundles).toHaveBeenCalledTimes(2),
-    );
-    // The newly imported bundle should now be visible in the list.
-    expect(
-      await screen.findByText('# Bambu Lab H2D 0.4 nozzle'),
-    ).toBeInTheDocument();
-  });
-
-  it('shows an error and does not refresh on upload failure', async () => {
-    vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([]);
-    vi.mocked(api.importSlicerBundle).mockRejectedValueOnce(
-      new Error('Bundle is missing bundle_structure.json'),
-    );
-
-    const { container } = render(<SlicerBundlesPanel />);
-
-    await waitFor(() => expect(api.listSlicerBundles).toHaveBeenCalled());
-
-    const fileInput = container.querySelector(
-      'input[type="file"]',
-    ) as HTMLInputElement;
-    const file = new File([new Uint8Array([0])], 'bad.bbscfg', {
-      type: 'application/zip',
-    });
-    fireEvent.change(fileInput, { target: { files: [file] } });
-
-    await waitFor(() =>
-      expect(api.importSlicerBundle).toHaveBeenCalled(),
-    );
-    // Listing should NOT be re-called on failure — only the initial load.
-    expect(api.listSlicerBundles).toHaveBeenCalledTimes(1);
-    // Empty state still showing.
-    expect(
-      screen.getByText(/no bundles imported yet/i),
-    ).toBeInTheDocument();
-  });
-});
-
-describe('SlicerBundlesPanel — delete flow', () => {
-  it('deletes a bundle after confirmation and refreshes the list', async () => {
-    vi.mocked(api.listSlicerBundles)
-      .mockResolvedValueOnce([SAMPLE_BUNDLE])
-      .mockResolvedValueOnce([]);
-    vi.mocked(api.deleteSlicerBundle).mockResolvedValueOnce(undefined);
-
-    render(<SlicerBundlesPanel />);
-
-    // Wait for the bundle to render.
-    await screen.findByText('# Bambu Lab H2D 0.4 nozzle');
-
-    // Click the trash button (aria-label="Delete").
-    fireEvent.click(screen.getByRole('button', { name: /delete/i }));
-
-    // ConfirmModal should appear with the bundle name in the message.
-    const confirmMessage = await screen.findByText(
-      /Slice requests that reference "# Bambu Lab H2D 0.4 nozzle" will fail/i,
-    );
-    expect(confirmMessage).toBeInTheDocument();
-
-    // The modal renders its own "Delete" button — there are now two buttons
-    // matching /delete/i. Click the one inside the dialog (last in document
-    // order, since the modal portal renders after the panel).
-    const deleteButtons = screen.getAllByRole('button', { name: /delete/i });
-    fireEvent.click(deleteButtons[deleteButtons.length - 1]);
-
-    await waitFor(() =>
-      expect(api.deleteSlicerBundle).toHaveBeenCalledWith(
-        'abc123def456abcd',
-      ),
-    );
-    // Cache invalidation should re-fire the list query.
-    await waitFor(() =>
-      expect(api.listSlicerBundles).toHaveBeenCalledTimes(2),
-    );
-  });
-
-  it('keeps the bundle in the list when the user cancels the delete dialog', async () => {
-    vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([SAMPLE_BUNDLE]);
-
-    render(<SlicerBundlesPanel />);
-
-    await screen.findByText('# Bambu Lab H2D 0.4 nozzle');
-    fireEvent.click(screen.getByRole('button', { name: /delete/i }));
-
-    // Cancel by clicking the "Cancel" button on the ConfirmModal.
-    const cancelButton = await screen.findByRole('button', { name: /cancel/i });
-    fireEvent.click(cancelButton);
-
-    // Delete API never called, list never re-fetched.
-    expect(api.deleteSlicerBundle).not.toHaveBeenCalled();
-    expect(api.listSlicerBundles).toHaveBeenCalledTimes(1);
-    // Bundle still rendered.
-    expect(
-      screen.getByText('# Bambu Lab H2D 0.4 nozzle'),
-    ).toBeInTheDocument();
-  });
-});

+ 16 - 141
frontend/src/__tests__/utils/slicerPrinterMatch.test.ts

@@ -4,7 +4,6 @@ import {
   matchesPrinterModelSuffix,
   presetCompatibility,
   EMPTY_COMPATIBILITY_INDEX,
-  type CompatibilityBundle,
 } from '../../utils/slicerPrinterMatch';
 
 const X1C = 'Bambu Lab X1 Carbon 0.4 nozzle';
@@ -31,60 +30,9 @@ const PRINTER_MODELS: Record<string, string> = {
   'Bambu Lab X2D': 'X2D',
 };
 
-// Two uploaded bundles, one per printer — the ground truth all matching
-// is derived from. Note P2S: a model the old hard-coded list never knew
-// about, now covered purely because its bundle was uploaded (#1325).
-const BUNDLES: CompatibilityBundle[] = [
-  {
-    printer_preset_name: X1C,
-    process: ['0.20mm Standard @BBL X1C', '0.20mm Strength @BBL X1C'],
-    filament: ['Bambu PLA Basic @BBL X1C'],
-  },
-  {
-    printer_preset_name: P2S,
-    process: ['0.20mm Standard @BBL P2S', '0.16mm Standard @BBL P2S'],
-    filament: ['Bambu PLA Basic @BBL P2S'],
-  },
-];
-
 describe('buildCompatibilityIndex', () => {
-  it('maps each preset name to the printers whose bundles ship it', () => {
-    const index = buildCompatibilityIndex(BUNDLES, PRINTER_MODELS);
-    expect([...(index.process.get('0.20mm Standard @BBL X1C') ?? [])]).toEqual([X1C]);
-    expect([...(index.process.get('0.16mm Standard @BBL P2S') ?? [])]).toEqual([P2S]);
-    expect([...(index.filament.get('Bambu PLA Basic @BBL P2S') ?? [])]).toEqual([P2S]);
-  });
-
-  it('unions printers when several bundles ship the same preset name', () => {
-    const shared = '0.20mm Standard';
-    const index = buildCompatibilityIndex(
-      [
-        { printer_preset_name: X1C, process: [shared], filament: [] },
-        { printer_preset_name: P2S, process: [shared], filament: [] },
-      ],
-      PRINTER_MODELS,
-    );
-    expect(index.process.get(shared)).toEqual(new Set([X1C, P2S]));
-  });
-
-  it("strips BambuStudio's '# ' user-clone prefix so names compare equal", () => {
-    const index = buildCompatibilityIndex(
-      [{ printer_preset_name: X1C, process: ['# 0.20mm Custom'], filament: [] }],
-      PRINTER_MODELS,
-    );
-    expect(index.process.has('0.20mm Custom')).toBe(true);
-  });
-
-  it('skips bundles with no printer name', () => {
-    const index = buildCompatibilityIndex(
-      [{ printer_preset_name: '', process: ['Orphan Process'], filament: [] }],
-      PRINTER_MODELS,
-    );
-    expect(index.process.size).toBe(0);
-  });
-
   it('inverts the printer-model registry into short-code → display fragment', () => {
-    const index = buildCompatibilityIndex([], PRINTER_MODELS);
+    const index = buildCompatibilityIndex(PRINTER_MODELS);
     expect(index.bambuModelByShortCode.X1C).toBe('X1 Carbon');
     expect(index.bambuModelByShortCode.P2S).toBe('P2S');
     expect(index.bambuModelByShortCode['A1 Mini']).toBe('A1 Mini');
@@ -92,18 +40,13 @@ describe('buildCompatibilityIndex', () => {
   });
 
   it('tolerates an empty printer-model registry (model fetch hasn\'t resolved yet)', () => {
-    const index = buildCompatibilityIndex(BUNDLES);
+    const index = buildCompatibilityIndex();
     expect(index.bambuModelByShortCode).toEqual({});
-    // Bundle matching still works on its own.
-    expect([...(index.process.get('0.20mm Standard @BBL X1C') ?? [])]).toEqual([X1C]);
   });
 });
 
 describe('presetCompatibility', () => {
-  const index = buildCompatibilityIndex(BUNDLES, PRINTER_MODELS);
-  // Bundle-free index used by the #1325 follow-up fallback tests: any match
-  // here must come from the @BBL name parse alone.
-  const namesOnlyIndex = buildCompatibilityIndex([], PRINTER_MODELS);
+  const index = buildCompatibilityIndex(PRINTER_MODELS);
 
   it('uses compatible_printers exactly when present (imported / local tier)', () => {
     const preset = { name: 'My Process', compatible_printers: [X1C] };
@@ -117,65 +60,12 @@ describe('presetCompatibility', () => {
     ).toBe('unknown');
   });
 
-  it('matches a preset shipped by the selected printer\'s bundle', () => {
-    expect(presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C, index)).toBe(
-      'match',
-    );
-    expect(
-      presetCompatibility({ name: 'Bambu PLA Basic @BBL P2S' }, 'filament', P2S, index),
-    ).toBe('match');
-  });
-
-  it('flags a preset whose bundle is for a different printer (the #1325 bug)', () => {
-    // X1C selected, but this process only ships in the P2S bundle.
-    expect(presetCompatibility({ name: '0.16mm Standard @BBL P2S' }, 'process', X1C, index)).toBe(
-      'mismatch',
-    );
-  });
-
-  it('falls back to @BBL name parsing when no bundle covers the preset (#1325 follow-up)', () => {
-    // No A1 bundle uploaded, but the preset's @BBL A1 tag is enough to
-    // resolve it: A1 ≠ X1C so it belongs in "Other printers".
-    expect(
-      presetCompatibility({ name: '0.20mm Standard @BBL A1' }, 'process', X1C, index),
-    ).toBe('mismatch');
-  });
-
-  it('falls back to @BBL name parsing when no bundles are imported at all', () => {
-    // Brand-new user, zero bundles, every preset would have been "unknown"
-    // under the bundle-only design — now resolves via the name suffix.
-    expect(
-      presetCompatibility(
-        { name: '0.20mm Standard @BBL X1C' },
-        'process',
-        X1C,
-        namesOnlyIndex,
-      ),
-    ).toBe('match');
-    expect(
-      presetCompatibility(
-        { name: '0.20mm Standard @BBL P2S' },
-        'process',
-        X1C,
-        namesOnlyIndex,
-      ),
-    ).toBe('mismatch');
-  });
-
   it('is unknown when no printer is selected', () => {
     expect(
       presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', null, index),
     ).toBe('unknown');
   });
 
-  it("matches across the '# ' user-clone prefix", () => {
-    const index2 = buildCompatibilityIndex(
-      [{ printer_preset_name: X1C, process: ['# 0.20mm Custom'], filament: [] }],
-      PRINTER_MODELS,
-    );
-    expect(presetCompatibility({ name: '0.20mm Custom' }, 'process', X1C, index2)).toBe('match');
-  });
-
   it('compatible_printers wins over @BBL even when the name suggests a different printer', () => {
     // Authoritative slicer declaration: this @BBL P2S preset has been
     // manually reassigned to X1C. The compatible_printers list must win.
@@ -184,7 +74,7 @@ describe('presetCompatibility', () => {
         { name: '0.20mm Standard @BBL P2S', compatible_printers: [X1C] },
         'process',
         X1C,
-        namesOnlyIndex,
+        index,
       ),
     ).toBe('match');
     expect(
@@ -192,30 +82,16 @@ describe('presetCompatibility', () => {
         { name: '0.20mm Standard @BBL P2S', compatible_printers: [X1C] },
         'process',
         P2S,
-        namesOnlyIndex,
+        index,
       ),
     ).toBe('mismatch');
   });
-
-  it('bundle index wins over @BBL when they disagree', () => {
-    // Hypothetical bundle that ships a P2S-tagged preset as compatible
-    // with the X1C printer too — bundle-as-ground-truth overrules the
-    // name-suffix inference.
-    const reassigned = buildCompatibilityIndex(
-      [{ printer_preset_name: X1C, process: ['0.20mm Standard @BBL P2S'], filament: [] }],
-      PRINTER_MODELS,
-    );
-    expect(
-      presetCompatibility({ name: '0.20mm Standard @BBL P2S' }, 'process', X1C, reassigned),
-    ).toBe('match');
-  });
 });
 
 // ─── #1325 follow-up: @BBL name fallback ──────────────────────────────────
 
-describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
-  // No bundles, but with the registry loaded — exactly the new-user shape.
-  const idx = buildCompatibilityIndex([], PRINTER_MODELS);
+describe('presetCompatibility — @BBL name fallback', () => {
+  const idx = buildCompatibilityIndex(PRINTER_MODELS);
 
   // Bambu's short codes vs the long forms in printer-preset names: the
   // entire reason the fallback needs a registry to consult.
@@ -235,8 +111,8 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
     ['0.20mm Standard @BBL H2D', 'Bambu Lab H2D 0.4 nozzle', 'match'],
     ['0.20mm Standard @BBL H2D', 'Bambu Lab H2D Pro 0.4 nozzle', 'mismatch'],
     ['0.20mm Standard @BBL H2D Pro', 'Bambu Lab H2D Pro 0.4 nozzle', 'match'],
-    // Models missing from the original hardcoded list (the #1325 bug),
-    // now resolved via the backend registry.
+    // Models the original hardcoded list missed, now resolved via the
+    // backend registry.
     ['Bambu PLA Basic @BBL P2S', P2S, 'match'],
     ['Bambu PLA Basic @BBL P2S', X1C, 'mismatch'],
     ['0.20mm Standard @BBL X2D', 'Bambu Lab X2D 0.4 nozzle', 'match'],
@@ -306,11 +182,11 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
   });
 
   it('still resolves @BBL when the registry has not loaded yet (raw-token only)', () => {
-    // EMPTY_COMPATIBILITY_INDEX = no bundles, no models — first paint of
-    // the SliceModal before the /slicer/printer-models fetch resolves.
-    // Short codes that match their printer-name fragment directly (P2S,
-    // H2D, etc.) still work; codes that differ in form (X1C vs "X1
-    // Carbon") gracefully fall through to 'unknown'.
+    // EMPTY_COMPATIBILITY_INDEX = no models — first paint of the SliceModal
+    // before the /slicer/printer-models fetch resolves. Short codes that
+    // match their printer-name fragment directly (P2S, H2D, etc.) still
+    // work; codes that differ in form (X1C vs "X1 Carbon") gracefully
+    // fall through to 'mismatch'.
     expect(
       presetCompatibility(
         { name: '0.20mm Standard @BBL P2S' },
@@ -341,8 +217,7 @@ describe('presetCompatibility — nozzle filtering on @BBL name fallback', () =>
   const X1C_04 = 'Bambu Lab X1 Carbon 0.4 nozzle';
   const X1C_06 = 'Bambu Lab X1 Carbon 0.6 nozzle';
   const X1C_08 = 'Bambu Lab X1 Carbon 0.8 nozzle';
-  // No bundles uploaded — exercise the @BBL fallback in isolation.
-  const index = buildCompatibilityIndex([], PRINTER_MODELS);
+  const index = buildCompatibilityIndex(PRINTER_MODELS);
 
   it('treats a no-suffix process as 0.4 (Bambu default) and matches a 0.4 printer', () => {
     expect(
@@ -461,7 +336,7 @@ describe('matchesPrinterModelSuffix (#1649)', () => {
 describe('presetCompatibility with Bambu cloud A1M rename (#1649)', () => {
   const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
   const A1 = 'Bambu Lab A1 0.4 nozzle';
-  const idx = buildCompatibilityIndex([], PRINTER_MODELS);
+  const idx = buildCompatibilityIndex(PRINTER_MODELS);
 
   it('matches a cloud preset using the new @BBL A1M suffix against an A1 Mini printer', () => {
     // The slicer-mirrored case: technopaw's report — A1 Mini cloud presets

+ 1 - 87
frontend/src/api/client.ts

@@ -1390,13 +1390,6 @@ export interface PresetRef {
   source: PresetSource;
   id: string;
 }
-export interface SliceBundleSpec {
-  bundle_id: string;
-  printer_name: string;
-  process_name: string;
-  // Per-slot filament names in plate order. Index 0 = slot 1, etc.
-  filament_names: string[];
-}
 export interface SliceRequest {
   printer_preset_id?: number;
   process_preset_id?: number;
@@ -1409,11 +1402,6 @@ export interface SliceRequest {
   // backend validator promotes a singular into a one-element list when this
   // is omitted, so legacy single-color clients keep working unchanged.
   filament_presets?: PresetRef[];
-  // Bundle dispatch: when set, the backend skips PresetRef resolution and
-  // picks the JSON triplet from a sidecar-stored .bbscfg by name. Mutually
-  // exclusive with the preset fields above (validator accepts both, but
-  // dispatch ignores the preset side when bundle is set).
-  bundle?: SliceBundleSpec;
   plate?: number;
   export_3mf?: boolean;
   // Build-plate override (#1337). When omitted, the slicer uses the process
@@ -1424,20 +1412,6 @@ export interface SliceRequest {
   bed_type?: string | null;
 }
 
-// GET /api/v1/slicer/bundles — Printer Preset Bundles imported from
-// BambuStudio's "File → Export → Export Preset Bundle" dialog. Each bundle
-// is a .bbscfg zip the user uploads once per printer, after which the
-// SliceModal can pick its inner presets by name (no re-upload per slice).
-// Backend: backend/app/api/routes/slicer_presets.py — bundle endpoints.
-export interface SlicerBundle {
-  id: string;
-  printer_preset_name: string;
-  printer: string[];
-  process: string[];
-  filament: string[];
-  version: string | null;
-}
-
 // GET /api/v1/slicer/presets — unified listing across cloud / local / standard.
 export type SlicerCloudStatus = 'ok' | 'not_authenticated' | 'expired' | 'unreachable';
 export interface UnifiedPreset {
@@ -1455,7 +1429,7 @@ export interface UnifiedPreset {
   // compatible with. Populated for the local tier (the slicer's own
   // `compatible_printers`); null for cloud / standard. The SliceModal filters
   // the process / filament dropdowns by the selected printer using this when
-  // present, and otherwise by the user's uploaded Slicer Bundles (#1325).
+  // present (#1325).
   compatible_printers?: string[] | null;
 }
 export interface UnifiedPresetsBySlot {
@@ -4254,27 +4228,10 @@ export const api = {
     archiveId: number,
     plateId?: number,
     requestId?: string,
-    // Optional bundle context: when supplied, the backend's preview slice
-    // (run for unsliced project files) uses slice_with_bundle so gram
-    // numbers reflect the same triplet the real print will use. All four
-    // fields must be set for the bundle path to engage; partial context
-    // falls back to the embedded-settings preview without erroring.
-    bundle?: {
-      bundle_id: string;
-      printer_name: string;
-      process_name: string;
-      filament_names: string[];
-    },
   ) => {
     const qs = new URLSearchParams();
     if (plateId !== undefined) qs.set('plate_id', String(plateId));
     if (requestId) qs.set('request_id', requestId);
-    if (bundle) {
-      qs.set('bundle_id', bundle.bundle_id);
-      qs.set('printer_name', bundle.printer_name);
-      qs.set('process_name', bundle.process_name);
-      qs.set('filament_names', bundle.filament_names.join(';'));
-    }
     return request<{
       archive_id: number;
       filename: string;
@@ -5886,24 +5843,10 @@ export const api = {
     fileId: number,
     plateId?: number,
     requestId?: string,
-    // Optional bundle context — see getArchiveFilamentRequirements above
-    // for the contract. Same shape so callers can share a builder helper.
-    bundle?: {
-      bundle_id: string;
-      printer_name: string;
-      process_name: string;
-      filament_names: string[];
-    },
   ) => {
     const qs = new URLSearchParams();
     if (plateId !== undefined) qs.set('plate_id', String(plateId));
     if (requestId) qs.set('request_id', requestId);
-    if (bundle) {
-      qs.set('bundle_id', bundle.bundle_id);
-      qs.set('printer_name', bundle.printer_name);
-      qs.set('process_name', bundle.process_name);
-      qs.set('filament_names', bundle.filament_names.join(';'));
-    }
     return request<{
       file_id: number;
       filename: string;
@@ -6040,35 +5983,6 @@ export const api = {
   getSlicerPrinterModels: () =>
     request<Record<string, string>>('/slicer/printer-models'),
 
-  // Slicer Bundles (.bbscfg) — Printer Preset Bundles imported from BambuStudio.
-  // Settings → Slicer Bundles uploads/lists/deletes; the SliceModal picks
-  // presets by name from a chosen bundle (separate follow-up).
-  listSlicerBundles: () =>
-    request<SlicerBundle[]>('/slicer/bundles'),
-  importSlicerBundle: (file: File) => {
-    // The /slicer/bundles upload accepts multipart with field name "file"
-    // (matches the FastAPI route's UploadFile parameter). Bypass `request`
-    // because it always JSON-stringifies the body — multipart needs the
-    // browser to set the boundary in the Content-Type header.
-    const fd = new FormData();
-    fd.append('file', file);
-    return fetch(`${API_BASE}/slicer/bundles`, {
-      method: 'POST',
-      headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {},
-      body: fd,
-    }).then(async (res) => {
-      if (!res.ok) {
-        const err = await res.json().catch(() => ({}));
-        throw new Error(err.detail || `HTTP ${res.status}`);
-      }
-      return res.json() as Promise<SlicerBundle>;
-    });
-  },
-  deleteSlicerBundle: (bundleId: string) =>
-    request<void>(`/slicer/bundles/${encodeURIComponent(bundleId)}`, {
-      method: 'DELETE',
-    }),
-
   // Local Presets (OrcaSlicer imports)
   getLocalPresets: () =>
     request<LocalPresetsResponse>('/local-presets/'),

+ 36 - 294
frontend/src/components/SliceModal.tsx

@@ -1,4 +1,4 @@
-import { Cloud, CloudOff, Cog, Loader2, Package, RefreshCw, X } from 'lucide-react';
+import { Cloud, CloudOff, Cog, Loader2, RefreshCw, X } from 'lucide-react';
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -6,10 +6,8 @@ import {
   api,
   type PresetRef,
   type PresetSource,
-  type SliceBundleSpec,
   type SliceJobProgress,
   type SliceRequest,
-  type SlicerBundle,
   type SlicerCloudStatus,
   type UnifiedPreset,
   type UnifiedPresetsBySlot,
@@ -323,14 +321,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // entry per AMS slot the plate uses. Pre-pick (effect below) initialises
   // each slot from the source plate's required (type, colour).
   const [filamentPresets, setFilamentPresets] = useState<(PresetRef | null)[]>([]);
-  // Bundle dispatch (alternative to the preset triplet). When non-null, the
-  // SliceModal hides the cloud/local/standard preset dropdowns and shows
-  // bundle-scoped pickers (process + per-slot filament from the chosen
-  // bundle's contents). Submit routes through the backend's bundle dispatch
-  // (`SliceRequest.bundle`) which skips PresetRef resolution.
-  const [selectedBundleId, setSelectedBundleId] = useState<string | null>(null);
-  const [bundleProcessName, setBundleProcessName] = useState<string | null>(null);
-  const [bundleFilamentNames, setBundleFilamentNames] = useState<(string | null)[]>([]);
   const [errorMessage, setErrorMessage] = useState<string | null>(null);
   // null = plate not yet picked (or single-plate / non-3MF — picker is skipped
   // and we'll backfill 1 at submit time). Set to a 1-indexed plate number once
@@ -456,46 +446,26 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
     }
   };
 
-  // Imported Printer Preset Bundles (.bbscfg). Empty list when no sidecar
-  // configured / no bundles imported yet; the bundle picker hides itself
-  // in that case so users without bundles see the original modal layout.
-  const bundlesQuery = useQuery({
-    queryKey: ['slicerBundles'],
-    queryFn: api.listSlicerBundles,
-    staleTime: 60_000,
-    enabled: !platesQuery.isLoading && !needsPlatePicker,
-    // Bundle listing is a hard 503 when the sidecar is offline; don't
-    // retry tight loops in that case.
-    retry: false,
-  });
   // Canonical Bambu printer-model registry — drives the @BBL <code> name
-  // fallback in slicerPrinterMatch when no slicer bundle covers a cloud /
-  // standard preset (#1325 follow-up). Long staleTime: the registry only
-  // changes across backend releases.
+  // fallback in slicerPrinterMatch for cloud / standard presets (#1325).
+  // Long staleTime: the registry only changes across backend releases.
   const printerModelsQuery = useQuery({
     queryKey: ['slicerPrinterModels'],
     queryFn: api.getSlicerPrinterModels,
     staleTime: Infinity,
   });
-  const selectedBundle: SlicerBundle | null = useMemo(() => {
-    if (!selectedBundleId || !bundlesQuery.data) return null;
-    return bundlesQuery.data.find((b) => b.id === selectedBundleId) ?? null;
-  }, [selectedBundleId, bundlesQuery.data]);
-  const isBundleMode = selectedBundle != null;
 
   // Selected-printer context for the process / filament filter (#1325).
   const selectedPrinterName = useMemo<string | null>(() => {
     if (!presetsQuery.data || !printerPreset) return null;
     return findPreset(presetsQuery.data, printerPreset, 'printer')?.name ?? null;
   }, [presetsQuery.data, printerPreset]);
-  // Compatibility ground truth: the user's uploaded Slicer Bundles plus the
-  // backend Bambu printer-model registry (#1325 + follow-up). The bundle
-  // path handles imported / custom presets; the registry-driven @BBL name
-  // fallback inside slicerPrinterMatch picks up cloud / standard presets
-  // for users who haven't uploaded bundles yet.
+  // Compatibility ground truth: the slicer's own `compatible_printers` list
+  // on local-imported presets, plus the @BBL <code> name fallback for cloud
+  // / standard presets via the backend Bambu printer-model registry.
   const compatIndex = useMemo<PrinterCompatibilityIndex>(
-    () => buildCompatibilityIndex(bundlesQuery.data ?? [], printerModelsQuery.data ?? {}),
-    [bundlesQuery.data, printerModelsQuery.data],
+    () => buildCompatibilityIndex(printerModelsQuery.data ?? {}),
+    [printerModelsQuery.data],
   );
 
   // Printer / process preset names the source 3MF was prepared with. The
@@ -563,35 +533,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
     });
   }, [presetsQuery.data, filamentSlots, selectedPrinterName, compatIndex]);
 
-  // Bundle-mode auto-pick: when the user picks a bundle (or the slot count
-  // changes after the picker is open), default the process to the bundle's
-  // first listed process and every filament slot to the bundle's first
-  // listed filament. Plain string match — bundles store delta files keyed
-  // by user preset name, no scoring needed since the user picks per-slot
-  // afterwards if the default is wrong.
-  useEffect(() => {
-    if (!selectedBundle) {
-      // Reset bundle picks when bundle is cleared so re-selection
-      // re-defaults rather than carrying stale values.
-      setBundleProcessName(null);
-      setBundleFilamentNames([]);
-      return;
-    }
-    setBundleProcessName((current) => {
-      // Preserve a manual pick if it still exists in the bundle; otherwise
-      // re-default. Same shape as the preset auto-pick effect above.
-      if (current && selectedBundle.process.includes(current)) return current;
-      return selectedBundle.process[0] ?? null;
-    });
-    setBundleFilamentNames((current) => {
-      if (current.length === filamentSlots.length && current.every((n) => n != null)) {
-        return current;
-      }
-      const fallback = selectedBundle.filament[0] ?? null;
-      return filamentSlots.map((_, i) => current[i] ?? fallback);
-    });
-  }, [selectedBundle, filamentSlots]);
-
   const enqueueMutation = useMutation({
     mutationFn: async (plate: number | null) => {
       const body = buildSliceBody(plate);
@@ -614,29 +555,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // is the 1-indexed plate number to slice, or ``null`` for STL / single-
   // plate 3MF sources where the field is omitted entirely.
   function buildSliceBody(plate: number | null): SliceRequest {
-    if (isBundleMode) {
-      if (
-        !selectedBundle ||
-        !bundleProcessName ||
-        bundleFilamentNames.length === 0 ||
-        bundleFilamentNames.some((n) => n == null)
-      ) {
-        throw new Error(t('slice.bundleAllRequired'));
-      }
-      const bundleSpec: SliceBundleSpec = {
-        bundle_id: selectedBundle.id,
-        printer_name: selectedBundle.printer[0] ?? selectedBundle.printer_preset_name,
-        process_name: bundleProcessName,
-        filament_names: bundleFilamentNames as string[],
-      };
-      return {
-        bundle: bundleSpec,
-        ...(plate != null ? { plate } : {}),
-        // Bed-type override (#1337) also flows through the bundle path —
-        // the sidecar forwards `bedType` as --curr_bed_type to the CLI.
-        ...(bedType != null ? { bed_type: bedType } : {}),
-      };
-    }
     if (
       !printerPreset ||
       !processPreset ||
@@ -659,17 +577,12 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // Slice button stays disabled until the preview slice / embedded-metadata
   // read has succeeded (filamentReqsQuery.isSuccess) and every filament slot
   // has a picked profile.
-  const isReady = isBundleMode
-    ? selectedBundle != null &&
-      bundleProcessName != null &&
-      filamentReqsQuery.isSuccess &&
-      bundleFilamentNames.length > 0 &&
-      bundleFilamentNames.every((n) => n != null)
-    : printerPreset != null &&
-      processPreset != null &&
-      filamentReqsQuery.isSuccess &&
-      filamentPresets.length > 0 &&
-      filamentPresets.every((r) => r != null);
+  const isReady =
+    printerPreset != null &&
+    processPreset != null &&
+    filamentReqsQuery.isSuccess &&
+    filamentPresets.length > 0 &&
+    filamentPresets.every((r) => r != null);
   const isEnqueuing = enqueueMutation.isPending;
   const totalPlateCount = platesQuery.data?.plates?.length ?? 0;
   const canSliceAll = isMultiPlate && totalPlateCount > 1 && !needsPlatePicker;
@@ -769,71 +682,27 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                   status === 'ok' (returns null in that case), but the Refresh
                   button stays visible regardless so users can pick up cloud /
                   bundled changes even when sign-in is healthy. */}
-              {/* Bundle picker — only renders when at least one .bbscfg has
-                  been imported via Settings → Slicer Bundles. Lets the user
-                  trade the cloud/local/standard tier for a single curated
-                  triplet from a previously-uploaded BambuStudio bundle. */}
-              {bundlesQuery.data && bundlesQuery.data.length > 0 && (
-                <BundlePicker
-                  bundles={bundlesQuery.data}
-                  selectedId={selectedBundleId}
-                  onChange={setSelectedBundleId}
-                  disabled={isEnqueuing}
-                />
-              )}
-              {/* Preset triplet — hidden when a bundle is selected so the
-                  user only sees one tier at a time. The bundle's process +
-                  filament dropdowns render below in their stead. */}
-              {!isBundleMode && (
-                <>
-                  <PresetDropdown
-                    label={t('slice.printer')}
-                    slot="printer"
-                    data={presetsQuery.data}
-                    value={printerPreset}
-                    onChange={setPrinterPreset}
-                    disabled={isEnqueuing}
-                  />
-                  <PresetDropdown
-                    label={t('slice.process')}
-                    slot="process"
-                    data={presetsQuery.data}
-                    value={processPreset}
-                    onChange={setProcessPreset}
-                    disabled={isEnqueuing}
-                    selectedPrinterName={selectedPrinterName}
-                    compatIndex={compatIndex}
-                  />
-                </>
-              )}
-              {isBundleMode && selectedBundle && (
-                <>
-                  {/* Bundle's printer is implicit (each .bbscfg has exactly
-                      one). Show it as a read-only label so the user can
-                      verify the printer they're slicing for. */}
-                  <div>
-                    <label className="block text-sm text-bambu-gray mb-1">
-                      {t('slice.printer')}
-                    </label>
-                    <div className="px-3 py-2 rounded-md bg-bambu-dark/40 border border-bambu-dark-tertiary text-white text-sm">
-                      {selectedBundle.printer_preset_name}
-                    </div>
-                  </div>
-                  <BundleStringDropdown
-                    label={t('slice.process')}
-                    options={selectedBundle.process}
-                    value={bundleProcessName}
-                    onChange={setBundleProcessName}
-                    disabled={isEnqueuing}
-                  />
-                </>
-              )}
+              <PresetDropdown
+                label={t('slice.printer')}
+                slot="printer"
+                data={presetsQuery.data}
+                value={printerPreset}
+                onChange={setPrinterPreset}
+                disabled={isEnqueuing}
+              />
+              <PresetDropdown
+                label={t('slice.process')}
+                slot="process"
+                data={presetsQuery.data}
+                value={processPreset}
+                onChange={setProcessPreset}
+                disabled={isEnqueuing}
+                selectedPrinterName={selectedPrinterName}
+                compatIndex={compatIndex}
+              />
               {/* Bed-type override (#1337). Always visible, always enabled.
-                  In non-bundle mode the backend patches curr_bed_type on the
-                  resolved process JSON before forwarding to the sidecar; in
-                  bundle mode the same value rides through as a sidecar form
-                  field so the bundle's materialised process JSON gets the
-                  override applied there too. */}
+                  The backend patches curr_bed_type on the resolved process
+                  JSON before forwarding to the sidecar. */}
               <BedTypeDropdown
                 value={bedType}
                 onChange={setBedType}
@@ -848,39 +717,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                   requestId={previewRequestId}
                   sourceName={source.filename}
                 />
-              ) : isBundleMode && selectedBundle ? (
-                filamentSlots.map((slot, idx) => {
-                  const isUsed = slot.used_in_plate !== false;
-                  const baseLabel =
-                    filamentSlots.length > 1
-                      ? t('slice.filamentSlot', {
-                          index: idx + 1,
-                          type: slot.type,
-                        })
-                      : t('slice.filament');
-                  const label = isUsed
-                    ? baseLabel
-                    : `${baseLabel} ${t('slice.notUsedByPlate')}`;
-                  return (
-                    <BundleStringDropdown
-                      key={`bundle-filament-${idx}`}
-                      label={label}
-                      options={selectedBundle.filament}
-                      value={bundleFilamentNames[idx] ?? null}
-                      onChange={(name) =>
-                        setBundleFilamentNames((current) => {
-                          const next = current.length === filamentSlots.length
-                            ? [...current]
-                            : filamentSlots.map((_, i) => current[i] ?? null);
-                          next[idx] = name;
-                          return next;
-                        })
-                      }
-                      disabled={isEnqueuing || !isUsed}
-                      swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
-                    />
-                  );
-                })
               ) : (
                 filamentSlots.map((slot, idx) => {
                   // Slots flagged by the backend as not used by the
@@ -1114,9 +950,8 @@ interface PresetDropdownProps {
   // configuring against the source 3MF's per-slot colour.
   swatchColor?: string;
   // Selected printer context (#1325). When provided for a process / filament
-  // slot, presets that resolve to a different printer (per the uploaded
-  // Slicer Bundles in compatIndex) move into a trailing "Other printers"
-  // group instead of the main tier list.
+  // slot, presets that resolve to a different printer (per compatIndex) move
+  // into a trailing "Other printers" group instead of the main tier list.
   selectedPrinterName?: string | null;
   compatIndex?: PrinterCompatibilityIndex;
 }
@@ -1226,96 +1061,3 @@ function PresetDropdown({
     </label>
   );
 }
-
-// Top-of-modal bundle picker. The "None" option leaves the user on the
-// cloud/local/standard tier path; selecting a bundle id flips the modal
-// into bundle dispatch mode (see SliceModal state above).
-interface BundlePickerProps {
-  bundles: SlicerBundle[];
-  selectedId: string | null;
-  onChange: (id: string | null) => void;
-  disabled?: boolean;
-}
-
-function BundlePicker({ bundles, selectedId, onChange, disabled }: BundlePickerProps) {
-  const { t } = useTranslation();
-  return (
-    <label className="block">
-      <span className="block text-sm text-bambu-gray mb-1 inline-flex items-center gap-1.5">
-        <Package className="w-3.5 h-3.5" />
-        {t('slice.bundle')}
-      </span>
-      <select
-        value={selectedId ?? ''}
-        onChange={(e) => onChange(e.target.value || null)}
-        disabled={disabled}
-        className="w-full px-3 py-2 rounded-md bg-bambu-dark border border-bambu-dark-tertiary text-white text-sm focus:outline-none focus:border-bambu-gray disabled:opacity-50"
-      >
-        <option value="">
-          {t('slice.bundleNone')}
-        </option>
-        {bundles.map((b) => (
-          <option key={b.id} value={b.id}>
-            {b.printer_preset_name}
-          </option>
-        ))}
-      </select>
-    </label>
-  );
-}
-
-// Plain-string dropdown used for bundle-mode process / filament selectors.
-// Bundles store presets as a flat list of names within their printer-tied
-// directory, so a `<select>` of strings is enough — no source tier, no
-// optgroups. Same swatch / disabled affordances as the cloud/local/standard
-// PresetDropdown above so the visual rhythm of the form stays consistent.
-interface BundleStringDropdownProps {
-  label: string;
-  options: string[];
-  value: string | null;
-  onChange: (next: string | null) => void;
-  disabled?: boolean;
-  swatchColor?: string;
-}
-
-function BundleStringDropdown({
-  label,
-  options,
-  value,
-  onChange,
-  disabled,
-  swatchColor,
-}: BundleStringDropdownProps) {
-  const { t } = useTranslation();
-  return (
-    <label className="block">
-      <span className="block text-sm text-bambu-gray mb-1 inline-flex items-center gap-1.5">
-        {swatchColor && (
-          <span
-            className="inline-block w-3 h-3 rounded-sm border border-black/20"
-            style={{ backgroundColor: swatchColor || 'transparent' }}
-            aria-hidden
-          />
-        )}
-        <span>{label}</span>
-      </span>
-      <select
-        value={value ?? ''}
-        onChange={(e) => onChange(e.target.value || null)}
-        disabled={disabled || options.length === 0}
-        className="w-full px-3 py-2 rounded-md bg-bambu-dark border border-bambu-dark-tertiary text-white text-sm focus:outline-none focus:border-bambu-gray disabled:opacity-50"
-      >
-        <option value="">
-          {options.length === 0
-            ? t('slice.noPresetsForSlot')
-            : t('slice.selectPreset')}
-        </option>
-        {options.map((name) => (
-          <option key={name} value={name}>
-            {name}
-          </option>
-        ))}
-      </select>
-    </label>
-  );
-}

+ 26 - 178
frontend/src/components/SlicerBundlesPanel.tsx

@@ -1,198 +1,46 @@
-import { useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { Loader2, Package, Trash2, Upload } from 'lucide-react';
-import { api, type SlicerBundle } from '../api/client';
+import { Package } from 'lucide-react';
 import { Card, CardContent, CardHeader } from './Card';
-import { Button } from './Button';
-import { ConfirmModal } from './ConfirmModal';
-import { useToast } from '../contexts/ToastContext';
 
-// Settings panel for managing BambuStudio "Printer Preset Bundles"
-// (.bbscfg) on the slicer sidecar. Sits below the slicer-API URL panel
-// in SettingsPage and is hidden when use_slicer_api is off — without a
-// configured sidecar there's nowhere to upload bundles to.
-//
-// Backend wiring: backend/app/api/routes/slicer_presets.py exposes
-// /api/v1/slicer/bundles (POST/GET/DELETE). The list call returns []
-// when no sidecar is configured, so an empty render is the natural
-// "first-run" state for users who haven't enabled the sidecar yet.
+// Static notice replacing the former Printer Preset Bundle import UI.
+// Removed in #1712: BambuStudio's bundle export only includes user-
+// customised presets, so users who exported a bundle ended up with no
+// process presets to slice with (BS doesn't export system processes).
+// Users with custom presets now route through Single Preset Import or
+// cloud sync; the standard tier on the sidecar already provides every
+// stock preset for slicing.
 export function SlicerBundlesPanel() {
   const { t } = useTranslation();
-  const queryClient = useQueryClient();
-  const { showToast } = useToast();
-  const fileInputRef = useRef<HTMLInputElement>(null);
-  const [pendingDelete, setPendingDelete] = useState<SlicerBundle | null>(null);
-
-  const { data: bundles, isLoading } = useQuery({
-    queryKey: ['slicer-bundles'],
-    queryFn: api.listSlicerBundles,
-  });
-
-  const importMutation = useMutation({
-    mutationFn: (file: File) => api.importSlicerBundle(file),
-    onSuccess: (bundle) => {
-      queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] });
-      showToast(
-        t('settings.slicerBundles.uploadSuccess', {
-          defaultValue: 'Imported {{name}}',
-          name: bundle.printer_preset_name,
-        }),
-        'success',
-      );
-      // Reset the file input so the same file can be re-selected after a
-      // failed retry. (Without this, a second click on the same file
-      // doesn't trigger onChange and looks like the panel is broken.)
-      if (fileInputRef.current) fileInputRef.current.value = '';
-    },
-    onError: (err: Error) => {
-      showToast(
-        t('settings.slicerBundles.uploadError', {
-          defaultValue: 'Bundle upload failed: {{message}}',
-          message: err.message,
-        }),
-        'error',
-      );
-      if (fileInputRef.current) fileInputRef.current.value = '';
-    },
-  });
-
-  const deleteMutation = useMutation({
-    mutationFn: (bundleId: string) => api.deleteSlicerBundle(bundleId),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] });
-      setPendingDelete(null);
-      showToast(
-        t('settings.slicerBundles.deleteSuccess', {
-          defaultValue: 'Bundle removed',
-        }),
-        'success',
-      );
-    },
-    onError: (err: Error) => {
-      showToast(
-        t('settings.slicerBundles.deleteError', {
-          defaultValue: 'Bundle delete failed: {{message}}',
-          message: err.message,
-        }),
-        'error',
-      );
-      setPendingDelete(null);
-    },
-  });
-
-  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const file = e.target.files?.[0];
-    if (!file) return;
-    importMutation.mutate(file);
-  };
-
   return (
     <Card>
       <CardHeader>
         <h3 className="text-base font-semibold text-white flex items-center gap-2">
-          <Package className="w-4 h-4 text-bambu-green" />
-          {t('settings.slicerBundles.title', { defaultValue: 'Slicer Bundles' })}
+          <Package className="w-4 h-4 text-bambu-gray" />
+          {t('settings.slicerBundlesRemoved.title', {
+            defaultValue: 'Slicer Bundles (removed)',
+          })}
         </h3>
       </CardHeader>
-      <CardContent className="space-y-3">
-        <p className="text-xs text-bambu-gray">
-          {t('settings.slicerBundles.description', {
+      <CardContent className="space-y-2">
+        <p className="text-sm text-bambu-gray">
+          {t('settings.slicerBundlesRemoved.description', {
             defaultValue:
-              'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.',
+              'Printer Preset Bundle (.bbscfg) import was removed. BambuStudio\'s bundle export only includes user-customised presets, so the import never delivered standard processes / filaments and slicing fell back to embedded settings.',
           })}
         </p>
-
-        <div className="flex items-center gap-2">
-          <input
-            ref={fileInputRef}
-            type="file"
-            accept=".bbscfg,.zip,application/zip"
-            onChange={handleFileChange}
-            className="hidden"
-            disabled={importMutation.isPending}
-          />
-          <Button
-            variant="primary"
-            onClick={() => fileInputRef.current?.click()}
-            disabled={importMutation.isPending}
-          >
-            {importMutation.isPending ? (
-              <>
-                <Loader2 className="w-4 h-4 animate-spin" />
-                {t('settings.slicerBundles.uploading', { defaultValue: 'Uploading…' })}
-              </>
-            ) : (
-              <>
-                <Upload className="w-4 h-4" />
-                {t('settings.slicerBundles.uploadButton', { defaultValue: 'Upload bundle' })}
-              </>
-            )}
-          </Button>
-        </div>
-
-        {isLoading ? (
-          <div className="flex items-center gap-2 text-sm text-bambu-gray">
-            <Loader2 className="w-4 h-4 animate-spin" />
-            {t('settings.slicerBundles.loading', { defaultValue: 'Loading bundles…' })}
-          </div>
-        ) : bundles && bundles.length > 0 ? (
-          <ul className="divide-y divide-bambu-dark-tertiary border border-bambu-dark-tertiary rounded-lg">
-            {bundles.map((b) => (
-              <li
-                key={b.id}
-                className="flex items-center justify-between px-3 py-2 hover:bg-bambu-dark-tertiary/30"
-              >
-                <div className="min-w-0 flex-1">
-                  <p className="text-sm text-white truncate">{b.printer_preset_name}</p>
-                  <p className="text-xs text-bambu-gray mt-0.5">
-                    {t('settings.slicerBundles.summary', {
-                      defaultValue:
-                        '{{processCount}} process · {{filamentCount}} filament presets',
-                      processCount: b.process.length,
-                      filamentCount: b.filament.length,
-                    })}
-                    {b.version && ` · v${b.version}`}
-                  </p>
-                </div>
-                <button
-                  type="button"
-                  onClick={() => setPendingDelete(b)}
-                  disabled={deleteMutation.isPending}
-                  className="ml-3 p-1.5 text-bambu-gray hover:text-red-400 disabled:opacity-40"
-                  aria-label={t('settings.slicerBundles.delete', { defaultValue: 'Delete' })}
-                >
-                  <Trash2 className="w-4 h-4" />
-                </button>
-              </li>
-            ))}
-          </ul>
-        ) : (
-          <p className="text-sm text-bambu-gray italic">
-            {t('settings.slicerBundles.empty', {
-              defaultValue: 'No bundles imported yet.',
-            })}
-          </p>
-        )}
-      </CardContent>
-
-      {pendingDelete && (
-        <ConfirmModal
-          title={t('settings.slicerBundles.confirmDeleteTitle', {
-            defaultValue: 'Remove this bundle?',
+        <p className="text-sm text-bambu-gray">
+          {t('settings.slicerBundlesRemoved.alternatives', {
+            defaultValue:
+              'Use Single Preset Import for individual customs, or sync via Bambu Cloud / Orca Cloud. Stock presets come from the slicer sidecar automatically.',
           })}
-          message={t('settings.slicerBundles.confirmDeleteMessage', {
+        </p>
+        <p className="text-sm text-bambu-gray">
+          {t('settings.slicerBundlesRemoved.lookupOrder', {
             defaultValue:
-              'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.',
-            name: pendingDelete.printer_preset_name,
+              'Slice-time preset lookup order: 1) Imported (local), 2) Orca Cloud, 3) Bambu Cloud, 4) Standard (sidecar fallback).',
           })}
-          confirmText={t('common.delete', { defaultValue: 'Delete' })}
-          variant="danger"
-          isLoading={deleteMutation.isPending}
-          onConfirm={() => deleteMutation.mutate(pendingDelete.id)}
-          onCancel={() => setPendingDelete(null)}
-        />
-      )}
+        </p>
+      </CardContent>
     </Card>
   );
 }

+ 5 - 18
frontend/src/i18n/locales/de.ts

@@ -1964,21 +1964,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer Sidecar-URL',
     bambuStudioApiUrl: 'Bambu Studio Sidecar-URL',
     slicerApiUrlDescription: 'URL des Slicer-API-Sidecar-Containers. Leer lassen, um die SLICER_API_URL- bzw. BAMBU_STUDIO_API_URL-Umgebungsvariablen zu nutzen.',
-    slicerBundles: {
-      title: 'Slicer-Bundles',
-      description: 'Importiere ein Drucker-Voreinstellungspaket (.bbscfg), das aus BambuStudio exportiert wurde (Datei → Exportieren → Voreinstellungspaket exportieren → "Drucker-Voreinstellungspaket"). Nach dem Import können Slice-Anfragen Voreinstellungen aus dem Bundle per Name auswählen, ohne das JSON-Profil-Trio erneut hochzuladen.',
-      uploadButton: 'Bundle hochladen',
-      uploading: 'Hochladen…',
-      loading: 'Bundles werden geladen…',
-      empty: 'Noch keine Bundles importiert.',
-      summary: '{{processCount}} Prozess · {{filamentCount}} Filament-Voreinstellungen',
-      delete: 'Löschen',
-      uploadSuccess: '{{name}} importiert',
-      uploadError: 'Bundle-Upload fehlgeschlagen: {{message}}',
-      deleteSuccess: 'Bundle entfernt',
-      deleteError: 'Löschen des Bundles fehlgeschlagen: {{message}}',
-      confirmDeleteTitle: 'Dieses Bundle entfernen?',
-      confirmDeleteMessage: 'Slice-Anfragen, die "{{name}}" referenzieren, schlagen fehl, bis das Bundle erneut importiert wird.',
+    slicerBundlesRemoved: {
+      title: 'Slicer-Bundles (entfernt)',
+      description: 'Der Import von Drucker-Voreinstellungs-Bundles (.bbscfg) wurde entfernt. Der Bundle-Export von BambuStudio enthält nur benutzerdefinierte Voreinstellungen, daher lieferte der Import nie die Standard-Prozesse / -Filamente, und das Slicen fiel auf eingebettete Einstellungen zurück.',
+      alternatives: 'Verwende Einzel-Voreinstellungs-Import für individuelle Anpassungen oder synchronisiere via Bambu Cloud / Orca Cloud. Standard-Voreinstellungen kommen automatisch vom Slicer-Sidecar.',
+      lookupOrder: 'Reihenfolge der Voreinstellungssuche beim Slicen: 1) Importiert (lokal), 2) Orca Cloud, 3) Bambu Cloud, 4) Standard (Sidecar-Fallback).',
     },
     externalCameras: 'Externe Kameras',
     costTracking: 'Kostenverfolgung',
@@ -3571,9 +3561,6 @@ export default {
     refreshPresets: 'Aktualisieren',
     refreshPresetsTitle: 'Profile neu laden — die aktuellen Cloud- und Bundle-Listen abrufen (nach dem Löschen eines Profils in Bambu Studio oder Bambu Handy verwenden)',
     allPresetsRequired: 'Alle Profile müssen ausgewählt sein',
-    bundle: 'Slicer-Bundle',
-    bundleNone: '— Keines (Profile einzeln auswählen) —',
-    bundleAllRequired: 'Bundle-Prozess und jeder Filament-Slot müssen ausgewählt sein',
     enqueuing: 'Slice-Auftrag wird übermittelt…',
     queued: 'In Warteschlange…',
     failed: 'Slicen fehlgeschlagen. Logs des Slicer-Sidecars prüfen.',

+ 5 - 18
frontend/src/i18n/locales/en.ts

@@ -1967,21 +1967,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.',
-    slicerBundles: {
-      title: 'Slicer Bundles',
-      description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.',
-      uploadButton: 'Upload bundle',
-      uploading: 'Uploading…',
-      loading: 'Loading bundles…',
-      empty: 'No bundles imported yet.',
-      summary: '{{processCount}} process · {{filamentCount}} filament presets',
-      delete: 'Delete',
-      uploadSuccess: 'Imported {{name}}',
-      uploadError: 'Bundle upload failed: {{message}}',
-      deleteSuccess: 'Bundle removed',
-      deleteError: 'Bundle delete failed: {{message}}',
-      confirmDeleteTitle: 'Remove this bundle?',
-      confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.',
+    slicerBundlesRemoved: {
+      title: 'Slicer Bundles (removed)',
+      description: 'Printer Preset Bundle (.bbscfg) import was removed. BambuStudio\'s bundle export only includes user-customised presets, so the import never delivered standard processes / filaments and slicing fell back to embedded settings.',
+      alternatives: 'Use Single Preset Import for individual customs, or sync via Bambu Cloud / Orca Cloud. Stock presets come from the slicer sidecar automatically.',
+      lookupOrder: 'Slice-time preset lookup order: 1) Imported (local), 2) Orca Cloud, 3) Bambu Cloud, 4) Standard (sidecar fallback).',
     },
     externalCameras: 'External Cameras',
     costTracking: 'Cost Tracking',
@@ -3574,9 +3564,6 @@ export default {
     refreshPresets: 'Refresh',
     refreshPresetsTitle: 'Refresh presets — fetch the latest cloud and bundled listings (use after deleting a preset in Bambu Studio or Bambu Handy)',
     allPresetsRequired: 'All presets must be selected',
-    bundle: 'Slicer bundle',
-    bundleNone: '— None (pick presets individually) —',
-    bundleAllRequired: 'Bundle process and every filament slot must be picked',
     enqueuing: 'Submitting slice job…',
     queued: 'Queued…',
     failed: 'Slicing failed. Check the slicer sidecar logs.',

+ 5 - 18
frontend/src/i18n/locales/es.ts

@@ -1967,21 +1967,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'URL del contenedor auxiliar de la API del laminador. Déjelo en blanco para usar los valores predeterminados de las variables de entorno SLICER_API_URL / BAMBU_STUDIO_API_URL.',
-    slicerBundles: {
-      title: 'Paquetes del laminador',
-      description: 'Importe un paquete de preajustes de impresora (.bbscfg) exportado desde BambuStudio (Archivo → Exportar → Exportar paquete de preajustes → «Paquete de preajustes de impresora»). Una vez importado, las solicitudes de laminado pueden elegir preajustes del paquete por nombre sin volver a subir el trío de perfiles JSON.',
-      uploadButton: 'Subir paquete',
-      uploading: 'Subiendo…',
-      loading: 'Cargando paquetes…',
-      empty: 'Aún no se ha importado ningún paquete.',
-      summary: '{{processCount}} preajustes de proceso · {{filamentCount}} de filamento',
-      delete: 'Eliminar',
-      uploadSuccess: '{{name}} importado',
-      uploadError: 'Error al subir el paquete: {{message}}',
-      deleteSuccess: 'Paquete eliminado',
-      deleteError: 'Error al eliminar el paquete: {{message}}',
-      confirmDeleteTitle: '¿Eliminar este paquete?',
-      confirmDeleteMessage: 'Las solicitudes de laminado que hagan referencia a "{{name}}" fallarán hasta que se vuelva a importar el paquete.',
+    slicerBundlesRemoved: {
+      title: 'Paquetes del laminador (eliminado)',
+      description: 'Se eliminó la importación de paquetes de preajustes de impresora (.bbscfg). La exportación de paquetes de BambuStudio solo incluye preajustes personalizados, por lo que la importación nunca entregaba procesos / filamentos estándar y el laminado recurría a la configuración incrustada.',
+      alternatives: 'Usa Importación de preajuste individual para personalizaciones, o sincroniza vía Bambu Cloud / Orca Cloud. Los preajustes estándar vienen del sidecar del laminador automáticamente.',
+      lookupOrder: 'Orden de búsqueda de preajustes al laminar: 1) Importado (local), 2) Orca Cloud, 3) Bambu Cloud, 4) Estándar (sidecar de respaldo).',
     },
     externalCameras: 'Cámaras externas',
     costTracking: 'Seguimiento de costes',
@@ -3574,9 +3564,6 @@ export default {
     refreshPresets: 'Actualizar',
     refreshPresetsTitle: 'Actualizar preajustes — recuperar los listados más recientes de la nube y los paquetes (úselo tras eliminar un preajuste en Bambu Studio o Bambu Handy)',
     allPresetsRequired: 'Deben seleccionarse todos los preajustes',
-    bundle: 'Paquete del laminador',
-    bundleNone: '— Ninguno (elegir preajustes individualmente) —',
-    bundleAllRequired: 'Deben elegirse el proceso del paquete y todas las ranuras de filamento',
     enqueuing: 'Enviando el trabajo de laminado…',
     queued: 'En cola…',
     failed: 'Error al laminar. Consulte los registros del contenedor auxiliar del laminador.',

+ 5 - 18
frontend/src/i18n/locales/fr.ts

@@ -1920,21 +1920,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'URL du conteneur sidecar slicer-API. Laisser vide pour utiliser les variables d\'environnement SLICER_API_URL / BAMBU_STUDIO_API_URL.',
-    slicerBundles: {
-      title: 'Bundles de slicer',
-      description: 'Importez un Printer Preset Bundle (.bbscfg) exporté depuis BambuStudio (Fichier → Exporter → Exporter le bundle de préréglages → "Printer preset bundle"). Une fois importé, les requêtes de découpage peuvent sélectionner les préréglages du bundle par nom sans re-téléverser le triplet de profils JSON.',
-      uploadButton: 'Téléverser le bundle',
-      uploading: 'Téléversement…',
-      loading: 'Chargement des bundles…',
-      empty: 'Aucun bundle importé pour le moment.',
-      summary: '{{processCount}} processus · {{filamentCount}} préréglages de filament',
-      delete: 'Supprimer',
-      uploadSuccess: '{{name}} importé',
-      uploadError: 'Échec du téléversement du bundle : {{message}}',
-      deleteSuccess: 'Bundle supprimé',
-      deleteError: 'Échec de la suppression du bundle : {{message}}',
-      confirmDeleteTitle: 'Supprimer ce bundle ?',
-      confirmDeleteMessage: 'Les requêtes de découpage référençant "{{name}}" échoueront jusqu\'à la réimportation du bundle.',
+    slicerBundlesRemoved: {
+      title: 'Bundles de slicer (supprimé)',
+      description: 'L\'import de Printer Preset Bundles (.bbscfg) a été supprimé. L\'export de bundle de BambuStudio ne comprend que les préréglages personnalisés, donc l\'import ne livrait jamais les processus / filaments standard et le découpage retombait sur les paramètres intégrés.',
+      alternatives: 'Utilisez l\'Import de préréglage unique pour les personnalisations, ou synchronisez via Bambu Cloud / Orca Cloud. Les préréglages standard viennent automatiquement du sidecar du trancheur.',
+      lookupOrder: 'Ordre de recherche des préréglages au découpage : 1) Importé (local), 2) Orca Cloud, 3) Bambu Cloud, 4) Standard (repli sidecar).',
     },
     externalCameras: 'Caméras externes',
     costTracking: 'Suivi des coûts',
@@ -3560,9 +3550,6 @@ export default {
     refreshPresets: 'Actualiser',
     refreshPresetsTitle: 'Actualiser les préréglages — récupérer les dernières listes Cloud et bundle (à utiliser après avoir supprimé un préréglage dans Bambu Studio ou Bambu Handy)',
     allPresetsRequired: 'Tous les préréglages doivent être sélectionnés',
-    bundle: 'Pack de découpage',
-    bundleNone: '— Aucun (choisir les préréglages individuellement) —',
-    bundleAllRequired: 'Le processus du pack et chaque emplacement de filament doivent être choisis',
     enqueuing: 'Envoi du travail de découpage…',
     queued: 'En file d\'attente…',
     failed: 'Échec du découpage. Vérifiez les journaux du sidecar.',

+ 5 - 18
frontend/src/i18n/locales/it.ts

@@ -1920,21 +1920,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'URL del container sidecar slicer-API. Lascia vuoto per usare le variabili d\'ambiente SLICER_API_URL / BAMBU_STUDIO_API_URL.',
-    slicerBundles: {
-      title: 'Bundle slicer',
-      description: 'Importa un Printer Preset Bundle (.bbscfg) esportato da BambuStudio (File → Esporta → Esporta bundle preset → "Printer preset bundle"). Una volta importato, le richieste di slicing possono scegliere preset dal bundle per nome senza ricaricare il triplet di profili JSON.',
-      uploadButton: 'Carica bundle',
-      uploading: 'Caricamento…',
-      loading: 'Caricamento bundle…',
-      empty: 'Nessun bundle importato ancora.',
-      summary: '{{processCount}} processo · {{filamentCount}} preset filamento',
-      delete: 'Elimina',
-      uploadSuccess: '{{name}} importato',
-      uploadError: 'Caricamento bundle fallito: {{message}}',
-      deleteSuccess: 'Bundle rimosso',
-      deleteError: 'Eliminazione bundle fallita: {{message}}',
-      confirmDeleteTitle: 'Rimuovere questo bundle?',
-      confirmDeleteMessage: 'Le richieste di slicing che fanno riferimento a "{{name}}" falliranno fino al re-import del bundle.',
+    slicerBundlesRemoved: {
+      title: 'Bundle slicer (rimosso)',
+      description: 'L\'importazione di Printer Preset Bundle (.bbscfg) è stata rimossa. L\'esportazione di bundle di BambuStudio include solo preset personalizzati, quindi l\'importazione non forniva mai i processi / filamenti standard e lo slicing ricorreva alle impostazioni incorporate.',
+      alternatives: 'Usa Importazione preset singolo per personalizzazioni, o sincronizza tramite Bambu Cloud / Orca Cloud. I preset standard arrivano automaticamente dal sidecar dello slicer.',
+      lookupOrder: 'Ordine di ricerca dei preset al momento dello slicing: 1) Importato (locale), 2) Orca Cloud, 3) Bambu Cloud, 4) Standard (fallback sidecar).',
     },
     externalCameras: 'Camere esterne',
     costTracking: 'Tracciamento costi',
@@ -3559,9 +3549,6 @@ export default {
     refreshPresets: 'Aggiorna',
     refreshPresetsTitle: 'Aggiorna i preset — recupera gli elenchi più recenti dal cloud e dai bundle (da usare dopo aver eliminato un preset in Bambu Studio o Bambu Handy)',
     allPresetsRequired: 'Tutti i preset devono essere selezionati',
-    bundle: 'Bundle slicer',
-    bundleNone: '— Nessuno (scegli i preset singolarmente) —',
-    bundleAllRequired: 'Devi scegliere il processo del bundle e ogni slot filamento',
     enqueuing: 'Invio lavoro di slicing…',
     queued: 'In coda…',
     failed: 'Slicing fallito. Controlla i log del sidecar.',

+ 5 - 18
frontend/src/i18n/locales/ja.ts

@@ -1963,21 +1963,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'slicer-APIサイドカーコンテナのURL。空のままにすると SLICER_API_URL / BAMBU_STUDIO_API_URL 環境変数のデフォルト値が使用されます。',
-    slicerBundles: {
-      title: 'スライサーバンドル',
-      description: 'BambuStudioからエクスポートされたPrinter Preset Bundle(.bbscfg)をインポートします(ファイル → エクスポート → プリセットバンドルをエクスポート → "Printer preset bundle")。インポート後、スライス要求はJSONプロファイルトリプレットを再アップロードせずにバンドルから名前でプリセットを選択できます。',
-      uploadButton: 'バンドルをアップロード',
-      uploading: 'アップロード中…',
-      loading: 'バンドルを読み込み中…',
-      empty: 'バンドルはまだインポートされていません。',
-      summary: '{{processCount}}プロセス · {{filamentCount}}フィラメントプリセット',
-      delete: '削除',
-      uploadSuccess: '{{name}}をインポート済み',
-      uploadError: 'バンドルのアップロードに失敗: {{message}}',
-      deleteSuccess: 'バンドルを削除しました',
-      deleteError: 'バンドルの削除に失敗: {{message}}',
-      confirmDeleteTitle: 'このバンドルを削除しますか?',
-      confirmDeleteMessage: '「{{name}}」を参照するスライス要求は、バンドルを再インポートするまで失敗します。',
+    slicerBundlesRemoved: {
+      title: 'スライサーバンドル(削除済み)',
+      description: 'プリンタープリセットバンドル(.bbscfg)のインポートは削除されました。BambuStudioのバンドルエクスポートはユーザーがカスタマイズしたプリセットのみを含むため、インポートでは標準プロセス/フィラメントが提供されず、スライスは埋め込み設定にフォールバックしていました。',
+      alternatives: '個別カスタマイズには単一プリセットインポートを使うか、Bambu Cloud / Orca Cloudで同期してください。標準プリセットはスライサーサイドカーから自動的に提供されます。',
+      lookupOrder: 'スライス時のプリセット検索順: 1) インポート済み(ローカル)、2) Orca Cloud、3) Bambu Cloud、4) 標準(サイドカーのフォールバック)。',
     },
     externalCameras: '外部カメラ',
     costTracking: 'コスト追跡',
@@ -3571,9 +3561,6 @@ export default {
     refreshPresets: '再読み込み',
     refreshPresetsTitle: 'プリセットを再取得 — クラウドとバンドルの最新リストを取得します(Bambu Studio または Bambu Handy でプリセットを削除した後にお使いください)',
     allPresetsRequired: 'すべてのプリセットを選択する必要があります',
-    bundle: 'スライサーバンドル',
-    bundleNone: '— なし(プリセットを個別に選択)—',
-    bundleAllRequired: 'バンドルのプロセスとすべてのフィラメントスロットを選択してください',
     enqueuing: 'スライスジョブを送信中…',
     queued: '待機中…',
     failed: 'スライスに失敗。サイドカーのログを確認してください。',

+ 5 - 18
frontend/src/i18n/locales/ko.ts

@@ -1838,21 +1838,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer 사이드카 URL',
     bambuStudioApiUrl: 'Bambu Studio 사이드카 URL',
     slicerApiUrlDescription: '슬라이서 API 사이드카 컨테이너의 URL. SLICER_API_URL / BAMBU_STUDIO_API_URL 환경 변수 기본값을 사용하려면 비워두세요.',
-    slicerBundles: {
-      title: '슬라이서 번들',
-      description: 'BambuStudio에서 내보낸 프린터 프리셋 번들(.bbscfg)을 가져오세요. 가져온 후 슬라이스 요청이 JSON 프로필 트리플렛을 다시 업로드하지 않고 번들에서 이름으로 프리셋을 선택할 수 있습니다.',
-      uploadButton: '번들 업로드',
-      uploading: '업로드 중…',
-      loading: '번들 로딩 중…',
-      empty: '아직 가져온 번들 없음.',
-      summary: '{{processCount}}개 프로세스 · {{filamentCount}}개 필라멘트 프리셋',
-      delete: '삭제',
-      uploadSuccess: '{{name}} 가져옴',
-      uploadError: '번들 업로드 실패: {{message}}',
-      deleteSuccess: '번들 제거됨',
-      deleteError: '번들 삭제 실패: {{message}}',
-      confirmDeleteTitle: '이 번들을 제거하시겠습니까?',
-      confirmDeleteMessage: '"{{name}}"을(를) 참조하는 슬라이스 요청은 번들이 재가져오기될 때까지 실패합니다.'
+    slicerBundlesRemoved: {
+      title: '슬라이서 번들 (제거됨)',
+      description: '프린터 프리셋 번들 (.bbscfg) 가져오기가 제거되었습니다. BambuStudio의 번들 내보내기에는 사용자 정의 프리셋만 포함되므로, 가져오기로는 표준 프로세스 / 필라멘트가 제공되지 않았고 슬라이싱은 임베디드 설정으로 되돌아갔습니다.',
+      alternatives: '개별 사용자 정의는 단일 프리셋 가져오기를, 또는 Bambu Cloud / Orca Cloud를 통해 동기화하세요. 표준 프리셋은 슬라이서 사이드카에서 자동으로 제공됩니다.',
+      lookupOrder: '슬라이스 시점의 프리셋 조회 순서: 1) 가져옴 (로컬), 2) Orca Cloud, 3) Bambu Cloud, 4) 표준 (사이드카 폴백).',
     },
     externalCameras: '외부 카메라',
     costTracking: '비용 추적',
@@ -3392,9 +3382,6 @@ export default {
     actionAllTitle: '모든 플레이트를 단일 다중 플레이트 출력으로 슬라이싱합니다 (단일 아카이브). 필라멘트 선택은 프로젝트가 정의하는 모든 슬롯을 포함합니다.',
     allPlatesToggle: '{{count}}개 플레이트 모두 슬라이싱',
     otherPrinters: '다른 프린터',
-    bundle: '슬라이서 번들',
-    bundleNone: '— 없음 (개별로 프리셋 선택) —',
-    bundleAllRequired: '번들 프로세스 및 모든 필라멘트 슬롯을 선택해야 합니다',
     runningWithProgressMultiPlate: '플레이트 {{plateIndex}}/{{plateCount}} • {{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
     failedTitle: '슬라이싱 실패',
     bedType: {

+ 5 - 18
frontend/src/i18n/locales/pt-BR.ts

@@ -1920,21 +1920,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'URL do contêiner sidecar slicer-API. Deixe em branco para usar SLICER_API_URL / BAMBU_STUDIO_API_URL.',
-    slicerBundles: {
-      title: 'Bundles do fatiador',
-      description: 'Importe um Printer Preset Bundle (.bbscfg) exportado do BambuStudio (Arquivo → Exportar → Exportar Bundle de Predefinições → "Printer preset bundle"). Após importado, solicitações de fatiamento podem escolher predefinições do bundle por nome sem re-enviar o triplet de perfis JSON.',
-      uploadButton: 'Enviar bundle',
-      uploading: 'Enviando…',
-      loading: 'Carregando bundles…',
-      empty: 'Nenhum bundle importado ainda.',
-      summary: '{{processCount}} processo · {{filamentCount}} predefinições de filamento',
-      delete: 'Excluir',
-      uploadSuccess: '{{name}} importado',
-      uploadError: 'Falha ao enviar bundle: {{message}}',
-      deleteSuccess: 'Bundle removido',
-      deleteError: 'Falha ao excluir bundle: {{message}}',
-      confirmDeleteTitle: 'Remover este bundle?',
-      confirmDeleteMessage: 'Solicitações de fatiamento referenciando "{{name}}" falharão até o bundle ser reimportado.',
+    slicerBundlesRemoved: {
+      title: 'Bundles do fatiador (removido)',
+      description: 'A importação de Printer Preset Bundles (.bbscfg) foi removida. A exportação de bundle do BambuStudio inclui apenas predefinições personalizadas, portanto a importação nunca entregava processos / filamentos padrão e o fatiamento recorria às configurações incorporadas.',
+      alternatives: 'Use a Importação de predefinição individual para personalizações, ou sincronize via Bambu Cloud / Orca Cloud. As predefinições padrão vêm do sidecar do fatiador automaticamente.',
+      lookupOrder: 'Ordem de busca de predefinições no fatiamento: 1) Importada (local), 2) Orca Cloud, 3) Bambu Cloud, 4) Padrão (fallback do sidecar).',
     },
     externalCameras: 'Câmeras Externas',
     costTracking: 'Rastreamento de Custos',
@@ -3559,9 +3549,6 @@ export default {
     refreshPresets: 'Atualizar',
     refreshPresetsTitle: 'Atualizar predefinições — buscar as listagens mais recentes da nuvem e dos pacotes (use após excluir uma predefinição no Bambu Studio ou Bambu Handy)',
     allPresetsRequired: 'Todas as predefinições devem ser selecionadas',
-    bundle: 'Pacote do fatiador',
-    bundleNone: '— Nenhum (escolher predefinições individualmente) —',
-    bundleAllRequired: 'O processo do pacote e cada slot de filamento devem ser escolhidos',
     enqueuing: 'Enviando trabalho de fatiamento…',
     queued: 'Na fila…',
     failed: 'Falha ao fatiar. Verifique os logs do sidecar.',

+ 5 - 18
frontend/src/i18n/locales/tr.ts

@@ -1967,21 +1967,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer yardımcı bileşen URL',
     bambuStudioApiUrl: 'Bambu Studio yardımcı bileşen URL',
     slicerApiUrlDescription: 'Dilimleyici-API yardımcı bileşen konteynerinin URL\'si. SLICER_API_URL / BAMBU_STUDIO_API_URL ortam değişkeni varsayılanlarını kullanmak için boş bırakın.',
-    slicerBundles: {
-      title: 'Dilimleyici Paketleri',
-      description: 'BambuStudio\'dan dışa aktarılmış (Dosya → Dışa Aktar → Ön Ayar Paketini Dışa Aktar → "Yazıcı ön ayar paketi") bir Yazıcı Ön Ayar Paketi (.bbscfg) içe aktarın. İçe aktarıldıktan sonra dilimleme istekleri, JSON profil üçlüsünü yeniden yüklemeden paketten ön ayarları ada göre seçebilir.',
-      uploadButton: 'Paket yükle',
-      uploading: 'Yükleniyor…',
-      loading: 'Paketler yükleniyor…',
-      empty: 'Henüz içe aktarılmış paket yok.',
-      summary: '{{processCount}} işlem · {{filamentCount}} filament ön ayarı',
-      delete: 'Sil',
-      uploadSuccess: '{{name}} içe aktarıldı',
-      uploadError: 'Paket yüklemesi başarısız: {{message}}',
-      deleteSuccess: 'Paket kaldırıldı',
-      deleteError: 'Paket silme başarısız: {{message}}',
-      confirmDeleteTitle: 'Bu paket kaldırılsın mı?',
-      confirmDeleteMessage: 'Paket yeniden içe aktarılana kadar "{{name}}" referans alan dilimleme istekleri başarısız olacak.',
+    slicerBundlesRemoved: {
+      title: 'Dilimleyici Paketleri (kaldırıldı)',
+      description: 'Yazıcı Ön Ayar Paketi (.bbscfg) içe aktarma kaldırıldı. BambuStudio\'nun paket dışa aktarması yalnızca kullanıcı tarafından özelleştirilmiş ön ayarları içerir, bu nedenle içe aktarma hiçbir zaman standart süreçleri / filamentleri sağlamadı ve dilimleme gömülü ayarlara geri döndü.',
+      alternatives: 'Bireysel özelleştirmeler için Tekli Ön Ayar İçe Aktarma\'yı kullanın veya Bambu Cloud / Orca Cloud üzerinden senkronize edin. Standart ön ayarlar otomatik olarak dilimleyici sidecar\'ından gelir.',
+      lookupOrder: 'Dilimleme sırasında ön ayar arama sırası: 1) İçe aktarılmış (yerel), 2) Orca Cloud, 3) Bambu Cloud, 4) Standart (sidecar yedeği).',
     },
     externalCameras: 'Harici Kameralar',
     costTracking: 'Maliyet Takibi',
@@ -3560,9 +3550,6 @@ export default {
     refreshPresets: 'Yenile',
     refreshPresetsTitle: "Ön ayarları yenile — en güncel bulut ve paketli listeleri getir (Bambu Studio veya Bambu Handy'de bir ön ayar sildikten sonra kullanın)",
     allPresetsRequired: 'Tüm ön ayarlar seçilmelidir',
-    bundle: 'Dilimleyici paketi',
-    bundleNone: '— Hiçbiri (ön ayarları ayrı ayrı seçin) —',
-    bundleAllRequired: 'Paket işlemi ve her filament yuvası seçilmelidir',
     enqueuing: 'Dilimleme işi gönderiliyor…',
     queued: 'Kuyrukta…',
     failed: 'Dilimleme başarısız. Dilimleyici yardımcı bileşen günlüklerini kontrol edin.',

+ 5 - 18
frontend/src/i18n/locales/zh-CN.ts

@@ -1965,21 +1965,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'slicer-API sidecar 容器的 URL。留空以使用 SLICER_API_URL / BAMBU_STUDIO_API_URL 环境变量默认值。',
-    slicerBundles: {
-      title: '切片器捆绑包',
-      description: '导入从 BambuStudio 导出的 Printer Preset Bundle (.bbscfg)(文件 → 导出 → 导出预设捆绑包 → "Printer preset bundle")。导入后,切片请求可以按名称从捆绑包中选择预设,无需重新上传 JSON 配置三元组。',
-      uploadButton: '上传捆绑包',
-      uploading: '上传中…',
-      loading: '加载捆绑包中…',
-      empty: '尚未导入捆绑包。',
-      summary: '{{processCount}} 个工艺 · {{filamentCount}} 个耗材预设',
-      delete: '删除',
-      uploadSuccess: '已导入 {{name}}',
-      uploadError: '上传捆绑包失败:{{message}}',
-      deleteSuccess: '捆绑包已移除',
-      deleteError: '删除捆绑包失败:{{message}}',
-      confirmDeleteTitle: '移除此捆绑包?',
-      confirmDeleteMessage: '引用「{{name}}」的切片请求将失败,直到捆绑包重新导入。',
+    slicerBundlesRemoved: {
+      title: '切片器捆绑包(已移除)',
+      description: '打印机预设包 (.bbscfg) 导入已移除。BambuStudio 的包导出仅包含用户自定义的预设,因此导入从未提供标准工艺 / 耗材,切片会回退到嵌入设置。',
+      alternatives: '对于单独的自定义,请使用单个预设导入,或通过 Bambu Cloud / Orca Cloud 同步。标准预设自动来自切片器侧车。',
+      lookupOrder: '切片时的预设查找顺序:1) 已导入(本地),2) Orca Cloud,3) Bambu Cloud,4) 标准(侧车回退)。',
     },
     externalCameras: '外部摄像头',
     costTracking: '成本追踪',
@@ -3559,9 +3549,6 @@ export default {
     refreshPresets: '刷新',
     refreshPresetsTitle: '刷新预设 — 获取最新的云端和打包配置列表(在 Bambu Studio 或 Bambu Handy 中删除预设后使用)',
     allPresetsRequired: '必须选择所有预设',
-    bundle: '切片器套装',
-    bundleNone: '— 无(单独选择预设)—',
-    bundleAllRequired: '必须选择套装的工艺和每个耗材槽',
     enqueuing: '提交切片任务中…',
     queued: '已排队…',
     failed: '切片失败。请检查切片器 sidecar 日志。',

+ 5 - 18
frontend/src/i18n/locales/zh-TW.ts

@@ -1965,21 +1965,11 @@ export default {
     orcaslicerApiUrl: 'OrcaSlicer sidecar URL',
     bambuStudioApiUrl: 'Bambu Studio sidecar URL',
     slicerApiUrlDescription: 'slicer-API sidecar 容器的 URL。留空以使用 SLICER_API_URL / BAMBU_STUDIO_API_URL 環境變數預設值。',
-    slicerBundles: {
-      title: '切片器捆綁包',
-      description: '匯入從 BambuStudio 匯出的 Printer Preset Bundle (.bbscfg)(檔案 → 匯出 → 匯出預設捆綁包 → "Printer preset bundle")。匯入後,切片請求可以按名稱從捆綁包中選擇預設,無需重新上傳 JSON 設定三元組。',
-      uploadButton: '上傳捆綁包',
-      uploading: '上傳中…',
-      loading: '載入捆綁包中…',
-      empty: '尚未匯入捆綁包。',
-      summary: '{{processCount}} 個製程 · {{filamentCount}} 個耗材預設',
-      delete: '刪除',
-      uploadSuccess: '已匯入 {{name}}',
-      uploadError: '上傳捆綁包失敗:{{message}}',
-      deleteSuccess: '捆綁包已移除',
-      deleteError: '刪除捆綁包失敗:{{message}}',
-      confirmDeleteTitle: '移除此捆綁包?',
-      confirmDeleteMessage: '引用「{{name}}」的切片請求將失敗,直到捆綁包重新匯入。',
+    slicerBundlesRemoved: {
+      title: '切片器捆綁包(已移除)',
+      description: '印表機預設套件 (.bbscfg) 匯入已移除。BambuStudio 的套件匯出僅包含使用者自訂的預設,因此匯入從未提供標準製程 / 耗材,切片會回退到嵌入設定。',
+      alternatives: '對於單獨的自訂,請使用單一預設匯入,或透過 Bambu Cloud / Orca Cloud 同步。標準預設自動來自切片器側車。',
+      lookupOrder: '切片時的預設查找順序:1) 已匯入(本機),2) Orca Cloud,3) Bambu Cloud,4) 標準(側車回退)。',
     },
     externalCameras: '外部攝影機',
     costTracking: '成本追蹤',
@@ -3559,9 +3549,6 @@ export default {
     refreshPresets: '重新整理',
     refreshPresetsTitle: '重新整理預設 — 擷取最新的雲端與打包設定清單(在 Bambu Studio 或 Bambu Handy 中刪除預設後使用)',
     allPresetsRequired: '必須選擇所有預設',
-    bundle: '切片器套裝',
-    bundleNone: '— 無(單獨選擇預設)—',
-    bundleAllRequired: '必須選擇套裝的製程和每個耗材槽',
     enqueuing: '提交切片任務中…',
     queued: '已排隊…',
     failed: '切片失敗。請檢查切片器 sidecar 日誌。',

+ 18 - 76
frontend/src/utils/slicerPrinterMatch.ts

@@ -6,20 +6,10 @@
 //
 //   1. Imported (local-tier) presets carry the slicer's own
 //      `compatible_printers` list — an exact list of printer-preset names.
-//   2. Uploaded Slicer Bundles (.bbscfg). A bundle is scoped to one printer
-//      and lists the process / filament presets shipped with it, so a preset
-//      a bundle covers is compatible with exactly that bundle's printer. A
-//      newly released Bambu model is covered the moment its bundle is
-//      uploaded — no code change required.
-//   3. BambuStudio's own `@BBL <model>` naming convention on shipped cloud
-//      / standard presets. This used to be the only signal, was removed in
-//      the first cut of #1325 in favour of (2) — which works for the author
-//      and anyone who uploaded their bundles, but silently no-ops for users
-//      who hadn't (the reporter's case). Restored as a fallback below the
-//      bundle path so the table is only consulted when bundles can't decide.
-//      The token → printer-fragment table is derived from the backend's
-//      canonical PRINTER_MODEL_MAP (fetched via /slicer/printer-models),
-//      not duplicated here.
+//   2. BambuStudio's own `@BBL <model>` naming convention on shipped cloud
+//      / standard presets. The token → printer-fragment table is derived
+//      from the backend's canonical PRINTER_MODEL_MAP (fetched via
+//      /slicer/printer-models), not duplicated here.
 //
 // The result drives grouping, not hard hiding: a preset no rule covers
 // stays in the main list, and only a preset that resolves to a *different*
@@ -27,41 +17,20 @@
 
 export type PrinterCompatibility = 'match' | 'mismatch' | 'unknown';
 
-// Minimal shape of a Slicer Bundle needed for matching (see SlicerBundle in
-// api/client.ts). `printer_preset_name` scopes the bundle to one printer;
-// `process` / `filament` are the preset names that bundle ships.
-export interface CompatibilityBundle {
-  printer_preset_name: string;
-  process: string[];
-  filament: string[];
-}
-
-// Lookup tables consumed by `presetCompatibility`. `process` / `filament` are
-// preset-name → set-of-compatible-printer-names built from uploaded bundles.
-// `bambuModelByShortCode` is the @BBL token → printer-preset fragment map
-// derived from the backend's PRINTER_MODEL_MAP — e.g. `X1C` → `X1 Carbon`.
-// All three are empty by default; an empty `bambuModelByShortCode` means the
-// @BBL fallback still works when token and printer-name fragment match
-// directly (raw-token comparison), and gracefully degrades otherwise.
+// Lookup tables consumed by `presetCompatibility`. `bambuModelByShortCode`
+// is the @BBL token → printer-preset fragment map derived from the backend's
+// PRINTER_MODEL_MAP — e.g. `X1C` → `X1 Carbon`. An empty map means the @BBL
+// fallback still works when token and printer-name fragment match directly
+// (raw-token comparison), and gracefully degrades otherwise.
 export interface PrinterCompatibilityIndex {
-  process: Map<string, Set<string>>;
-  filament: Map<string, Set<string>>;
   bambuModelByShortCode: Record<string, string>;
 }
 
-/** An empty index — used when no bundles / models are loaded yet. */
+/** An empty index — used when the model map hasn't loaded yet. */
 export const EMPTY_COMPATIBILITY_INDEX: PrinterCompatibilityIndex = {
-  process: new Map(),
-  filament: new Map(),
   bambuModelByShortCode: {},
 };
 
-// Bundle preset names occasionally carry BambuStudio's "# " user-clone
-// prefix; strip it so a bundle entry and a tier-listed preset compare equal.
-function normalizePresetName(name: string): string {
-  return name.replace(/^#\s*/, '').trim();
-}
-
 // Bambu cloud started shipping terse model codes in `@BBL <code>` suffixes
 // mid-2026 — the most visible one is "A1 Mini" → "A1M" (#1649, reported by
 // @technopaw). User-authored profiles still use the long display name, so
@@ -115,33 +84,12 @@ function buildShortCodeMap(
 }
 
 /**
- * Build the compatibility index from the user's uploaded Slicer Bundles and
- * the backend printer-model registry. Each bundle contributes its printer
- * to every process / filament name it ships; a name shipped by several
- * bundles accumulates every printer.
+ * Build the compatibility index from the backend printer-model registry.
  */
 export function buildCompatibilityIndex(
-  bundles: readonly CompatibilityBundle[],
   printerModels: Record<string, string> = {},
 ): PrinterCompatibilityIndex {
-  const process = new Map<string, Set<string>>();
-  const filament = new Map<string, Set<string>>();
-  const add = (map: Map<string, Set<string>>, name: string, printer: string) => {
-    const key = normalizePresetName(name);
-    if (!key) return;
-    const set = map.get(key) ?? new Set<string>();
-    set.add(printer);
-    map.set(key, set);
-  };
-  for (const bundle of bundles) {
-    const printer = bundle.printer_preset_name?.trim();
-    if (!printer) continue;
-    for (const name of bundle.process) add(process, name, printer);
-    for (const name of bundle.filament) add(filament, name, printer);
-  }
   return {
-    process,
-    filament,
     bambuModelByShortCode: buildShortCodeMap(printerModels),
   };
 }
@@ -190,8 +138,8 @@ function extractPrinterPresetModel(printerPresetName: string): { model: string;
 
 /**
  * Name-based fallback for presets BambuStudio ships with a `@BBL <model>`
- * tag (#1325 follow-up). Used only after `compatible_printers` and the
- * uploaded-bundle index have already returned `'unknown'`.
+ * tag (#1325 follow-up). Used only after `compatible_printers` has returned
+ * `'unknown'`.
  *
  * Compares BOTH model AND nozzle. The nozzle filter is required because
  * Bambu ships per-nozzle process / filament variants (0.2 / 0.4 / 0.6 /
@@ -243,12 +191,12 @@ function classifyByBambuName(
  * - 'match'    — the preset is compatible with the selected printer.
  * - 'mismatch' — the preset resolves to a *different* printer.
  * - 'unknown'  — compatibility can't be determined (no `compatible_printers`,
- *                no uploaded bundle, no recognizable `@BBL` tag, or no
- *                printer is selected); the caller must not hide it.
+ *                no recognizable `@BBL` tag, or no printer is selected);
+ *                the caller must not hide it.
  */
 export function presetCompatibility(
   preset: { name: string; compatible_printers?: string[] | null },
-  slot: 'process' | 'filament',
+  _slot: 'process' | 'filament',
   selectedPrinterName: string | null,
   index: PrinterCompatibilityIndex,
 ): PrinterCompatibility {
@@ -259,13 +207,7 @@ export function presetCompatibility(
   if (compat && compat.length > 0) {
     return compat.includes(selectedPrinterName) ? 'match' : 'mismatch';
   }
-  // (2) Consult the uploaded Slicer Bundles.
-  const printers = index[slot].get(normalizePresetName(preset.name));
-  if (printers && printers.size > 0) {
-    return printers.has(selectedPrinterName) ? 'match' : 'mismatch';
-  }
-  // (3) BambuStudio's `@BBL <model>` name convention — covers cloud /
-  // standard presets for users who haven't uploaded bundles for every
-  // printer their cloud catalogue includes.
+  // (2) BambuStudio's `@BBL <model>` name convention — covers cloud /
+  // standard presets that don't carry compatible_printers.
   return classifyByBambuName(preset.name, selectedPrinterName, index.bambuModelByShortCode);
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
static/assets/index-45eedLWT.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
static/assets/index-7s3X35pi.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
static/assets/index-VyNhPxaj.js


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Dtvr3nyH.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-7s3X35pi.css">
+    <script type="module" crossorigin src="/assets/index-VyNhPxaj.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-45eedLWT.css">
   </head>
   <body>
     <div id="root"></div>

Vissa filer visades inte eftersom för många filer har ändrats