Note
This is a daily beta build (2026-04-21). 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.
Improved
- Printer Card Shows Plate Name on Multi-Plate Prints (#881) — When two printers were running different plates of the same multi-plate 3MF, the Printers page cards displayed the same file name on both and gave no visual way to tell them apart. The Queue view already showed the plate name by querying the archive's plate list; the Printers page didn't have that linkage. The
GET /printers/{id}/statusendpoint now returnscurrent_archive_id(resolved by matching the MQTTsubtask_idagainstPrintArchive.subtask_id, the same bridge introduced in #972 for restart-resume) andcurrent_plate_id(parsed from the MQTTgcode_filepath by a new sharedparse_plate_idhelper that's also used by the WebSocket push path, so plate transitions within a running print reflect immediately instead of waiting 30 s for the next REST poll). The card fetches plate metadata via the sameapi.getArchivePlates()call the Queue page uses — shared React Query cache keeps it cheap across polls — and renders the actual plate name (or a "Plate N" fallback) only when the source 3MF is multi-plate, so single-plate prints stay noise-free. Falls back to the previousplate_(\d+).gcoderegex when there's no archive linkage (e.g. prints started directly from the printer LCD). Regression tests cover the plate-id extraction across Bambu Studio path shapes and the label-override precedence informatPrintName. Thanks to stringham for the follow-up and screenshot.
Fixed
- AMS Slot Configure: Custom Cloud Preset Resolves to "Generic" in Slicer & Printer LCD (#1053 follow-up) — After configuring any AMS slot (HT or regular) with a user custom Bambu Cloud preset built on top of a Bambu base profile (e.g. "Sting3D ABS" inheriting from "Generic ABS BBL H2D"), OrcaSlicer's Sync Filaments continued to resolve the slot to "Generic ABS" and the custom preset never appeared on the printer's own LCD — independent of the earlier UI fix (commit
87a5aa36) which only corrected Bambuddy's own modal. Root cause: when Bambu Cloud'sGET /cloud/settings/{setting_id}returns a user preset withfilament_id: nullandbase_id: "GFSB99_07"(cloud doesn't mint a distinct filament_id for presets that only override fields of a generic base),ConfigureAmsSlotModal.tsx:382-384fell back toconvertToTrayInfoIdx(base_id)which strips the version suffix and theSprefix →"GFB99"— Generic ABS's filament_id. The printer accepted and reported backGFB99, so both the LCD and OrcaSlicer correctly resolved the slot to Generic ABS. The fallback was never right: the preceding default already settray_info_idx = convertToTrayInfoIdx(selectedPresetId)which for anyPFUS*/PFSP*setting_id returns the base setting_id itself (via the helper'sstartsWith('PFUS')branch added earlier), and the printer + both slicers round-trip that format unchanged — confirmed by existing backend integration tests (test_configure_pfus_sent_directly,test_pfus_slicer_filament_used_directly), by the print scheduler's slot-matching which already expectsP*short-form IDs in the printer's reportedtray_info_idx(print_scheduler.py:910), and by the inventory Assign Spool flow which has been sendingPFUS*preset IDs to the printer for months. The buggy fallback overwrote the correct default with a generic mapping. Fixed by removing the base_id branch: when cloud detail carries a distinctfilament_idwe still prefer it, otherwise we keep the setting_id-derived default. BambuStudio Sync now resolves the custom preset cleanly; OrcaSlicer (whose user presets don't carry afilament_idfield at all, onlyinherits) will continue to fall back to the inherited generic — that's an OrcaSlicer preset-format limitation, not something Bambuddy can fix on its side, and the behaviour is strictly not worse than before. Regression tests inConfigureAmsSlotModal.test.tsxpin four paths: (1) cloud detail withfilament_id: null→tray_info_idxis thePFUS*setting_id, (2) cloud detail with a concretefilament_id→ that filament_id wins over the default, (3) GFS* Bambu presets skip the cloud-detail fetch entirely and still map to the shortGF*filament_id, and (4) a 5xx / network error on the cloud-detail fetch degrades gracefully to thePFUS*default instead of aborting the configure flow. An end-to-end backend test (test_configure_pfus_preserves_setting_id_pair) locks in that bothtray_info_idx=PFUS…andsetting_id=PFUS…survive the HT-slotPOST /slots/{ams}/{tray}/configurepath untouched. Thanks to mrnoisytiger for the detailed browser-console / network / backend-log diagnostic data that isolated the fallback path, and for sharing the OrcaSlicer preset JSON that showed the missingfilament_idfield. - Single Malformed
rgbaBricks the Entire Filaments Inventory Page (#1055) — A user's Filaments page went blank and "Add Spool" became a no-op with no visible error. The backend was returning HTTP 500 fromGET /api/v1/inventory/spoolswithfastapi.exceptions.ResponseValidationError: rgba → 'FFFFFFF' should match pattern '^[0-9A-Fa-f]{8}$'— a single legacy spool row had a 7-char rgba (missing one trailingF) and Pydantic's strict pattern onSpoolResponserefused to serialize the whole list because of it. Root cause spans three layers: (1)SpoolUpdatehad no rgba pattern constraint, so PATCH calls could plant malformed values straight into the DB (SpoolCreatedid validate, but only on initial create); (2) theColorSectionhex input's onChange ternaryval.length <= 6 ? 'FF' : ''silently emitted 7-char strings for 5-char or 7-char typed input (5 chars +FFalpha = 7 chars; 7 chars got no alpha appended at all), which then flowed to the unvalidated PATCH endpoint; (3)SpoolResponseinherited the same pattern asSpoolCreate, so any malformed row already in the DB exploded the entire list endpoint on serialize even though write-side validation was the right place for the check. Fixed on all three layers:SpoolUpdate.rgbanow carries the same^[0-9A-Fa-f]{8}$pattern asSpoolCreate, so PATCH requests with malformed rgba are rejected with 422 at the boundary. The hex input always emits a fully-formed 8-char RRGGBBAA on every keystroke — 8-char paste passes through, 7-char drops the stray char, shorter input is right-padded with'0'and given FF alpha.SpoolResponse.rgbais now an unconstrainedOptional[str]: the pattern belongs on request schemas where Pydantic can reject bad input, not on responses where it turns a single bad row into a total page failure. A legacy malformed row still appears in the UI (the color just renders as whatever browser default applies) but the user can see, edit, and delete it instead of having to hand-edit SQLite. Backend tests cover all three schema contracts (16 cases acrossSpoolCreateaccept/reject,SpoolUpdateaccept/reject,SpoolResponselenient-tolerance on 7-char / null / garbage). Frontend tests cover the hex-input normalization for every input length 0–8 plus non-hex strip-and-pad. Thanks to fdsghy4a for the end-to-end debugging and for locating the exact malformed row in their DB. - Printer-Card "Print" Button Leaves Transient Copy in File Manager (#730) — The "Print" button on a printer card (and the equivalent drag-drop-onto-card flow) was silently uploading the chosen file into the Library file manager as a side effect before printing. Root cause is structural: the frontend opened
FileUploadModalto persist the file as aLibraryFile, thenPrintModaldispatched a library print throughPOST /library/files/{id}/print, which uses the LibraryFile as the source for both the archive copy and the FTP upload to the printer. When the dispatch finished, both theLibraryFilerow and its disk file indata/library/were left behind, so every one-off Direct-Print accumulated an unwanted File Manager entry that the user had to find and delete manually. The other three print entry points are untouched: Archive "Reprint" never involved the library, and File Manager "Print" / Project Detail "Print" are paths where the user deliberately put the file in the library, so their entries are preserved.POST /library/files/{id}/printnow accepts an optionalcleanup_library_after_dispatchboolean. When true,_run_print_library_filestages the LibraryFile row for deletion in the same transaction as the archive insert (so a mid-flight FTP orstart_printfailure rolls back both at once, leaving no orphan), commits together, then unlinks the library disk file and thumbnail from disk after commit succeeds. External library files (is_external = True, pointing at user-managed folders outside Bambuddy's control) are never touched regardless of the flag. The Printers-page Direct-Print flow is the only caller that sendstrue; every otherapi.printLibraryFilecall site leaves the flag unset so default-False preserves their library entries. Added two unit tests at the enqueue level (default-false + flag-propagates-true), two integration tests at the endpoint level (default-false + forwards-true + cleanup flag never leaks into the MQTT options dict), and two frontend tests onPrintModalguarding thatcleanupLibraryAfterDispatchonly forwards when explicitly set — so future File Manager / Project Detail entry points can't accidentally inherit the Direct-Print semantics. Thanks to 3823u44238 for flagging the surprising side effect. - Direct / File Manager / Library Prints Still Unattributed to User (#730) — The 0.2.3.1 fix (commit
f03d0c4c) plumbed the authenticated user fromPOST /library/files/{id}/printinto the background-dispatch job object, but the dispatcher itself never read it back out:_run_print_library_filecalledArchiveService.archive_print()without thecreated_by_idparameter and never calledprinter_manager.set_current_print_user(). Net effect: direct prints from the printer-card "Print" button, File Manager prints, and Library prints all continued to land archives withcreated_by_id = NULL(invisible to the per-user stats filter), and the post-print email notification had no user to target. The dispatcher now forwardsjob.requested_by_user_idto the archive at creation time and registers the current-print user afterstart_printsucceeds — matching the reprint path's behaviour. Reprint-from-Archive attribution is a separate bug (the reprint reuses the source archive row as-is, so a NULLcreated_by_idstays NULL) and is tracked on #730. Thanks to 3823u44238 for the thorough end-to-end retest. - Spoolman Iframe Blocked by CSP on HTTP Instances (#1054) — The Filament tab showed a blank page with a brief Spoolman flash on reload. Browser console reported
Content-Security-Policy: The page's settings blocked the loading of a resource (frame-src) at http://<host>:7912/spool because it violates the following directive: "frame-src 'self' https:". Root cause: commit53a70e37(#995) tightened the CSP to allow external sidebar iframes but only whitelistedhttps:, overlooking that self-hosted services on LANs — Spoolman, OctoPrint, etc. — almost always run over plain HTTP. Theframe-srcdirective now allowshttp:as well (frame-src 'self' http: https:), matching theconnect-src 'self' ws: wss:pattern already used for WebSockets.frame-ancestors 'none'still prevents Bambuddy itself from being framed cross-origin. Thanks to saint-hh for reporting. - AMS-HT: Custom Filament Preset Reverts to "Generic" in UI After Configure (#1053) — After configuring an AMS-HT slot (HT-A/HT-B) with a custom Bambu Cloud preset (e.g. "Devil Design PLA Basic"), the slot card and Configure modal kept showing "Generic PLA" even though the
ams_filament_settingcommand succeeded and BambuStudio / the printer's LCD both rendered the correct custom preset. Root cause: theGET /api/v1/printers/{id}/slot-presetsendpoint keyed its response dict byams_id * 4 + tray_id, which collapses cleanly to the same integer the frontend uses for regular AMS slots (0 through 15) but produces128 * 4 + 0 = 512for HT-A — a key nothing looks up. The frontend's PrintersPage HT render path callsgetGlobalTrayId(ams.id, …, false)which returns the ams_id itself (128for HT-A), and SpoolBuddy's AMS page used a third, unrelated formula ((amsId - 128) * 4 + trayId + 64 = 64). All three agreed for regular AMS so the mismatch only surfaced on HT, where the saved preset name never reached the UI and the render fell through totray.tray_type→ rendered as "Generic PLA". Backend now keys the response via a_slot_preset_keyhelper that mirrors frontendgetGlobalTrayId(HT →ams_id, regular/external →ams_id * 4 + tray_id), and SpoolBuddyAmsPage uses the sharedgetGlobalTrayIdhelper instead of its home-grown formula. Regression test covers the key scheme for regular, HT, and external slots. Thanks to mrnoisytiger for the detailed reproduction. - ⚠️ Bed-Jog "Home Z" Could Crash the Bed Into the Toolhead (#1052) — Critical safety fix. On H2C (and by extension any Bambu printer where Z-home moves the bed UP toward an endstop — H2D, H2S, and X1 family all share this kinematics) the bed-jog modal's "Home Z" button sent a raw
G28 Zover thegcode_lineMQTT command. BareG28 Zskips the toolhead-park step that a fullG28runs first, so the bed raised without stopping at a safe height — in the reporter's case the toolhead happened to be parked on the purge chute and no damage was caused, but hitting the button with a toolhead anywhere else would have driven the bed into it at full Z speed. Root cause was the/api/v1/printers/{id}/home-axesendpoint's per-axis gcode mapping ("z" → "G28 Z","xy" → "G28 X Y","all" → "G28"). The endpoint now ignores theaxesargument entirely and always sends a bareG28, which Bambu firmware expands into the safe multi-step sequence (park toolhead → home XY → home Z). The MQTT client helperBambuClient.home_axes()has the same change. The bed-jog modal is retitled "Auto Home" and its copy now says "parks the toolhead, then homes X, Y, and Z" so users aren't surprised when X/Y motion happens first. After a successful Auto Home click, the modal no longer re-prompts on the next jog in the same session — the "not homed" warning is gated on a session-scoped acknowledgement flag that was only being set by "Move anyway" and now also fires on successful Auto Home. Regression test covers all three axes arguments producing the same bareG28. Thanks to mikefromdot for catching this with an undamaged retest. - AMS: Configure / Assign Spool Hidden on Reset Slots, and Assign Spool Missing Matching-Material Inventory (#1047) — Two separate symptoms from the same report. (1) After resetting an AMS slot from the printer UI, the Bambuddy printer card showed "Empty Slot" with no Configure or Assign Spool actions on hover, while the same slot in SpoolBuddy's AMS page still let the user re-configure it. Root cause: commit
c9efa4b8(#784) added atray?.state === 10gate to theEmptySlotHoverCardactions, intended to show the buttons only when a spool was physically present but not loaded (state=10) and hide them on truly empty slots (state=9). In practice, firmware often reportsstate=9(or nostatefield at all) after a user-initiated reset — even when a spool is still physically in the slot — so the actions disappeared exactly when the user needed them. The gate is redundant anyway (EmptySlotHoverCardis only rendered when the slot has notray_type, so it's definitionally empty from Bambuddy's perspective), and configuring an empty slot is a valid "tell the printer what will be loaded here" operation. The gate is now removed at both the standard-AMS and AMS-HT render paths. (2) After configuring a slot with a Generic profile (e.g. "Devil Design PLA Basic Red"), the Assign Spool modal didn't list the matching inventory spool unless the user enabled the "Show all spools" toggle. Root cause: the filter atAssignSpoolModal.tsx:144requirednormalizeValue(spool.slicer_filament_name) === normalizeValue(trayInfo.profile)— manually-added inventory spools typically don't haveslicer_filament_namepopulated, so they failed the exact-profile check even when the material matched. The filter now prefers an exact slicer-profile match when both sides advertise one, and falls back to partial material match in either direction (so e.g. a spool withmaterial="PLA"is selectable for a slot reporting"PLA Basic") when profile info is missing. (3) Once the matching spool was assignable, a "profile mismatch" confirmation dialog still warned on every assignment because Bambu Studio / OrcaSlicer slicer-profile names carry a printer/nozzle/variant qualifier after `` (e.g."Devil Design PLA Basic Bambu Lab H2D 0.4 nozzle (Custom)") while the tray stores only the bare base name (`"Devil Design PLA Basic"`), and `checkProfileMatch` compared the full strings. Both the filter and the mismatch check now strip the `…` qualifier before comparing, so identical base profiles are treated as a match. Regression test covers a spool with no slicer profile being surfaced for a slot whose profile + material are both set. Thanks to TravisWilder for the report. - Skip Objects: Enlarged Preview Image Fails to Load on Auth-Enabled Instances (#1046) — Clicking the mini print-pr