# Changelog All notable changes to Bambuddy will be documented in this file. ## [0.2.5b2] - Unreleased ### Fixed - **Multi-nozzle prints no longer collapse all filaments onto one nozzle (#1825, reporter @needo37)** — The single-active-extruder shortcut added in #851 (for #827) at `threemf_tools.py:354` runs `before` the per-filament `group_id` mapping, and fires whenever `extruder_nozzle_stats` reports exactly one extruder as having a nozzle installed. On the H2D / H2D Pro / X2D (2-nozzle) and H2C (3+-nozzle tool-changer), this field is data-driven from the slicer profile's enumerated nozzle volume types — when an HT-AMS or High-Flow nozzle's type isn't enumerated in the slice's profile (common with asymmetric extruder setups, e.g. HT-AMS feeding the right nozzle on an H2D), the slicer emits e.g. `['Standard#1', 'Standard#0']` even though the print genuinely uses both extruders. `sum(active_extruders) == 1` triggered → every filament was force-assigned to `physical_extruder_map[active_idx]`, the authoritative per-filament `group_id` was discarded, and the Filament Mapping panel showed both filaments badged **L** with the auto-match hard filter (`print_scheduler.py` `_compute_ams_mapping_for_printer` ~line 1239) blocking the wrong-nozzle tray as "Type not found". Bug is **parser-side and model-agnostic** — triggers purely on 3MF data shape, not on the attached AMS hardware: regular dual-AMS H2D installs typically slice to `['Standard#1', 'Standard#1']` (sum==2) and never enter the buggy branch, which is why this bug was invisible on the most common dual-AMS setup. Physical nozzle routing was **not** affected — the actual extrude path comes from the sliced gcode + the verbatim `nozzle_mapping` from the project_file (#1780), not from this parse — so the bug surfaced as auto-match failure + wrong L/R badge, not wrong-nozzle extrusion. **Fix.** Gate the single-active shortcut on `len(distinct_group_ids) <= 1` from `slice_info.config`. The slice_info parse is hoisted above the shortcut check (and reused by Priority 1) so the gate adds zero extra I/O. When the slice contains ≥2 distinct group_ids, the shortcut skips and the existing `group_id`-based Priority 1 mapping runs. The gate only **narrows** the shortcut path — it can't widen the buggy collapse onto any previously-working slice. The same condition generalizes to H2C and any future N-nozzle printer for free (no nozzle-count branching). **Tests.** Two new cases in `TestExtractNozzleMappingFrom3MF`: `test_single_active_under_report_with_multi_group_falls_through` pins the #1825 regression (`['Standard#1','Standard#0']` + group_ids `{0,1}` → `{1:1, 2:0}` not `{1:1, 2:1}`); `test_single_active_with_single_group_still_uses_shortcut` preserves the #851 behaviour (same stats + only `group_id=0` → shortcut still fires → `{1:1, 2:1}`). Existing `test_single_active_extruder_maps_all_slots` and `test_two_active_extruders_falls_through` stay green. **Suites.** `pytest -n 30 backend/tests/unit/test_scheduler_ams_mapping.py backend/tests/unit/test_scheduler_filament_deficit.py backend/tests/unit/test_scheduler_filament_override.py backend/tests/unit/test_fallback_archive_mqtt_filament.py backend/tests/integration/test_archives_api.py backend/tests/integration/test_library_api.py` 272/272 green. `ruff check backend/` clean. **Scope.** Backend-only, parse layer. No DB migration. No new permission. No frontend change. The L/R-only badge limitation on 3+-nozzle printers (H2C tool-changer) called out in the report is a separate cosmetic follow-up and not part of this fix. - **Assign-spool picker note now visible on mobile (#793 follow-up, reporter @EmcetPL)** — The original fix for #793 added the spool note as an HTML `title=` tooltip on each picker button in `AssignSpoolModal.tsx` (lines 417 + 492). `title=` only surfaces on hover, which doesn't exist on touch devices — a phone user tapping a card just selects it, the note never appears. Users who store their tracking ID in the note field were blind on mobile. **Fix.** Render the note as a small muted truncated line directly under the weight on both the internal-inventory branch and the Spoolman branch: `text-[10px] text-bambu-gray/70 mt-1 truncate`, kept inside the `truthy &&` guard so empty notes don't add a blank row. The existing `title={spool.note}` is preserved on the new `
` element so desktop hover and mobile-browser long-press still surface the full untruncated text for notes that overflow the truncate. Keeps the 2-col mobile grid density unchanged (one extra `text-[10px]` line is ~12 px), no new state, no popover/modal, no new touch target. Mirrored across both branches per the inventory-parity rule so internal and Spoolman pickers stay shape-equal. Frontend `npm run build` clean. `npx vitest run AssignSpoolModal.test.tsx AssignToAmsModal.test.tsx` 23/23 green. **Scope.** No backend change. No new permission. No new i18n key (the note text is user-authored, not translatable). - **API keys with Manage Library permission can now rename / delete / move library files (#1832, reporter @MorganMLGman)** — `require_ownership_permission` gates API keys on `all_perm` only (line 1668) — the comment block at line 1659 says OWN and ALL "both map to the same scope flag" for queue / archives / etc., so checking `all_perm` is the correct gate. Library deliberately broke that invariant by putting `LIBRARY_UPDATE_ALL` / `LIBRARY_DELETE_ALL` in `_APIKEY_DENIED_PERMISSIONS` while only the OWN variants were allowlisted under `can_manage_library`. Net effect: every library curation route (DELETE `/library/files/{id}`, PUT `/library/files/{id}` rename, POST `/library/files/move`) returned `403 "API keys cannot be used for administrative operations"` for keys with `can_manage_library=True`, contradicting the wiki docs that explicitly list "rename and delete your own library entries" under that scope. Only `POST /library/files/{id}/slice` worked (it doesn't go through `require_ownership_permission`). The "ALL stays admin-only because it crosses the user boundary" comment was internally inconsistent: API keys have no per-row ownership identity (`user=None`), so the route's `file.created_by_id != user.id` ownership check would `AttributeError` on a key acting under OWN anyway — the only path that ever worked was `can_modify_all=True`, which `all_perm` denial blocked outright. **Fix.** Fold `LIBRARY_UPDATE_ALL` and `LIBRARY_DELETE_ALL` into `_APIKEY_SCOPE_BY_PERMISSION` mapping to `can_manage_library` (matching the `can_queue` precedent — both `QUEUE_UPDATE_OWN` and `QUEUE_UPDATE_ALL` map to `can_queue` for the same per-key-identity reason). Remove both from `_APIKEY_DENIED_PERMISSIONS`. `LIBRARY_PURGE` deliberately stays denied — it bypasses the soft-delete window and is genuinely destructive, the kind of cross-boundary op the denylist exists for. **Tests.** 5 new cases in `TestLibraryPermissions` pinning the route-level contract — `test_apikey_with_manage_library_can_delete_file`, `test_apikey_with_manage_library_can_rename_file`, `test_apikey_with_manage_library_can_move_file`, `test_apikey_without_manage_library_still_blocked` (regression guard that the fix widens the allowed-permission set, not the per-key scope check), and `test_apikey_with_manage_library_still_cannot_purge` (LIBRARY_PURGE stays admin-only). The matrix drift-detection in `test_auth_apikey_rbac.py` updated to include `LIBRARY_UPDATE_OWN`, `LIBRARY_UPDATE_ALL`, `LIBRARY_DELETE_ALL` under `can_manage_library` and removes `LIBRARY_DELETE_ALL` from `_ADMIN_CASES`. `pytest -n 30 backend/tests/unit backend/tests/integration` green (6494). Ruff clean. **Scope.** No DB migration. No schema change. No frontend change. The wiki entry for API key permissions at `/features/api-keys/#available-permissions` now matches actual behaviour. - **Administrators system group self-heals to include every current permission on upgrade — covers `printer_sensor_history:read` and every future new permission** — Fresh installs bootstrap the Administrators group with `ALL_PERMISSIONS` (every value in the `Permission` enum), so a fresh install always has the full set. On upgraded installs, `seed_default_groups()` in `backend/app/core/database.py` previously only backfilled the specific permissions explicitly listed in one-off migration blocks (`library:purge`, `archives:purge`, the OWN/ALL read-flag split, `orca_cloud:auth`, `pipelines:*`, …). Any permission added to the enum without a matching block silently stayed missing on existing admin rows, leaving admins gated out of the feature it controlled. The most recent gap was `printer_sensor_history:read` (Read Printer Sensor History was never granted to upgraded admin groups, so the Sensor History charts read as 403 for admins on installs seeded before that permission existed). **Fix.** Replaced the per-permission admin backfills with a single sync block: for the Administrators system group, append every value in `ALL_PERMISSIONS` that isn't already on the row. Additive only — custom permissions added by hand (e.g. plugin permissions, hand-edited rows) are preserved. The legacy admin-only backfills (`library:purge`/`archives:purge` block, the OWN/ALL read-flag block including `orca_cloud:auth` and the legacy `archives:read`/`library:read`/`queue:read` UI gates, and the Administrators branch of the pipeline backfill) are retired since they're subsumed by the sync. Non-admin backfills (Operators / Viewers OWN-tier read flags, Operators `orca_cloud:auth`, pipelines for non-admin groups, MakerWorld + `printers:clear_plate` cross-group adders) are untouched. **Tests.** Three new cases in `test_read_permission_backfill_migration.py`: `test_administrators_printer_sensor_history_read_backfilled` (the exact regression reported), `test_administrators_sync_covers_every_current_permission` (generic invariant — every `ALL_PERMISSIONS` value lands on Administrators after the sync, catches any future new permission without a one-off test), and `test_administrators_sync_is_additive_only` (hand-added custom permissions are preserved). 12/12 backfill-migration tests + 102/102 broader permission tests green; ruff clean. - **Slicer Pipelines runs dashboard — native browser `