fix(profiles): add PLA-CF + Bambu CF/GF/specialty variants to filament_type select (#1686)

The Profiles page edit modal (shared by BL Cloud / Orca Cloud / Local
  Profiles) renders filament_type from backend/app/data/filament_fields.json
  via GET /cloud/fields/filament. The curated 11-option list pre-dated Bambu's
  CF/GF lineup expansion, so PLA-CF and most carbon/glass-fiber variants were
  unselectable — saved presets carried the wrong material code at dispatch.

  Expanded to 25 BambuStudio-aligned options grouped by family: PLA (+ CF/GF/
  AERO), PETG (+ CF), ABS (+ GF), ASA (+ CF/GF), PC, PCTG, PA family (+ CF/
  PAHT-CF/PA6-CF/PA6-GF), PET-CF, TPU, PPS family (+ CF/GF for X1E), PVA, HIPS.

  K-profiles editor unaffected (picks filament_id, not filament_type).
This commit is contained in:
maziggy
2026-06-09 07:38:59 +02:00
parent fb1e9a917e
commit bfbf5d59af
3 changed files with 73 additions and 4 deletions
+2
View File
@@ -10,6 +10,8 @@ All notable changes to Bambuddy will be documented in this file.
- **Queue items + Print modal now show the build plate type, per-plate accurate (#1281, reported by @CMW-ISS)** — Reporter on a multi-printer farm with 40+-plate runs needed to walk to the printer with the right physical plate; the archive card had recently grown a bed-type badge, but the queue and the scheduling modal didn't. They were having to open the source 3MF in the slicer to look up which plate each queued / scheduled job needs. **Backend**: new `extract_bed_type_from_3mf(file_path, plate_id)` helper in `utils/threemf_tools.py`, alongside the existing `extract_filament_usage_from_3mf` shape — reads `Metadata/slice_info.config`, finds the `<plate>` with the matching `index`, returns its `curr_bed_type`. When `plate_id` is None it returns the first plate's value (matches the archive-level capture convention). `PrintQueueItemResponse` gains a `bed_type: str | None` field; `_enrich_response` populates it from `archive.bed_type` / `library_file.file_metadata["bed_type"]` as the file-level default, then overrides per-plate via the new helper when `item.plate_id` is set. This matters because `archive.bed_type` is captured at ingest as the FIRST plate's value only (see `services/archive.py:235`) — a 40-plate 3MF mixing PEI + Engineering returns "PEI" for every plate at the archive level, even though the user's plate 17 actually needs Engineering. The per-plate override re-reads the 3MF and returns the truth. **`/archives/{id}/plates`** (and the library-file equivalent) now include `bed_type` in each plate object so the PrintModal's plate selector can render the badge inline. **Frontend**: queue card meta row gains a bed badge after filament weight — uses the existing `getBedTypeInfo(bed_type)` helper from `utils/bedType.ts` (the same one the archive card uses, so all 11 canonical bed labels + icons are covered including the BambuStudio / OrcaSlicer spelling drift). PrintModal's per-plate `PlateSelector` shows the bed badge under each plate's filament line; the modal header carries a bed badge for the selected (or sole) plate, surfaced before the user hits Schedule. `PlateInfo` + `PlateMetadata` types both get an optional `bed_type` field. No new i18n keys needed — `getBedTypeInfo` returns the canonical English plate name as the human label, matching the archive card's existing convention. **Tests**: 8 new unit cases in `test_threemf_tools.py::TestExtractBedTypeFrom3mf` pin the helper (single-plate, multi-plate per-plate, no-plate-id defaults to first, unknown-plate-id → None, plate-without-bed-type → None (no fall-through to another plate's value), missing slice_info, invalid file, whitespace trim). Full backend suite green (3848/3848); frontend build clean; ESLint clean; vitest on touched pages 81/81; i18n parity 5092 leaves × 11 locales green.
### Fixed
- **Profile editor filament type dropdown now lists PLA-CF and the other Bambu CF / GF / specialty materials (#1686, reported by @Bgabor997)** — Creating or editing a filament preset on the Profiles page (BL Cloud, Orca Cloud, and Local Profiles all open the same shared editor) only offered 11 base materials (PLA, ABS, PETG, TPU, PA, PA-CF, PET-CF, PC, ASA, PVA, HIPS). Reporter on P1S wanted to tag a custom preset as PLA-CF — the dropdown source had no entry, so the saved preset's `filament_type` was wrong and the printer received the wrong material code at dispatch. **Root cause:** `backend/app/data/filament_fields.json` (served by `GET /cloud/fields/filament` and consumed by `ProfilesPage` via `getCloudFields`) shipped a curated subset that pre-dated Bambu's CF/GF lineup expansion. Other surfaces in the codebase already named the canonical list (`utils/filament_ids.py` `GENERIC_FILAMENT_IDS`, `spool-form/utils.ts` MATERIALS, the Bambu filament-id catalog in `cloud.py`), so the gap was specifically in the editor's allowed-values JSON. **Fix:** expanded the `filament_type` select to 25 BambuStudio-aligned options grouped by family — PLA (+ CF/GF/AERO), PETG (+ CF), ABS (+ GF), ASA (+ CF/GF), PC, PCTG, PA family (+ CF/PAHT-CF/PA6-CF/PA6-GF), PET-CF, TPU, PPS family (+ CF/GF for X1E), PVA, HIPS. No frontend, no i18n (material codes are universal). K-profiles editor unaffected — it picks `filament_id`, not `filament_type`. **Tests:** 15 unit cases in `test_filament_fields_options.py` pin every newly-added variant (PLA-CF, PLA-GF, PLA-AERO, PETG-CF, ABS-GF, ASA-CF, ASA-GF, PCTG, PAHT-CF, PA6-CF, PA6-GF, PPS, PPS-CF, PPS-GF) plus the baseline-must-still-be-present guard so a future curation pass can't silently drop them.
- **Native systemd install no longer fails when INSTALL_PATH is under /home (#1685, reported by @Geoff-S)** — `bambuddy.service` shipped with `ProtectHome=true`, which makes `/home/*` invisible to the service namespace. When the user installed into `/home/bambuddy/` (instead of the default `/opt/bambuddy/`), the `ExecStart=/home/bambuddy/venv/bin/uvicorn` path couldn't be resolved at exec time and the unit failed with `status=203/EXEC: Unable to locate executable`. The `ReadWritePaths=$INSTALL_PATH` directive doesn't reliably re-expose `/home/*` subpaths for executable resolution. **Fix:** `install/install.sh` now detects `INSTALL_PATH == /home/*` and emits `ProtectHome=read-only` for that case; the default `/opt/bambuddy/` install keeps the stricter `ProtectHome=true`. The manual `deploy/bambuddy.service` template defaults to `ProtectHome=read-only` with a comment explaining when to tighten it to `true`. `read-only` keeps `/home` immutable to the service (no security regression — the service can read its venv but not write anywhere outside the `ReadWritePaths` allowlist).
- **VP settings card now shows the target printer's serial in proxy mode** — On a proxy-mode VP, the runtime services (SSDP advertisement, MQTT bind identity, certificate subject) all use the target printer's actual serial via `target_printer_serial or self.serial` (`manager.py:235, 941, 957`), but the `/api/v1/virtual-printers` response — which feeds the VP settings card — always returned the self-generated suffix-based serial from `_get_serial_for_model(model_code, vp.serial_suffix)`. The card therefore displayed a serial that didn't match what the bridge actually advertises and what the slicer sees, breaking the visual "one identity per VP" mental model. **Fix:** `_vp_to_dict` (`api/routes/virtual_printers.py:77`) is now async and accepts `db`; when `vp.mode == VP_MODE_PROXY and vp.target_printer_id`, it issues a single `SELECT serial_number FROM printers WHERE id = vp.target_printer_id` and substitutes the result into the response `serial` field. Archive / queue / review modes keep the self-generated serial — those modes synthesise their own identity and never speak the target's. **Defensive fallback** when the target row is missing (printer deleted mid-config, manual SQL tweak, race between delete-printer and read-VP): the response falls back to the self-generated serial so the card still renders and the user can fix the target, rather than the API 500-ing. All 4 `_vp_to_dict` call sites (list, create, get, update) updated to `await` with `db`. **Tests:** 3 new in `TestVirtualPrinterSerialSurface` — proxy VP returns target serial across all three response paths (create / get / list), non-proxy VP with a target still uses the self-generated serial, orphaned proxy VP falls back to self-generated. Full VP API suite stays green (34/34); VP unit suite stays green (126/126); ruff clean.
+18 -4
View File
@@ -17,14 +17,28 @@
"description": "Material type",
"options": [
{"value": "PLA", "label": "PLA"},
{"value": "ABS", "label": "ABS"},
{"value": "PLA-CF", "label": "PLA-CF"},
{"value": "PLA-GF", "label": "PLA-GF"},
{"value": "PLA-AERO", "label": "PLA Aero"},
{"value": "PETG", "label": "PETG"},
{"value": "TPU", "label": "TPU"},
{"value": "PETG-CF", "label": "PETG-CF"},
{"value": "ABS", "label": "ABS"},
{"value": "ABS-GF", "label": "ABS-GF"},
{"value": "ASA", "label": "ASA"},
{"value": "ASA-CF", "label": "ASA-CF"},
{"value": "ASA-GF", "label": "ASA-GF"},
{"value": "PC", "label": "PC"},
{"value": "PCTG", "label": "PCTG"},
{"value": "PA", "label": "PA (Nylon)"},
{"value": "PA-CF", "label": "PA-CF"},
{"value": "PAHT-CF", "label": "PAHT-CF"},
{"value": "PA6-CF", "label": "PA6-CF"},
{"value": "PA6-GF", "label": "PA6-GF"},
{"value": "PET-CF", "label": "PET-CF"},
{"value": "PC", "label": "PC"},
{"value": "ASA", "label": "ASA"},
{"value": "TPU", "label": "TPU"},
{"value": "PPS", "label": "PPS"},
{"value": "PPS-CF", "label": "PPS-CF"},
{"value": "PPS-GF", "label": "PPS-GF"},
{"value": "PVA", "label": "PVA"},
{"value": "HIPS", "label": "HIPS"}
]
@@ -0,0 +1,53 @@
"""Regression pin for the filament_type select options exposed by
GET /cloud/fields/filament — used by the Profiles edit modal.
Issue #1686: PLA-CF and other CF/GF variants were missing from the dropdown,
so users could not tag custom presets with the correct material type.
"""
import json
from pathlib import Path
import pytest
FIELDS_PATH = Path(__file__).resolve().parents[2] / "app" / "data" / "filament_fields.json"
@pytest.fixture(scope="module")
def filament_type_options() -> set[str]:
with FIELDS_PATH.open() as f:
data = json.load(f)
field = next(f for f in data["fields"] if f["key"] == "filament_type")
return {opt["value"] for opt in field["options"]}
@pytest.mark.parametrize(
"material",
[
# Reported in #1686
"PLA-CF",
# Other Bambu CF / GF / specialty variants that share the same gap
"PLA-GF",
"PLA-AERO",
"PETG-CF",
"ABS-GF",
"ASA-CF",
"ASA-GF",
"PCTG",
"PAHT-CF",
"PA6-CF",
"PA6-GF",
"PPS",
"PPS-CF",
"PPS-GF",
],
)
def test_filament_type_includes_carbon_and_glass_fiber_variants(filament_type_options: set[str], material: str) -> None:
assert material in filament_type_options
def test_filament_type_keeps_baseline_materials(
filament_type_options: set[str],
) -> None:
baseline = {"PLA", "PETG", "ABS", "ASA", "PC", "PA", "PA-CF", "PET-CF", "TPU", "PVA", "HIPS"}
assert baseline.issubset(filament_type_options)