Note
This is a daily beta build (2026-05-06). It contains the latest fixes and improvements but may have undiscovered issues.
Docker users: Update by pulling the new image:
docker pull ghcr.io/maziggy/bambuddy:daily
or
docker pull maziggy/bambuddy:daily
**Tip:** Use [Watchtower](https://containrrr.dev/watchtower/) to automatically update when new daily builds are pushed.
Added
- Slicer Bundle (.bbscfg) import — pick presets from a stored bundle instead of resolving cloud/local/standard PresetRefs every slice — Closes the long tail of preset-resolution corner cases (cloud presets behind login, "from User" sentinel handling, the
#-prefix clone trick, danglinginheritson renamed parents, etc.) by letting users upload a BambuStudio "Printer Preset Bundle" (.bbscfg) once per printer and pick from it for every subsequent slice. Service layer (backend/app/services/slicer_api.py):BundleSummary/BundleNotFoundErrortypes,import_bundle/list_bundles/get_bundle/delete_bundlemethods,slice_with_bundlewhich posts/slicewith bundle id + per-category preset names instead of the JSON triplet. Routes (/api/v1/slicer/bundles, all gated onPermission.LIBRARY_UPLOAD):POST/GET/GET :id/DELETE :id. All routes proxy via_resolve_slicer_api_urlso they follow the user'spreferred_slicersetting (bambu_studio vs orcaslicer). Status-code mapping treats sidecar 4xx as 400,BundleNotFoundErroras 404, sidecar unreachable as 503, and sidecar 5xx as 502. Preview-slice (backend/app/services/slice_preview.py::get_preview_filaments) picks up optionalbundle_id+printer_name+process_name+filament_namesparams and routes throughslice_with_bundlewhen set; the cache key picks up a bundle-context fingerprint so different bundle picks on the same file occupy distinct entries — gram numbers in the preview now match what the real print will produce instead of being derived from the file's embedded process settings (which can drift from the triplet the actual slice would use). Thelibrary.pyandarchives.py/filament-requirementsroutes forward the new params. Dispatch (SliceRequest.bundle: SliceBundleSpec): when set,_run_slicer_with_fallbackskipsresolve_preset_refand callsslice_with_bundle; the validator skips the preset-required check so bundle-only requests validate. 3MF + bundle CLI 5xx still falls back to the embedded-settings slice path (used_embedded_settings=Truesurfaces in the response), and sidecar 404 (unknown bundle / preset name) maps to 400. Frontend SliceModal Bundle tier: new "Slicer bundle" picker at the top of the modal, rendered only when at least one bundle is imported (GET /slicer/bundlesnon-empty). Selecting a bundle replaces cloud / local / standard preset dropdowns with bundle-scoped pickers (process + per-slot filament names from the bundle) — printer is implicit (each.bbscfghas exactly one). "None" leaves the modal on the original preset-triplet path. Submit routes throughSliceRequest.bundleso the backend skips PresetRef resolution and asks the sidecar to materialise the JSON triplet from the stored bundle by name. Frontend types:SliceBundleSpec+bundle?: SliceBundleSpeconSliceRequest;getLibraryFileFilamentRequirements/getArchiveFilamentRequirementsaccept an optional 4th-arg bundle context object. The orca-slicer-api fork's bundle endpoints (shipped onbambuddy/bundle-import) are the server side of this — see the slicer-api sidecar docker-compose for the matching versions.
Fixed
- Filament usage double-counted when AMS auto-falls-back to a same-material spool (#957) — When one spool ran out mid-print and the AMS transparently switched to a sibling slot loaded with the same material, the usage tracker credited the originally-mapped spool with the full 3MF estimate AND added the fallback spool's remain%-delta on top — so a 78 g print could show as 78 g + 60 g = 138 g consumed across the two spools, leaving the empty spool's recorded weight beyond its label weight (the symptom the original report flagged on a 1209 g spool reading "1188.30 g used" while the new spool only got a 30 g credit). Two interacting bugs: (1) the tray-change recorder in
bambu_mqtt.pygated onstate in ("RUNNING", "PAUSE")literal strings, and P2S firmware briefly transitions out of RUNNING during the AMS swap, so the switch was never appended totray_change_log; (2) the usage-tracker splitting branch inusage_tracker.pywas gated onnot slot_to_tray, so even when the tray-change log was populated the splitting code only ran for prints where the slicer's mapping had not been captured — i.e. never on the actual fallback case. Fix: thebambu_mqtt.pygate now keys on the print-lifecycle flags (_was_running and not _completion_triggered) so any tray change between print start and completion is captured regardless of the momentarygcode_statestring. Theusage_tracker.pygate is split sotray_change_logevidence with > 1 entries always takes over fromslot_to_tray, treating the per-segment per-layer gcode usage as the source of truth when the printer actually fed from multiple trays. Path 2 (AMS remain%-delta fallback) then naturally skips both trays because they're already inhandled_traysafter splitting, eliminating the double-credit. Tests: newtest_tray_change_recorded_during_intermediate_stateandtest_tray_change_not_recorded_after_completionintest_bambu_mqtt.pyexercising the new gate; newtest_tray_switch_overrides_print_cmd_mappingintest_usage_tracker.pypinning that withams_mapping=[0]set andtray_change_log=[(0,0),(1,30)]the splitter produces two segments summing to the 3MF estimate (no double-count) and adds both(0,0)and(0,1)tohandled_trays. - 3D Preview returned
{"detail":"Not Found"}in Docker installs (#1218) — The embedded GCode viewer's static assets (gcode_viewer/) were not copied into the production Docker image, so clicking "3D Preview" on any archive loaded an iframe at/gcode-viewer/?archive=<id>that returned a bare FastAPI 404 — Firefox / Chrome rendered the JSON response inside the iframe area while the outer Bambuddy layout looked normal, masking the failure unless the user actually inspected the iframe. The Vite production build doesn't stagegcode_viewer/intostatic/either (the dev server serves it via aconfigureServermiddleware that's dev-only), and the only integration test for the route accepted404as a valid outcome ("assert response.status_code in (200, 404)") so CI never caught the missing files. Affected every Docker build since the embedded viewer landed in 0.2.4b1 (commit3adce435, 2026-04-22). Fix:Dockerfilenow copies thegcode_viewer/directory alongside the React build output. Defence in depth:backend/app/main.pylogs an ERROR at startup when_gcode_viewer_dir / "index.html"is missing so future packaging gaps surface indocker logsand the support bundle instead of as silent runtime 404s. Test guard:backend/tests/integration/test_gcode_viewer.pyaddstest_gcode_viewer_index_served_when_assets_presentwhich skips when the directory is intentionally absent (unit-test environments) but asserts200 OK+ a non-empty HTML body when the assets do exist on disk — so a future brokenCOPYfails CI loudly rather than continuing to ship a broken image. - Slice button no longer enabled before the preview slice resolves — Until the preview slice (or embedded-metadata read for already-sliced 3MFs) returned the per-plate filament list, the SliceModal rendered a synthetic single-slot fallback so the auto-pick had something to bind against. That made the Slice button enabled the moment the modal opened, even before the slicer had told us which AMS slots the plate actually consumes — clicking would dispatch against opaque defaults and the real-life print would either pick the wrong filament or fail with a slot-mismatch error after the fact. Adds
filamentReqsQuery.isSuccessto theisReadychain so the button stays disabled while the preview slice is in flight (or before the backend's/filament-requirementscall settles for sliced files) and flips to enabled the moment the real slot list lands and auto-pick fills it. - Docker data-volume ownership normalised at startup via gosu entrypoint (#1211) — Two long-standing failure modes have been biting Docker users repeatedly: (1) Docker named volumes are created by the daemon as
root:root, and the previouschmod 777 /app/dataDockerfile workaround only covered the named-volume root — so subdirs Bambuddy creates at runtime (virtual_printer/uploads,virtual_printer/certs, etc.) inherited wrong ownership when the container ran as1000:1000. (2) The shippeddocker-compose.ymlships./virtual_printer:/app/data/virtual_printeruncommented, and dockerd creates a missing bind-mount source on the host as root before the container starts — leaving the host directory unwritable by uid 1000 inside the container even though the named volume above it had the chmod-777 workaround. Symptom either way:[Errno 13] Permission denied: '/app/data/virtual_printer/uploads', no virtual printer ever starts, "VP doesn't work" support reports follow. Replaces the chmod-777 hack with a proper entrypoint:deploy/docker-entrypoint.shruns as root, chowns/app/dataand/app/logs(and/app/data/virtual_printerwhen bind-mounted) toPUID:PGID, then drops to that uid viagosubeforeexec'ing the app. The chown is gated behind a top-level ownership check so subsequent restarts skip the recursive traversal — no multi-second startup penalty on multi-GB archive directories. A sentinel.bambuddyfile in each data path prevents Docker from re-syncing image directory metadata on every mount (otherwise empty volumes have their ownership reverted from the image on each restart, defeating the idempotency). When the container is started with an explicituser:directive or--userflag the entrypoint detects it isn't root and falls through to directexec— preserving compatibility for users who pin a specific uid. Compose template changes: removesuser: "${PUID:-1000}:${PGID:-1000}"(the entrypoint owns privilege drop now), addsPUID/PGIDenv vars with the same defaults, and comments out the./virtual_printer:/app/data/virtual_printerbind mount by default with explicit "only needed if you also run a native install of Bambuddy on the same host and want both to share the VP CA cert" guidance. The entrypoint chowns the host-side dir through the bind mount the first time it sees wrong ownership, so existing uncommented installs continue to work and #1211 specifically gets fixed.