github maziggy/bambuddy v1.2.6b1-daily.20260813
Daily Beta Build v1.2.6b1-daily.20260813

pre-release3 hours ago

Note

This is a daily beta build (2026-08-13). 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

  • The bed can be held warm between chamber-heated prints, and a heat-soak that already happened is not paid for twice (#2727, contributed by ticfinack) — Back-to-back prints in ASA, ABS, PA or PC each soaked the chamber from cold, even when the print that just finished had left it at temperature. The chamber cooled during the minutes a plate sat waiting to be cleared, and then the next job spent its full soak putting the heat back. Two settings under Queue address that. Keep bed warm between prints holds the bed hot while a printer sits in FINISH waiting for its plate to be cleared and the next queued item needs chamber heat — the bed is the chamber's heating element here rather than a print surface, so the hold runs at the new Keep-warm bed temperature (90 °C by default, which is also above the threshold the common aftermarket chamber heaters switch on at), raised to the print's own bed temperature whenever that is higher. It only engages for filament that actually wants a hot chamber, worked out from what is loaded in the AMS, so a PLA or PETG job is skipped without anyone configuring anything. Alongside it, Bambuddy now samples each printer's chamber temperature into a rolling two-hour history and credits time the chamber has already spent at temperature against the next print's soak, shortening or skipping it. That credit is deliberately hard to earn: it stops at the newest reading, at any gap in the samples, and at the end of the last real dip below target, and a history that has gone stale credits nothing at all — the chamber can cross the threshold unobserved, so the full soak runs instead. A dip only counts once it has lasted six minutes, because an enclosed chamber cannot lose and regain several degrees quickly — measured on an X1C, cooling from 55 °C to below 48 °C takes between 23 and 73 minutes — so a brief low reading is a door being opened, which is exactly what a plate swap does during the window this feature runs in. The hold needs plate-clear confirmation and preheat both switched on, all three re-checked on every pass so turning one off stops the hold rather than leaving it running somewhere the interface can no longer reach. It is bounded by Stop keeping warm after (120 minutes by default): when that elapses the bed is switched off and does not re-arm, so a plate nobody clears cannot leave a bed hot all weekend. The hold is also released when the item is deleted, the queue empties, or a gate is switched off part-way through, and never when the printer reports a bed target other than the one Bambuddy set — a temperature you changed by hand is left alone. Off by default. Covered by backend tests.
  • A slim user listing, so an API client can put names to the ids it already gets back (#1894, reported by MorganMLGman) — Archives, the queue and the statistics endpoints all report ownership as a numeric created_by_id, and statistics accept it as a filter, but there was no way for an API key to find out whose id was whose: the only user listing returns emails, roles, group membership and every account's full permission set, so it is administrative and rejects keys outright. Anyone building against the API was left parsing ids out of somewhere else, or doing without. GET /users/slim now answers with id and username and nothing else. It is covered by the Read Status scope, because a key holding that scope could already filter statistics by any created_by_id it cared to guess — what was missing was only the ability to address the filter, not permission to use it. The full listing stays admin-only, and for groups there is a matching List User Names permission that grants the narrow read without the broad one. If all you need is your own id, GET /auth/me now answers that on its own and no user listing is involved.
  • Cost centers, budgets and a print ledger, for installs where somebody has to be billed (#1448, contributor behrinml, requested in #1065) — A print farm shared by a lab, a makerspace or a department has always been able to see what a print cost, but not to say whose budget it came out of. Bambuddy now has an optional billing layer that answers that. Settings → Workflow → Billing turns it on; it is off by default and nothing about an existing install changes until it is switched on. A cost center is a budget somebody can print against — a team, a course, a customer, a grant. Every user gets a private one automatically, so a personal install is usable the moment billing is enabled, and shared centers are created and staffed by an administrator with per-member permission to print against them. Each center carries an optional total or monthly budget; a center with no budget is explicitly unlimited rather than blocked. When a print is queued against a budgeted center, its cost is reserved rather than merely predicted, so a queue of ten jobs cannot each pass a check the tenth would fail — the reservation is released if the print never happens. The monthly window is configurable: pick the day it resets and the timezone that day is measured in, since a team spread across timezones otherwise disagrees about which month a print landed in. The estimate itself is computed on the server. The figure shown in the print dialog is a display hint, and the browser cannot lower it: budget enforcement uses Bambuddy's own calculation from the file's filament and the printer's rates, whatever the client sends. Completed prints are charged from the archive's measured cost, and a print that is aborted part-way is charged for the filament it actually used rather than the whole job. Every charge, deposit, withdrawal and administrative adjustment lands in a ledger on the new Finance page, which appears in the sidebar only while billing is on. Each charge carries a per-dispatch identity that survives restarts and reprints, so a job cannot be billed twice, and a charge that fails is reported through the UI and any configured notification provider rather than quietly not happening. Balances and budgets deliberately mean different things: a balance records what somebody has spent, and only a cost center's budget can stop a print. Personal balances count unassigned charges and the user's own private center; a print billed to a shared center does not touch the personal one. Four permissions gate the whole thing — cost_centers:read_own, read_all, modify and create — with the default Administrators group holding all four. Separately switchable, and off by default, is a printer kill switch that stops any print that starts on a printer without going through Bambuddy, so a farm that bills its users cannot be bypassed by sending a job straight from Bambu Studio. It stops the print, says so on screen and notifies. It also declines to act whenever it cannot prove ownership — a restart mid-print, a job Bambuddy dispatched but has not finished recording — because stopping a print is irreversible and refusing to act costs only a log line.
  • Nest projects under a master project and see the whole programme in one place (#1264, reporter enjoylifenow) — A project has always been a flat thing: a build with fifty parts and a build with two got the same single row, and the only way to keep a large job legible was to split it into separate projects that then knew nothing about each other. Projects can now be nested. The project dialog has a Parent project picker, so an assembly can sit under the build it belongs to, at whatever depth suits the work; the picker leaves out the project itself and anything already beneath it, because nesting a project inside its own branch is a loop rather than a hierarchy. A project that has sub-projects gains a second card reporting the whole tree at once — print jobs, parts, time, filament and total cost, with progress measured against every target in the tree added together. That card is deliberately separate from the project's own figures, which keep meaning exactly what they meant before: what this project printed, not what its sub-projects did. Each sub-project listed underneath now carries its own branch's totals rather than only a percentage, so the rows add up to the card above them instead of contradicting it. On the Projects page a sub-project is drawn inside its parent's group rather than as another card somewhere in the grid, because two cards that belong together cannot show it while they sit columns apart, however they are captioned; the group is ruled in the parent's own colour and nests as deep as the projects do. A sub-project whose parent is hidden by the status filter stays where it is and says which project it belongs to instead. Two things that only became reachable once the interface could reach them were fixed along the way: a project could be moved under its own sub-project, which the API refused only when a project was made its own direct parent, and a percentage shown against a sub-project was measured differently from the same percentage on the page it linked to. Deleting a project in the middle of a tree now lifts its sub-projects up to its own parent instead of cutting them loose at the top level.
  • Restore selected categories from a Git backup commit (#2714, contributor jmoore-skild, requested in #2656) — Bambuddy has pushed backups to GitHub, GitLab, Gitea and Forgejo for a long time, and every one of those commits was a restore point that nothing could read back. Recovering from a bad settings change, a rebuilt instance or a lost database meant opening the repository by hand and copying JSON into the right places, if you knew which places those were. Settings → Backup & Restore → Restore from Git now picks any of the twenty most recent commits and pulls back the categories you tick — K-profiles, app settings, spool inventory and print history — without touching anything you did not select. The modal previews the commit before anything is written: it shows how many items each category holds and greys out the ones that commit does not contain, so a category you only enabled last week is visibly absent from older commits rather than silently restoring nothing. Overwrite existing entries decides what happens when something already exists locally — off, it fills in what is missing and leaves the rest alone; on, it makes the local row match the backup. The result panel reports what actually happened per category as restored, skipped and failed, and those three always add up to the number the preview showed you, so a count that does not match the preview is a bug rather than something to interpret. Restoring never resurrects a credential: the backup carries MQTT, LDAP, Home Assistant and Prometheus secrets so that a repository is a complete record, but the restore refuses every one of them, and refuses along with them any switch that would be left pointing at a service it can no longer authenticate to — an exposed Prometheus endpoint with no token is worse than one that stays off. The keys that decide who can reach the instance at all are refused outright for the same reason — the four authentication-policy switches, and the whole LDAP family alongside them, since those name which directory server decides who you are rather than how the instance behaves. Authentication is reconfigured through the auth UI, which has the guards that a JSON file does not. Print archives come back as history only, since a Git backup holds metadata and never the 3MF or thumbnail bytes, and each one is returned to its owner by username rather than by user id — an id means nothing on a rebuilt instance, where it would hand one person's print history to whoever now holds that number. An archive whose owner this instance does not have lands unowned with a note saying so, rather than being attributed to a stranger; one that already exists locally keeps the owner it already has, because an owner the backup cannot name is not an instruction to take one away. K-profiles are the one category that leaves the database: they are sent to the printer over MQTT, which means the printer must be online, and writing a slot is always an overwrite there regardless of the toggle — the modal says so before you click rather than in the summary afterwards. Cloud profiles are backed up but deliberately not restorable, as writing them means writing to a Bambu or Orca account rather than to this instance. Restoring is permissioned per category: github:restore opens the dialog, and each category additionally requires the permission that owns its rows — settings:update, inventory:update, archives:update_all and kprofiles:update — so a role cannot write through a restore what it cannot write through the page that owns it. Administrators hold all of them already; a custom role built around the Backup permissions alone can open the dialog and preview a commit, but needs the owning permission for each category you want it to be able to write. Each category is committed as it completes rather than at the end, so a large restore does not hold the database against the rest of Bambuddy for the length of the run; the trade is that a failure part-way through leaves the categories that already finished in place, which the result panel reports rather than claiming nothing was restored. Translated in all locales; wiki updated. Covered by backend and frontend tests.
  • Home Assistant sensors on the printer card, with an optional print interlock (#1148, reporter bsaunder; #448, reporter baudneo) — Bambuddy could already switch a Home Assistant entity as a smart plug, but it had no way to read one. A printer in a home-built enclosure with a door contact, or an A1 with an aftermarket chamber thermometer, had all that data in Home Assistant and none of it in Bambuddy — the reporter's actual problem being that he could not tell whether he had left the enclosure open before starting a print from his phone. Settings → Smart Plugs → Home Assistant Sensors now binds any binary_sensor — a door, window, smoke or moisture contact — or any sensor that carries a reading to a printer, and its state appears on that printer's card. The wording follows Home Assistant's own device class, so a door reads Open or Closed rather than On or Off, and a thermometer reads 41.2 °C; entities with no device class fall back to on/off, exactly as Home Assistant shows them. A sensor can be given an alert condition — on, off, above a value, below a value — which highlights it on the card and unlocks two things it would otherwise be pointless to offer. Notify on alert sends a notification the moment the sensor enters that state, once on the way in rather than on every poll, and not again when a flaky contact drops off the network and comes back still alerting. Hold prints while alerting is the part that answers the original question without anyone having to look: queued jobs for that printer wait, with a reason on the Queue page you can read at a glance ("Waiting on Enclosure Door"), and start by themselves as soon as the door shuts. Nothing is ever cancelled, and a job queued as "Any X1C" simply goes to a sibling whose sensors are clear instead of waiting behind the one that is held. The interlock is deliberately one-directional: it holds only on a sensor that was read successfully and is alerting, so a Home Assistant that is unreachable holds nothing and the queue keeps running as though no interlock were configured. Sensors are their own thing rather than a smart plug with a wider entity filter — a plug carries auto-on, schedules, power alerts and "controls printer power", and the printer card's power button would have happily tried to switch a door contact. One backend poller reads every bound entity every 15 seconds and the cards serve that cached reading, so the cost to Home Assistant does not grow with the number of printers on screen or browser tabs open. Off by default in every respect: a newly bound sensor is display-only until you give it an alert condition, and both the notification and the interlock are separate opt-ins on top of that.
  • Open a File Manager model in your desktop slicer, and pick which one from the 3D preview (#2725, contributor pascalheidmann) — The Slice action on a file card only existed when the optional slicer sidecar was running. Turn the sidecar off — which is the default, and how most installs run — and the File Manager offered no way to get a model into a slicer at all, even though the Archives page has handed files to a locally-installed Bambu Studio or OrcaSlicer over the URI scheme for a long time. The File Manager was simply the one place that never got it. Slice now appears on every unsliced model (.3mf, .stl, .step, .stp) in both the card menu and the list view, and does whichever of the two things your configuration means: with the sidecar on it opens Bambuddy's slice modal and the work happens on the server, and with it off it hands the file to your desktop slicer. The icon says which you will get before you click — a cog for server-side slicing, an external-link arrow for the handoff — and which slicer receives the handoff comes from Settings → Workflow → Slicer → Open in Slicer, falling back to the preferred slicer as it always has. The 3D preview goes further, because that is where you are actually looking at the model and deciding: its slicer button is now a split button, and the chevron beside it offers the alternatives without changing any setting. With the sidecar off that is the slicer you did not pick as your desktop target; with it on, the primary button still slices server-side and the menu offers a one-off desktop handoff to either slicer. Two things that used to be silent now are not: a handoff refused for want of permission raises an error toast rather than launching the slicer at a URL it cannot fetch, where a permission problem looked exactly like "no slicer installed"; and the file-type rule is shared between the card menu and the 3D preview, so a file can no longer offer Slice in one place while showing it greyed out in the other. Permissions follow the endpoint each mode calls — the handoff is a download and needs the same library read permission that lets you see the file, server-side slicing writes a new file and needs upload rights — and the action is shown disabled with the missing permission named rather than hidden. Translated in all locales; wiki updated. Covered by frontend tests.
  • Temperatures on the streaming overlay, and a builder for its URL (#1422, reporter SMAW) — The overlay at /overlay/{printer} draws live print data over a full-screen camera view for OBS, a wall display or any browser source. It could already be tuned — which fields, what size, what frame rate — but only through query parameters documented in the wiki, and temperatures were not among the fields on offer. Both are now addressed. Nozzle, bed and chamber readings join the list, shown with the target while the heater is still climbing and with the target dropped once it is reached, so a settled hotend reads "220°C" rather than "220 / 220°C" for the rest of the print. Both nozzles appear on a dual-nozzle printer. They are drawn whether or not a print is running, since a preheating machine is exactly when they are worth watching, and each reading appears only when the printer genuinely reports it — chamber temperature stays absent on P1 and A1 models, which publish a value with no sensor behind it. And Settings → API Keys → Streaming Overlay now builds the URL for you: pick the printer, tick the fields, set size and frame rate, paste in a token if login is enabled, and copy the result, with an optional preview alongside it. The preview stays off until you ask for it so that leaving the settings page open does not hold a viewer on the printer's single camera connection. Making that preview possible needed one narrow change to the security headers: the overlay path now sends frame-ancestors 'self' instead of 'none', so Bambuddy's own UI can embed it. Every other page still refuses to be framed at all, 'self' permits a framer only on this same origin, and embedding the overlay from another host — Home Assistant on a different port, say — is unchanged and still requires TRUSTED_FRAME_ORIGINS. Temperatures are not in the default field set, so an overlay URL already pasted into a scene looks exactly the same after upgrading. Translated in all locales, wiki updated, covered by backend and frontend tests.
  • The external spool can be hidden from the printer card (#1782, reporter Arn0uDz) — An external spool holder that never gets used still occupies a full card's width in the Filaments row, next to the AMS units that are actually being used. An eye icon at the right-hand end of that row's header now hides it, and clicking it again brings it back, so nothing is lost behind a settings page you would have to remember. The choice is remembered per printer and stored in the browser, like the card size and the offline-printer filter — one machine in a fleet can be tidied up without touching the others, and nothing changes for anyone else using the same Bambuddy. The icon is deliberately absent on a printer with no AMS: there the external spool is the entire filament section, and hiding it would leave an empty row. That guard also covers the case of an AMS being unplugged from a printer whose external spool was hidden earlier — the spool reappears rather than leaving a blank row behind. On the H2D and H2S both external positions share one card and so hide together. Translated in all locales, wiki updated, covered by frontend tests.
  • The slice dialog can edit the full print-parameter set, not just pick a preset — Slicing from Bambuddy meant taking a process preset exactly as it came. Anything beyond that — one more wall for a bracket, supports for a single overhang, slower outer walls on a part that keeps scarring — meant going back to Bambu Studio, editing there, and re-exporting. The slice dialog now has a Process settings section carrying the whole tree: the same pages, groups and ordering the desktop slicer shows under Print Settings, with the same labels, tooltips, ranges and defaults, because they are extracted from the slicer's own sources rather than hand-picked. The dialog itself widens to make room: on a reasonably sized screen it now uses two columns, with every "what am I slicing with" decision — pipeline, printer, process, filaments, bed type, layout passes — kept together on the left and the settings panel given a column of its own on the right, open and ready rather than folded away. Narrower screens keep the single column and the collapsed panel. The settings a source file's designer changed (#2622) now live in this panel too, marked from file against the options they belong to instead of in a separate list further up the dialog — so there is one place that shows what a slice will actually use. Machine-coupled ones stay flagged and unticked as before, anything the panel has no entry for is listed by name rather than quietly dropped, and typing your own value still wins. Switching on "Use the file's built-in settings" greys the panel out rather than removing it, so the dialog does not appear to lose a feature when that toggle is flipped — it stays visible, says why it is inactive, and applies nothing. Options that select which filament prints a feature — support base and interface, and the per-region pickers for walls, infill and surfaces — list the filaments you actually picked on the left rather than asking for a slot number, so "support interface" can be set to the PVA in slot 2 by name. Defaults and ranges are read out of the slicer's C++ initialisers, so a few arrived in source form — the whole Line width group showed "0." rather than "0" — and those are now cleaned as the data is generated instead of being papered over at display time. Every field starts from the values your picked process preset actually sets, fetched by flattening it through the slicer sidecar — the same resolver that does the slicing, so the numbers cannot disagree with what a slice produces. A field you never touch shows the preset's value, and reverting returns to it. Where those values cannot be read the panel falls back to the slicer's own defaults and says why — a sidecar older than the feature, one that did not answer, or none configured at all — rather than presenting the defaults as if they were your preset's. The first is much the most likely, because the sidecar image is pulled independently of your Bambuddy version, so that message names the fix outright: update the sidecar image. It behaves the way the desktop one does. Simple / Advanced / Expert matches the slicer's own visibility tiers, search reaches across every page at once, changed settings are marked and individually revertable, and settings the slicer itself disables in your current configuration are greyed out — infill options with infill at zero, ironing options with ironing off — because Bambuddy evaluates the slicer's own enable rules rather than approximating them. Where a rule cannot be decided with certainty the setting stays editable, on the grounds that a missing control looks like a bug while a redundant one is merely ignored. Edits apply to one slice, are not saved into a preset, and are written after the source file's support configuration and any carried designer settings, so an explicit choice is never silently overridden; an untouched panel produces exactly the request it did before. Parameter names and descriptions are in English even where the rest of Bambuddy is not — several hundred strings lifted verbatim from the slicer, which is a separate job from translating Bambuddy's own interface. The dialog's own wording is translated in all locales. Wiki updated, covered by backend and frontend tests.

Changed

  • A filament whose exact colour is not loaded now prints in the closest one available, not the first one within tolerance (#2804) — When a file asks for a colour no spool matches exactly, the matcher falls back to spools that are close enough. It picked whichever of those came first in AMS slot order, so the winner depended on which slot a spool happened to sit in: a required #3A7BD5 with a purple #6253AD in tray 1 and a near-identical #3B7AD2 in tray 3 took the purple, and moving the spools between slots changed the outcome with nothing else changed. Eligible spools are now ranked by how close they actually look, and the nearest wins. Closeness is measured with CIEDE2000, the CIE's perceptual colour-difference formula, rather than by subtracting the RGB numbers — those two disagree often enough to matter, because RGB arithmetic overstates blue badly: against a dark green requirement it rates a purple as the nearer of two eligible spools and a green as the further one. Which spool qualifies as close enough is unchanged — the same per-channel tolerance as before — so no spool becomes usable or unusable because of this, only better or worse ranked among those already eligible. This changes which spool some prints use. If Prefer Lowest Filament is on, its ordering previously decided the fallback outright, and it now acts as the tie-break between spools that are equally close, on the grounds that printing in the right colour matters more than burning down a part-used spool, and that two equally close spools is the case that preference was actually for. Alpha is still ignored on both sides, so a transparent filament keeps matching its own colour. The four places that pick a spool — the scheduler's matcher and the three in the interface — now share one ranking rule instead of carrying four copies of it, which is what allowed them to disagree. The scheduler also logs which rule won for every slot it maps, and the colour distance when the winner was a near-match, rather than only doing so when Prefer Lowest was switched on; "why did it pick that spool" is answerable from the log now. Covered by backend and frontend tests.
  • Preheat no longer gives up on a chamber-heated print whose file carries no bed temperature (#2727, contributed by ticfinack) — Preheat read its bed target out of the slicer metadata, and a file that carried none — common in OrcaSlicer's gcode.3mf exports — made it skip the whole stage and start the print against a cold chamber, which is the outcome preheat exists to prevent. Where the print needs chamber heat the bed is simply how that heat is produced, so those jobs now heat the bed to the Keep-warm bed temperature instead of bailing out. A bed temperature found in the file still wins, and a print with no chamber requirement still skips, so no bed temperature is invented for the print itself; preheat's target is transient either way, since the print's own G-code sets its bed at start. If you have preheat enabled and your files carry no bed temperature, dispatch will now take noticeably longer than it used to — those jobs previously skipped straight to the upload and will now wait for the chamber to converge and soak, up to twenty minutes at the default wait and soak settings. This applies only with preheat switched on, and the two settings that govern it are unchanged. In the same pass, preheat gained the soak-crediting described above: on a printer that reports its chamber temperature, a soak the chamber has demonstrably already served is shortened or skipped rather than repeated. Both apply wherever preheat runs, not only under the new keep-warm toggle.
  • The G-code preview is now the slicer's own renderer — Sliced files previewed through an embedded copy of a third-party viewer, shown in an iframe. It drew each move as a screen-space line, so a print came out stringy and shimmered wherever layers crossed; it coloured by filament slot only; and being a separate app inside a frame, it could be neither themed nor translated, and needed its own machinery to detect a proxy refusing the embed. It has been replaced by Bambuddy's own viewer built on libvgcode, the renderer OrcaSlicer draws its own preview with, so extrusions are solid volumes that occlude one another and a print reads the way it does on the desktop. Colour by filament — the default, showing the print in the colours you actually assigned — or by feature, where walls, infill, supports, bridges and the prime tower each take the slicer's own colour, or by layer height or line width on a graduated scale. Every entry in the legend is a switch: click a filament or a feature to take it out of the view, which genuinely removes it rather than hiding it behind what it was covering, so you can look inside a part without its supports in the way. The layer slider has both ends, so a band of layers can be isolated rather than only a top capped, and travel moves can be shown. Reading the file needed real care: BambuStudio annotates its G-code quite differently from OrcaSlicer, and it emits a tenth of its moves as arcs — reading only the one dialect showed a 52-layer print as 23,165 layers in a single colour, and ignoring the arcs punched holes through every curved wall and tree support. Both are handled, along with the helical travel lifts that look like arcs but lay down nothing. Covered by frontend tests.
  • The 3D and G-code previews are rendered properly rather than sketched — The model preview drew every surface with the same flat shading, so a print read as a coloured silhouette with no form, and it sat marooned in the middle of the frame with a screenful of empty space above it. The framing was the plainer bug: the camera distance came from a fixed multiple of the model's largest dimension, which takes no account of the camera's field of view or the shape of the panel it is drawn in, so a tall narrow preview was framed as though it were square. It is now solved against the model's bounding sphere and both fields of view, and fills the frame whatever the panel's proportions. The model itself is lit by a generated environment rather than two lamps and a wash of ambient light, which is what gives a curved surface a gradient across it instead of one flat tone, and it now casts a contact shadow so it looks like it is resting on the plate rather than pasted in front of it. The G-code preview drew each move as a two-pixel line, which is why a sliced model came out stringy and shimmered where layers crossed — a line has no thickness in the scene, so it cannot hide the layer behind it. Moves are now drawn as solid extrusions with real width and height, and the print occludes itself the way it does in a desktop slicer. The modal's second tab is gone: G-code already has its own full-page viewer, and a preview of a model is a different question from a preview of a print. Covered by frontend tests.
  • The slice dialog's process and filament lists now leave out presets that belong to another printer — They were already sorted by compatibility, but a preset for a different Bambu model still appeared, demoted to an "Other printers" group at the bottom of the dropdown. With a large cloud filament library that group is most of the list, so the filtering was doing little for the thing it was meant to help: finding the profile you actually want. Those presets are now held back, with the label reporting how many ("3 hidden") next to a Show all link that brings them back for that one dropdown. Two things are never hidden. A preset with no detectable printer — a custom or renamed profile — stays in the list, because absence of evidence is not evidence of incompatibility and hiding those would make people's own imported profiles vanish. And whatever is currently selected stays visible even when the list is collapsed, so a deliberate cross-printer pick, or one restored from a pipeline, is never silently discarded by being dropped from the options. Re-slicing for another printer remains fully supported, so this is a default view rather than a restriction. Fixing this also corrected a screen-reader bug in those dropdowns: the controls sat inside the label wrapping the select, which handed them the entire label as their spoken name. A separate defect surfaced alongside it — the filter did nothing at all when the selected printer was a preset you had edited, because BambuStudio names those copies with a leading "# " and the matcher did not know to look past it. On such a printer every preset read as "compatibility unknown", and profiles listing their compatible printers by name could be ruled out against the very printer they were cloned from.
  • The MQTT debug log now records the commands sent to a printer, not only what it reports backPrinter → Debug → MQTT captured one side of the conversation. Bambuddy listens on both of a printer's topics, but the one carrying commands returned before anything was written to the log, so a capture could show every status push the printer made and nothing it was ever told — including the commands Bambu Studio sends over the local network, which is the only place they can be observed at all. Those now appear alongside Bambuddy's own, grouped under the outgoing filter. It is what lets a question like "which value does Studio put in this field?" be answered from a user's capture instead of guessed at, and it is why #2774 could not be taken further. Commands Bambuddy sends appear twice, once as it publishes and once as the broker echoes it back, and the pair is itself evidence the command reached the broker. Logging is off until switched on, as before. Covered by backend tests.
  • The L and XL printer cards now scale their text and icons, not just their width (#1848, reporter misterff1) — Switching a card from M to XL made it wider, enlarged the printer name and the thumbnail, and left everything else exactly as it was: the AMS slot labels, temperatures, filament names, status text and every small button stayed pinned between 8 and 11 pixels, well under the smallest size used anywhere else in Bambuddy. The result was a full-width card carrying the same tiny text as the compact one, which is precisely the opposite of what someone reaching for a bigger card is asking for. Browser zoom is not an answer to this, since it enlarges the entire page and so preserves the very disparity being complained about. The card body now scales along with the card: L draws it 20% larger and XL 40% larger, icons included, so the controls grow with the text rather than staying fiddly to hit. The AMS-HT card needed two adjustments of its own, since its temperature and humidity readings sit beside the slot rather than under it. Its single slot was the only thing on that row able to grow, so it swallowed every spare pixel and pushed the readings hard against the card's edge — it is now capped at roughly two ordinary slots, which keeps them clear at any card width. The card itself also gained a ceiling of one full AMS card's width, so a unit that wraps onto a line of its own no longer stretches that single slot across the whole card. S and M are deliberately untouched — S is the dense fleet view where density is the point, and M is the default, so an existing install looks identical until you reach for a size that is already asking for more room. Wiki updated. Covered by frontend tests.
  • The "Slicer Bundles (removed)" card is gone from Settings — Bundle import was withdrawn in 0.2.5, and the panel it lived in was kept behind as a static notice explaining where the feature went and what to use instead. That notice has done its job: it has been visible for several releases, it was shown to everyone running the slicer sidecar whether or not they had ever imported a bundle, and it occupied a card in Settings → Workflow that could not be acted on. The card and its translations are removed. Nothing about slicing changes — single-preset import, Bambu Cloud and Orca Cloud sync all work as before, and the slice-time lookup order is still Imported, then Orca Cloud, then Bambu Cloud, then the sidecar's standard presets. In the same pass G-code Injection moved to the foot of the right-hand column, which evens out two columns that the removal had left lopsided; the card itself is unchanged, and the settings search still jumps straight to it. Wiki updated.
  • The H2C nozzle rack no longer crowds the temperatures beside it, and its slots are numbered — The rack card sat in the same row as the nozzle, bed and chamber readings, but it grew at twice their rate from a 190-pixel floor, so on anything wider than a compact card it claimed several hundred pixels to draw a row of six 28-pixel chips and left the rest empty. The width came out of its neighbours, which are the cards that actually need it: on a dual-nozzle machine the combined nozzle reading is "220° / 220°" and it was wrapping onto two lines beside a mostly blank rack. The card is now sized to what it draws and gives the remainder back; it can still give way and shrink on a narrow card rather than overflowing. Each slot also carries its physical rack position underneath it, 1 to 6, so a nozzle can be named rather than counted along — the numbering is positional, so an empty slot keeps its number and "the nozzle in slot 4" means the same thing however many of the six are occupied. Covered by frontend tests.
  • Error and warning toasts now stay up twice as long — Every pop-up notification disappeared after three seconds regardless of what it said. That is about right for "Settings saved", which confirms something you just did and is skimmed rather than read, but errors and warnings are a different kind of message: they carry a reason, often one relayed from the printer or the backend, and they run to a couple of lines. Three seconds was not long enough to finish reading one, and a missed error message is gone for good — there is no notification history to go back to. Errors and warnings now hold for six seconds. Success and informational toasts keep the three-second default, so the common case of clicking something and seeing it confirmed is unchanged, and the close button and the manual dismiss work exactly as before on all of them. The background print-dispatch toast is unaffected: it stays up while it has work in progress and clears itself shortly after the last job settles. Covered by frontend tests.

Fixed

  • A print stage Bambuddy has no name for now says so in the log — The printer reports its current activity as a number, and Bambuddy keeps a table of what those numbers mean. The table is hand-maintained and every new model adds to it, so a printer occasionally reports one that is not in it and the card reads "Unknown stage (72)" — which happened on an H2C, where the table runs to 66 and then jumps to 74. Stage changes were logged, but at debug level, which is off in normal running: the only record that it had happened at all was the card, and by the time anyone looked the printer had moved on. An unnamed stage is now recorded at the normal log level, once per stage number, along with the model, the stage it came from and what the printer was doing at the time — which is what identifying it afterwards actually needs. Stages that do have names stay at debug as before, so a normal print logs nothing new. Fixing this turned up a latent crash beside it: the stage-change log line builds its text before the log level is consulted, so a printer reporting a stage that was not a number at all — malformed telemetry rather than an unknown stage — would abort processing of that update entirely. Labelling a value can no longer do that. Covered by backend tests.
  • An H2C refused to start a multi-colour print, reporting that its hotends did not match the sliced file — The print uploaded, the printer took the command, and then stopped immediately with "the print has stopped because the available hotend quantity or model does not match the sliced file" (HMS 0500-4047). Bambuddy was telling the printer that one of the filaments the plate prints goes to no hotend at all, while the AMS mapping alongside it named the exact tray that filament comes from — a contradiction the firmware will not start a job on. The mistake was in reading the sliced file. Each filament in a 3MF names the group it belongs to, and on every other dual-nozzle Bambu that group number happens to also be the extruder it prints on, so Bambuddy read it as one. On a nozzle-rack machine it is not: the rack carriage can hold six hotends against the fixed carriage's one, so the slicer writes one group per nozzle it wants rather than one per carriage, and a three-colour plate can carry groups 0, 1 and 2 on a printer with two extruders. The filament in the group that ran past the end was quietly dropped, and a dropped filament is indistinguishable further down from a slot the plate genuinely does not print. Bambuddy now reads the group-to-extruder table the file states for itself, which is the only place the real answer is written. Where a filament still cannot be placed, no mapping is sent at all rather than a partial one, and the reason is logged: the printer then chooses its own nozzle, which is what it did before any of this existed and is much better than being handed an answer that contradicts itself. Two things were corrected alongside it. The mapping is now read from the plate being printed rather than from every plate in the file at once — a project with several plates can assign the same slot to different extruders on each, and the wrong plate's answer was as likely as the right one. And the mapping sent to the printer is now as long as the plate has filament slots, matching what Bambu Studio itself sends, instead of being padded to a fixed 32 entries. Covered by backend tests, and verified against the file that failed.
  • Interchangeable filaments were reported as the wrong material, and the pre-flight check disagreed with what would actually print — Bambu's firmware treats PA-CF, PA12-CF and PAHT-CF as the same material, and the scheduler has always matched them accordingly, so a PA12-CF spool would happily print a job asking for PA-CF. The interface compared the raw type names instead, so it labelled that same pairing a type mismatch — the badge contradicted what the printer was about to do, and the manual override picker made it worse by offering the spool the badge then rejected. Every place that judges whether a spool suits a requirement now reads one shared table. The pipeline pre-flight check had drifted the other way: it carried its own copy of that table, and the copy had come to disagree in both directions — it treated PLA Basic as interchangeable with PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. It now answers with the matcher's rules, which is the only answer worth giving: a check that predicts dispatch is wrong whenever it disagrees with dispatch, whichever way it leans. This makes the pre-flight check stricter in one case — a printer reporting a product name such as "PLA Basic" where the generic material is expected is now flagged rather than passed. That is the honest answer, and it is rare in practice, because the printer reports the material and the product name in separate fields. Nothing about which spool a print actually uses has changed. Covered by backend and frontend tests.
  • A process preset that turns supports on had them switched back off by the file being sliced (#2820, reported by zevulos) — The server slicer is handed the picked process preset as the authoritative settings for the slice, so anything the preset does not name comes from the file's own embedded settings. Since #1881 four support fields travel the other way as well — supports on/off, the support and interface filament slots, and tree versus normal — because Bambu's shipped process presets all set supports off (supports are a decision per print, not per quality level), and without carrying them a project exported with supports configured came out of the slicer as a single-material print with a PVA slot loaded and never used. That carry ran in both directions, which is wrong in the direction nobody asked for: a file that ships with supports off, which is very nearly every published model, stripped supports back out of a preset that deliberately turned them on. The reporter's own preset turns supports on with normal(auto) and snug, and the slice came back with supports disabled and set to tree(auto) — the only setting of the three that survived was the style, and only because it is not one of the four fields carried and they had re-entered it in the slice dialog. The file can now switch supports on but never off. Nothing is lost by that: since every shipped preset has supports off, a preset that has them on is a deliberate choice by whoever wrote it, and a file that wants supports still gets them along with its slot assignments. A file that never states whether it wants supports at all is treated the same as one that says no. The carry is also written to the log now, naming the fields it took, because the slice dialog shows the picked preset's values and a carried field quietly disagrees with what was on screen — with nothing in the log to say so, this bug reads as the preset being ignored, and the log line the report understandably keyed on was an unrelated one about the source file's own settings.
  • Printing several copies of a file uploaded straight to a printer left every copy after the first unable to run (#2819, reported by ooniiik) — Dropping a file onto a printer card uploads it to the library, prints it, and deletes it again: the file is a courier, not something you asked to keep. Ask for more than one copy, though, and each copy is its own queue entry pointing at that same temporary file — so the first one to dispatch deleted the file the others were still waiting for. What happened next depended on which database Bambuddy is running on, and neither answer was right. On SQLite the remaining copies were left pointing at a file that no longer existed: starting one failed with "Library file not found", and a copy set to start manually simply sat in the queue forever, giving no reason. On PostgreSQL, which enforces the same rule the schema has always described, the remaining copies were deleted outright — the jobs vanished from the queue with no error and no history, which is harder to notice and harder still to explain. On a farm running batches across twenty printers, both leave work that looks queued and is not. The copy that dispatches first now hands the rest the archive it just created, before the file is deleted: they print the same bytes from Print History, and the flag that consumes a library file is cleared on them so they cannot delete anything themselves. A copy already printing from its own archive keeps it, and a finished one keeps its outcome — but both stop referring to the deleted file, because on PostgreSQL that reference is the only thing tying them to the deletion, and the finished rows are what a batch order counts its progress from. Cross-model jobs (#671) that had this file as one of several alternatives keep their other alternatives and simply lose this one; one left with no alternative at all is handed the archive like any other copy. The same fix covers batch orders, which inherit the file from the item they were cloned from, and copies held back by a printer's previous-print gate, which come back the moment that gate is cleared. Reprints from Print History and any file kept in the library are untouched — nothing is deleted there in the first place. Covered by backend tests, and verified row for row against PostgreSQL as well as SQLite.
  • Deleting a file from the File Manager broke, or silently removed, the jobs queued against it (#2819, reported by ooniiik) — The same fault as above by a different route, and the one that accounted for most of the reporter's "Library file not found" history. Nothing connected a library file to the queue: deleting a file left its queued jobs pointing at something that was no longer there, and they failed at the printer days later with a message that named neither the file nor the deletion. On PostgreSQL it was worse and quieter — the jobs were removed along with the file, including finished ones, which is what a batch order counts its progress from, and including a job that was printing at that moment. Deleting a whole folder did the same to every file underneath it, and did not even clean up the cross-model alternatives that pointed at them. Now, whenever a file is removed for good — a file outside Bambuddy's own storage, a folder deleted with its contents, emptying the trash, or the retention sweeper finally clearing it — the jobs waiting on it are cancelled at that moment and say which file went, and every other row that referred to it is detached rather than destroyed, so print history and batch counts survive. A job that is already printing is left running: what is being deleted is the copy in the library, not the copy the printer is working from. Moving a file to the trash still changes nothing about the queue, because the trash is reversible and restoring a file should not require re-queueing everything that was waiting on it — but a job that does run while its file is in the trash now says so, and says that restoring the file and queueing again will work, instead of "Library file not found".
  • The Spool Inventory page scrolled sideways on a phone, taking the whole page with it (#2813, reported by chitrangdesign) — The header of Filament → Spool Inventory put its title and its five buttons — Import CSV, Export CSV, Locations, Print labels and Add Spool — in a single row that could neither wrap nor shrink. Side by side those buttons come to roughly 600 pixels, so on a 390-pixel screen the header ran past the edge of the viewport, and because the whole page scrolls as one region everything below it went with the header: the statistics bar, the filters and every spool card sat offset, and vertical scrolling fought a horizontal pan the entire time. Longer languages made it worse, since German renders the same two buttons as "CSV importieren" and "Etiketten drucken…". The header now stacks on narrow screens and the buttons wrap onto as many lines as they need, which is the same arrangement the Statistics, Settings and Archives headers have always used, and the one the filter bar further down this very page already used — this header simply never got it. At 640 pixels and wider nothing changes at all. The System Information header had the same construction with a single Refresh button, where it only just overflowed, and is fixed in the same way.
  • A spool emptied by AMS Filament Backup was left showing full, and the spool that took over was charged for the whole print — With backup on, the firmware switches to another slot holding the same material when one runs dry and carries on. Bambuddy has split a print's filament across the trays it really fed from since #957, but everything that split depends on lived only in memory: the plate the job was dispatched for, the slot-to-tray mapping it was dispatched with, the spool assignments as they stood at the start, and the log of tray changes. A print long enough to outlive a Bambuddy restart lost all of it — on the 14-hour print this was reported on, two restarts left the completion path with nothing but what the printer reports now, and with backup on "now" is the substitute tray. The full 1122 g went to the neighbouring spool, which then read 0 g of 1000 g, while the spool that had actually run dry was charged nothing and still read full. That context is now written to the database at print start and put back if Bambuddy restarts, with tray changes appended as they happen, so the split survives; a print that started before this release is seeded from the tray feeding at the moment of recovery, so at least the remainder is attributed correctly. Four things around it were wrong in the same direction. The printer's live mapping field was consulted ahead of the mapping the queue actually dispatched, and that field is precisely what backup rewrites — the stored one now wins, and the live field is the fallback it always should have been. The plate was read only from the in-memory session, though both the archive and the queue item recorded it; without it the 3MF parser summed every plate in a multi-plate file, so a second plate's filament was billed to a print that never ran it. The per-layer G-code read that measures each segment took no plate at all and used the first .gcode member in the zip, which for a Bambu Studio export is not plate 1 — the same member-order trap fixed in the G-code viewer above, here measuring one plate's layers against another plate's total. And when a slot reported empty mid-print, the assignment was auto-unlinked as though the spool had been removed, which is exactly wrong at a runout: the spool is still in the AMS, just consumed, and dropping the link left the completion path with nothing to charge that segment to. Empty slots are now left linked while a print is running; a genuinely different filament still reconciles as before. All of this applies whichever inventory Bambuddy is running against. Spoolman's own durable row has carried its plate-scoped figures and its dispatched mapping since #1820, but not the tray-change log, so the print-start capture now runs for both backends; its per-layer figures came from the same wrong-plate call; and its slot assignments — which are how a tag-less spool assigned through the Bambuddy UI is resolved at completion (#1459) — were being dropped at a runout exactly like the internal ones. Covered by backend tests, including the runout sequence from the reported print.
  • A print completion could close the wrong queue item — When a print ends, Bambuddy finds the queue row to close by asking for whatever is printing on that printer. Nothing tied the event to the row: the completion carries the printer's subtask name but no run identifier, so any completion delivered for a printer closed whichever job happened to be printing on it, whether or not the event was about that job. A run closed this way is marked completed with the printer still working, disappears from the queue into history, and takes the rest of its batch with it — the next plate stays pending because the queue rightly will not dispatch onto a busy printer. The handler now checks the completion against the file the row was dispatched with before closing it, and leaves the row alone, with a warning naming both, when they disagree. It only refuses on a positive disagreement: a row with no archive yet, or an event with no subtask name, is unverifiable rather than wrong, and refusing those would strand the item in printing and wedge the printer's queue — a worse failure than the one being prevented. Also fixed in the test suite, where this surfaced: it built its own database but the app's module-level engine still resolved DATABASE_URL, so tests that reach code opening its own session — the completion path among them — talked to whatever database the developer's .env named. That is now redirected to a throwaway file before any app import, and the run aborts rather than starts if the redirect did not take.
  • Number steppers, date pickers and dropdowns stayed in their light-mode colours on a dark theme — The little up/down arrows on a number field -- the print dialog's per-plate quantity, and every other one in Bambuddy -- were drawn as a pale grey chip inside a dark input, and the same applied to the calendar button on a date field, the popup a <select> opens, scrollbars and the yellow tint a browser paints over an autofilled field. Those parts of a form control are drawn by the browser rather than by us, and the browser was never told the page had gone dark: Bambuddy switches theme by swapping CSS variables under a class, which is invisible to it, so it assumed light and painted them to match a page it thought was white. It is now told which appearance is in use, and follows the theme in both directions. Two date fields that had been pinned to dark by hand to work around this -- in the statistics timeframe picker and the local-backup schedule -- no longer need to be, and had been wrong under the light theme. A <select> opened on a dark theme is the change worth knowing about: its list used to render as white-on-white in places, since the popup took the light appearance while the options inherited the theme's white text.
  • The G-code viewer opened a multi-plate file on the wrong plate, with no way to reach the others — Previewing a sliced multi-plate 3MF from the File Manager showed a plate nobody had asked for, and nothing on screen said which one it was or offered another. Three things were behind it. The route that serves a library file's G-code took no plate parameter at all, so the plate the viewer had always been putting in the URL was discarded — an unknown query parameter is dropped without a word. With nothing to go on, both this route and the archive one fell back to the first .gcode member in the file, and a 3MF's member order is whatever the slicer wrote rather than plate order: the two-plate file this was reported on stores plate 2 ahead of plate 1, which is exactly the plate that kept appearing. And no path into the viewer from the File Manager passes a plate in the first place, so even once the parameter worked there was no way to ask for plate 1. All three are fixed: the library route accepts plate exactly as the archive route does, an unqualified request returns the lowest-numbered plate rather than whichever came first in the zip, and the viewer has a plate switcher in its header for any file with more than one. The plate lives in the viewer's URL, so a link to one particular plate keeps working and a reload stays put. Filament colours follow the switcher too, which they previously did not — plate 2's toolpath could be drawn in plate 1's colours. The same first-member assumption sat under G-code injection and the finish-photo bed-drop height, both of which now resolve the lowest plate when no plate is named. Covered by backend and frontend tests, pinned to the member order of a real Bambu Studio export.
  • A print was refused for lack of filament that AMS Filament Backup had covered all along — The print dialog weighs each plate against the spool assigned to the slot it maps to, adding up the plates that draw on the same slot before it weighs any of them. What it did not know about was AMS Filament Backup: with that on, the firmware switches to another slot holding the same material once one runs out, so what a print can draw on is the whole matching pool and not the one slot. Queueing both plates of a job needing 1441 g of ABS was refused with "A3: needs 1441g, remaining 1000g" while the identical full spool in A4 sat next to it, doing nothing. The queue dispatcher has pooled matching spools since #1762 and would have started this print without complaint — the dialog was the only thing in the way, and "Print anyway" was always the correct answer to it. The dialog now pools the same way the dispatcher does: same filament profile and colour, and only within one nozzle's reach on the dual-extruder machines, since the firmware cannot cross nozzles even with backup on. Where a pool really is too small the warning quotes the pooled figures instead of the mapped slot's, because "needs 1441g, remaining 1000g" written next to a second full spool reads as a contradiction. Which spools back each other up is now worked out in one place on the server, by the code the dispatcher itself uses, and handed to the browser rather than decided a second time there — the two answers drifting apart is the whole of this bug. Doing it that way also gives the warning to Spoolman users for the first time: it used to resolve spools through the internal inventory only, so in Spoolman mode it had quietly approved everything, including prints that were genuinely short. Covered by backend and frontend tests.
  • Slicing a model stored on a network share wrote the result into Bambuddy's internal library instead of onto the share (#2810, reported by zevulos) — Server-side slicing files kept in an external folder produced a .gcode.3mf that appeared in the correct folder in the File Manager but never arrived in the folder itself, so it could not be opened from any other machine on the share. The output was always written to Bambuddy's own storage while the new entry was filed under the external folder, which is why the file looked present in the web interface and was missing everywhere else — and why it could not be reproduced without looking at the mount from outside Bambuddy. Uploads and moves already wrote to external folders correctly; slicing was the last path that did not, which is why moving a sliced file to another folder on the same share made it appear there. The result is now written next to its source, under a name that is readable on the share rather than an internal identifier. An existing file of the same name is never overwritten — a re-slice becomes Model (2).gcode.3mf — because the target is somebody's NAS and the file being replaced may not be Bambuddy's. If the external folder cannot accept the file at all, because it is mounted read-only, unreachable or not writable, the slice is kept rather than thrown away: it goes to the internal library as before, and now says so with a warning, since a file quietly filed somewhere you are not looking is the whole of this bug.
  • A virtual printer's address spilled outside its card on the Virtual Printer page (#2808, reported by underscan) — The collapsed header of each virtual-printer card lays its details out in one row: name, mode, model, the printer it proxies to, and the IP addresses it is bound to. Every item in that row was marked as not shrinkable, so the row was always as wide as its contents, and the card does not clip what does not fit — the last IP address and the enable toggle were drawn outside the card's border, on the page background. The name was supposed to shorten with an ellipsis to prevent this, but could not: an item in a row like that will not shrink below its own text unless it is explicitly allowed to, so the ellipsis never appeared. It takes three things at once to run out of room, which is why it took a report to surface: both address fields are filled in only when Bambuddy and the printer are on different subnets (the reporter runs two VLANs), a printer adopted without a name is called "Printer at ", and the cards sit three to a row on a wide screen. The details now wrap onto a second line inside the card when they do not fit. Nothing is hidden or shortened away — these are the addresses an operator opens the page to check — and the card clips at its own border as a backstop.
  • Restoring a SQLite backup into PostgreSQL failed part-way with a foreign-key violation on the file library — The restore rebuilds the schema and then imports every table, and it is meant to create those tables carrying no foreign keys at all, so that the order rows arrive in cannot matter; the constraints go back on once everything has landed. It did that by taking the keys off the application's own schema description before creating the tables, which only suppresses the REFERENCES clause written inside each CREATE TABLE. SQLAlchemy keeps a second view of a table's keys, derived from its columns, and that one was untouched -- so whenever it met a group of tables whose dependencies form a loop and could not put them in order, it fell back to adding their keys afterwards with ALTER TABLE, reading them from exactly the view that still had them. library_files, library_folders and print_archives all point at one another, so twelve constraints came back across those three tables. The same loop also costs them their place in the import order, so they were imported alphabetically instead -- which puts library_files ahead of the library_folders rows its folder_id refers to, and PostgreSQL refused the very first batch. The restore now drops the foreign keys in the database itself, after the tables exist, instead of trying to stop them being written: it no longer matters how a constraint came to be there, and a future loop between other tables cannot bring the problem back. One more fault went with it -- the keys were stripped from a schema description shared by the whole running process and only restored after the rebuild had finished, so a failure in between left the process without them until it was restarted. Verified end to end against a real PostgreSQL: a backup whose child rows import before their parents now restores cleanly, with all ninety constraints in place afterwards. Separately, the warning about keys that genuinely cannot be put back -- because the backup itself holds rows pointing at something that no longer exists -- now names them by the columns they link, such as print_archives(library_file_id) -> library_files.id. These constraints have no name of their own, so the warning used to print print_archives.None once for each of that table's five keys, which said nothing about where to look; the offending value PostgreSQL reports is logged alongside it. The restore itself is unaffected, and so is the data -- those columns are simply left unenforced. Covered by backend tests.
  • The slice dialog showed a guessed filament list for projects saved by a newer Bambu Studio than the slicer sidecar — Opening the slice dialog on an unsliced project runs a quick preview slice, purely to ask the slicer which AMS slots the chosen plate actually consumes. Bambu Studio 2.8 writes a machine G-code template containing {if timelapse_inline_photo} but does not export a definition for that variable, so the template is unresolvable the moment it leaves Studio: a sidecar running an older build stops with a placeholder parse error before producing any slice data. The preview then returned nothing and the dialog quietly fell back to guessing from the file's painted faces, with no indication the numbers were an estimate. On the H2D project this was found with, the guess dropped a whole slot -- the support material -- from a four-filament plate. Bambuddy now retries the preview once with just that one unparsable template emptied, leaving every other setting in the file untouched, which is what keeps the answer honest: the process settings, support configuration and per-slot filament assignments are all still the project's own, so the filament list and its gram figures match what the file would really print. Only templates that cannot extrude are ever emptied -- a start or filament-change template lays a prime line or purges, so silencing one would change the very grams the preview reports, and Bambuddy would rather return nothing than a confident wrong number. Verified against a real H2D slice: the retry reproduces the full four-slot list, gram for gram. Covered by backend tests.
  • Cancelling or deleting a queued item did not stop the preheat already running for it (#2727, contributed by ticfinack) — The cancel and delete routes write the item's new status to the database, and nothing else. A dispatch that had already begun preheating was parked in a sleep waiting for the chamber to reach temperature, where it could not see that write — so the heaters kept running out the rest of the wait and soak for a print that was not going to happen, twenty minutes at the default settings and longer if those have been raised, and the printer stayed marked busy the whole time, holding up every other job queued behind it. Those routes now tell the scheduler directly and the waits are taken in slices, so a cancelled preheat is abandoned within seconds and the heaters are switched off on the way out. The same unwinding now covers every other way a dispatch can end without starting a print — a failed upload, an error mid-dispatch, a plate whose file has gone missing — each of which used to leave the bed and chamber heating with nothing to turn them off. What preheat set is recorded and reversed, and a bed the printer reports at some other target is left alone rather than switched off, on the grounds that it belongs to whoever set it. A cancellation that lands after the upload has begun still starts the print, as it always has. Covered by backend tests.
  • STEP files were offered for server-side slicing, which cannot work — The Slice action appeared on .step / .stp files and the backend accepted the job, but neither slicer can load one from its command line: OrcaSlicer 2.4.2 and Bambu Studio 02.07.01.62 both answer Unknown file format. Input file must have .stl, .obj, .amf(.xml) extension. So the file was read, converted and uploaded, and the failure came back as "The input model file to the slicer can not be parsed" — which reads as a corrupt model rather than an unsupported format. The Slice button and the pipeline action no longer appear on STEP files, and the endpoint refuses one up front with a message that says to export it as STL or 3MF first. Open in Slicer is unchanged and still hands STEP to the desktop application, which opens it perfectly well — that was always the working path for these files.
  • A large model was refused with "Slicer CLI failed (500): File too large" and no way to find out what was too large (#2802, reported by zevulos) — Server-side slicing of a big multi-colour project failed on every attempt, and the message pointed at nothing. The slicer sidecar caps the size of the model it will accept; that cap was fixed at 100 MB, which real MakerWorld projects exceed. Worse than the limit was how it arrived: the sidecar's upload layer reports a size rejection as a kind of error its own handler does not recognise, so it fell through to a generic HTTP 500 carrying the bare words "File too large". A 500 reads as a crash inside the slicer, and Bambuddy's one good explanation about request size was written for the HTTP 413 that a reverse proxy sends, so it never appeared. The reporter did the only reasonable thing with what they were shown: set MAX_FILE_SIZE, BODY_PARSER_LIMIT and EXPRESS_PAYLOAD_LIMIT, restart everything, stop nginx in case it was interfering, and move the whole installation from Windows to Docker — none of which the sidecar reads, on a proxy that was never in the path. The cap is now 512 MB by default and settable with MAX_MODEL_UPLOAD_MB on the slicer-api service, and the sidecar answers an oversized upload with a 413 that names the limit and where it lives. Bambuddy recognises the rejection by what it says rather than by its status code, so an installation still running an older sidecar image gets the same explanation — including that the fix there is to update the image, since those have no setting to change. Two things followed from the same misreading: the failure was classed as a slicer crash, so every attempt retried the identical oversized upload "with embedded settings", spending a second 25-second conversion on a guaranteed-identical answer; and nothing anywhere recorded the size of what was being sent, so the support package from a slice that died on an upload cap looked exactly like one that died on a bad profile. Both are fixed — the retry is skipped, and each slice logs the model's size. Raising the cap also changed how the sidecar handles the upload: the model is streamed to disk instead of being held whole in memory, so a 512 MB project no longer costs half a gigabyte of RAM per concurrent slice on the small machines most likely to be running it. This needs a sidecar update to take effect, and the command has to name the sidecar: cd slicer-api/ && docker compose pull orca-slicer-api && docker compose up -d orca-slicer-api, substituting bambu-studio-api if that is the one you slice with. A bare docker compose pull looks like it works and does not: the Bambu Studio sidecar is declared behind a Compose profile, and Compose skips profile-gated services without saying so, leaving the old container running under restart: unless-stopped. The reporter hit exactly that on the first attempt at this fix — pulled, restarted, set MAX_MODEL_UPLOAD_MB=5000, and got the same 100 MB rejection, because the image never changed. Bambuddy's message, this changelog, the wiki and the sidecar README all gave the bare command; all four now name the service.
  • /auth/me described API keys as administrators they were never allowed to be (#1894, reported by MorganMLGman) — Asked to identify an API key, Bambuddy answered with a synthetic administrator: user id 0, role admin, is_admin: true, and every permission in the system. None of that was true. An API key cannot reach an administrative route at all, whatever its scopes and whoever owns it, so a client that built its interface from this answer — which is exactly what a native app does — offered buttons that failed with a permission error the moment anyone pressed one, and still had no way to learn which user id its own prints were filed under. The endpoint now reports the key's owner as its identity, is_admin: false, and a permission list containing precisely what the key's scopes admit, so what a client is told matches what it will be allowed to do. Keys created before keys had owners have no identity to report and keep the old id: 0 placeholder, but they no longer claim to be administrators either. Clients that branched on is_admin or role should branch on permissions instead.
  • An unacknowledged plate no longer stops AMS drying once a minute, for ever (#2801, reported by superflyer11) — With "require plate clear" on, a finished print left unacknowledged and something pending in that printer's queue put the scheduler into a loop: it stopped drying, auto-drying re-armed on the next tick, and it stopped it again — around 2000 state changes over ten days on the reporter's P2S, with no cycle ever running long enough to remove any moisture. Cycles the user had started by hand on other AMS units of the same printer were torn down with it. Two ideas had become tangled. Plate-clear answers "is the bed ready for the next job", which says nothing about whether the AMS may heat — and the gap between a finished print and the acknowledgment is exactly when drying is most useful, since the printer is free and nobody is waiting on it. Leaving the plate unacknowledged is also how people hold the queue by hand, so the hold was costing them the drying it should have enabled. On top of that, the "print takes priority" stop was reached only on the passes where the print was not going to start: drying is not one of the things the idle check looks at, so stopping a cycle could never make a blocked printer dispatchable, and the cycle was spent for nothing. Auto-drying no longer consults plate-clear at all; the stop now happens on dispatches that are actually going to proceed, and only where the model cannot dry through a print — hardware that can, and has been allowed to, keeps drying as #2758 established it should. A stop is also confined to cycles Bambuddy itself started, matching a contract the code already documented but did not honour, so a manual dry on another unit is left alone. Two smaller faults went with it: a printer merely waiting on the plate was being classed as mid-print, which silently applied the mid-print spool-protection cap to a printer that was not printing and logged the cycle as (mid-print) in FINISH; and a humidity reading that dipped to the threshold as the AMS cooled discarded the unit's whole history, including the 30-minute re-arm cooldown added in #2770 — so a reading oscillating a point either side of the threshold reset the very guard meant to ride it out. One behaviour change to be aware of: "Block queue while drying" previously had no effect on dispatch at all, and now does what it says — with it on, a queued print waits for a running cycle to finish. It is off by default.
  • An H2C could clean and level with one hotend and then print with another, several millimetres above the plate (#2800, reported by tru3l3gend) — The reporter's H2C ran its startup clean and bed levelling on the wrong nozzle, switched hotends, and then printed in mid-air; the same job sent from Bambu Studio was fine. The H2C is the only printer that mounts its nozzle from a rack of six, and a print command names that nozzle by its physical rack position — the firmware reports those as IDs 16 to 21 — rather than by the extruder index, 0 or 1, that every other dual-nozzle printer uses. Bambuddy only ever had a rack position when a job arrived through the Virtual Printer, which captures Bambu Studio's own pick and replays it untouched (#1780). Anything queued from the library, from an archive, through the webhook or from a slicer pipeline carried none, so the field was left off the command entirely and the firmware chose a nozzle for itself — and its choice does not have to agree with the one the file was sliced for. Bambuddy now reads the per-slot extruder assignment out of the file it is about to dispatch and resolves it against the rack position the printer is reporting at that moment, which is the only place it can be known: the mounted hotend can be swapped from the touchscreen between queueing a job and printing it. Nothing about this is guessed. When the rack position cannot be established — mid-swap, or a connection that has not yet reported one — the field is left off and the firmware picks exactly as it did before, because a wrong physical ID is what puts a print in the air and is far worse than no ID at all. A plate sliced for the fixed hotend alone now goes out with no such field at all, which is exactly what Bambu Studio does with one — until this correction those plates were being handed a rack position, since the extruder index they carry was the very one being read as "the rack". Which of the two carriages the rack feeds, and what physical ID the fixed hotend answers to, were both settled afterwards on the reporter's own machine, and both were wrong on the first pass: the rack was resolved onto the other extruder, and the fixed hotend was sent its extruder index in place of a physical ID. A plate using both nozzles printed the rack side in mid-air as a result, and the printer would not start a job at all once only the first of the two was corrected. Both values now agree with three native Bambu Studio captures and with a print that ran correctly on both nozzles from start to finish. Confined to the H2C throughout — the dispatch for every other printer, including the H2D and X2D, is unchanged. Diagnosed on real hardware by the reporter, who compared Bambuddy's dispatch against a working Bambu Studio one, established the rack ID range, supplied a patch, and then ran the A/B on both nozzles that pinned down the last two values.
  • Automatic drying no longer loops when the humidity threshold is set below what a warm AMS reports (#2770, reported by tchavei) — A reporter's H2D armed five separate 12-hour drying cycles inside four hours, one of them six seconds after the previous ended, and none of them ran for more than a couple of hours. Two things combine to produce that. The firmware ends a cycle whenever it decides the filament is dry, without reporting a fault: across this printer's history the run length tracks how wet the spools were, from nearly the full 12 hours when the AMS started at 32% down to minutes once it sat at 10-13%. That is the AMS doing its job. The loop is Bambuddy's. An AMS reports higher relative humidity while it is warm than once it has cooled — the same unit read 10-13% cold and 15-20% throughout every cycle — so with a threshold of 14% the reading at the moment a cycle ended was always still above it, and the next 30-second pass started another 12-hour cycle. Nothing counted, nothing waited, and it only stopped when the box finally cooled enough to read 13%. Auto-drying now waits half an hour after a cycle ends before it will arm another on the same unit, because the humidity reading means nothing until the AMS has cooled; and after two cycles in a row that bring the reading no lower it stops arming that unit altogether, says so in the log, and sends a notification — a new Auto-drying suspended event, on by default, since it reports that Bambuddy has stopped doing something and silence there reads as "still drying". Progress is judged against the lowest reading any cycle on that unit has ended at, so a genuinely wet spool in a humid room that is coming down slowly -- 40%, 37%, 35% -- keeps drying however far it still is from the threshold, and the suspension lifts by itself the moment the reading falls below it. Neither guard can ever stop a cycle that is running, and a cycle Bambuddy itself cut short for a print, or that you stopped by hand, is not counted against the unit -- so a farm that dries between queue jobs is unaffected. The threshold field in Settings → Filament → AMS Display Thresholds now warns when it is set below 20%, and every drying cycle end — early or normal — logs the unit's temperature and humidity, which is what made this diagnosable at all.
  • "database is locked" errors when a notification provider is unreachable (#2770) — A reporter's log showed two unrelated background tasks -- printer sensor history, and the queue's orphaned-dispatch sweep -- failing with sqlite3.OperationalError: database is locked, each one landing inside a Discord connect timeout that took exactly 30.000 seconds. It was not contention from writing too much. Bambuddy raises an alarm from inside the loop that records sensor history, at a point where the new history rows have been added to the session but not yet committed; the first database read inside the notification path then flushed those rows to satisfy itself, which opens a write transaction, and the provider was contacted over the network with that transaction still open. SQLite allows exactly one writer, and 30 seconds of waiting for a host that is not answering comfortably outlives the 15-second busy timeout, so every other task that wanted to write during that window failed. The two reads that run before a provider is contacted no longer flush the caller's pending work, so nothing holds the writer while the network is in play, and the connect timeout is now 5 seconds rather than 30 -- reaching a host either works quickly or is not going to. Sending the body keeps the full 30 seconds, so snapshot images on a slow uplink are unaffected. Only SQLite installs were affected; Postgres has no single-writer limit.
  • A 3D preview that a proxy refuses to embed now says so, instead of leaving you with the browser's error page (#2787, reported by trickfilm) — A reporter uploaded an STL, sliced it in Bambuddy, and found that the sliced file's 3D Preview showed a frowny icon and "hostname refused to connect" — while the STL's own preview worked. The split is exactly where the two previews part company: an STL or a source 3MF is drawn in the page itself, but a sliced file opens the embedded G-code viewer, which lives in an iframe. Bambuddy's own headers allow that frame — it is same-origin, and both the policy and the legacy header say so — which means a refusal comes from something between the browser and Bambuddy, typically a reverse proxy or security add-on sending its own framing header. None of that was visible: the browser drew its error page inside Bambuddy's layout, and nothing said what had been refused, by whom, or that the viewer opens perfectly well in a tab of its own. The page now asks for the viewer directly, reads the framing headers off the reply, and when they refuse the frame it replaces it with an explanation naming the exact header — so an operator can go and find the rule in their proxy configuration — plus a link that opens the viewer in its own tab, which no framing header applies to. A viewer that is missing from the installation is reported the same way rather than as raw JSON inside the frame. When the check cannot reach a verdict the frame is left exactly as it was, because a guess at a cause we cannot see would be worse than the browser's own page.
  • "We need you to confirm you are not a robot" on Bambu Cloud sign-in is now explained instead of just repeated (#2790) — A reporter tried to connect to Bambu Cloud and got that sentence as an error toast, with no CAPTCHA anywhere to answer and nothing to click. It is Bambu's sentence, not Bambuddy's: their anti-abuse layer had flagged the network and was answering the sign-in with HTTP 418 and a challenge body. Bambuddy had no idea what that was — the reply is well-formed JSON, so the existing Cloudflare-interstitial detector never fired on it, and the generic error path simply lifted Bambu's text out and showed it. The user was left to conclude their password was wrong, or that Bambuddy was broken; four sign-in attempts inside eighteen seconds appear in their log, each one more evidence for the thing that had flagged them. Bambuddy now recognises the challenge by its shape rather than by its wording, and the login form says what is actually happening: your email and password are not the problem, the block is tied to your public IP address rather than to your account, it normally clears by itself within a few hours, and retrying repeatedly extends it. The panel stays on screen — a toast is the wrong shape for a problem you cannot act on — and carries a one-click route to Use access token instead, which is the one way to connect while it lasts, since the token path does not go through the challenged endpoint. Sign-in requests are held back for five minutes after a challenge so Bambuddy stops making it worse, tracked per region and per host so a challenge on the API host cannot strand somebody halfway through a two-factor sign-in on the web one. MakerWorld imports, which meet the same challenge from the same edge, now share the detection instead of requiring the literal word "robot" in the error text. The System Health scanner has a matching signature, so the next support bundle from an affected install names the problem instead of coming back empty. The scanner's advice for a failed FTPS handshake was corrected at the same time: it still blamed firewalls and firmware, which last release's investigation (#2780) ruled out — it is the printer's own file service wedging, and the fix is to restart the printer.
  • Buttons show a pointer cursor again, and the AMS slot menu stops reshuffling itself (#2791, reported by AnthonyGrondin) — Hovering most of Bambuddy gave you an arrow, not the little hand that says "this does something". Not everywhere, though, which is what made it read as sloppiness rather than a bug: the update pill was inert while the buttons beside it were fine, a bed or nozzle tile responded but the history-graph button tucked into its corner did not, and dropdowns went either way with no pattern behind it. The pattern was there. Tailwind v3 gave every button a pointer cursor as part of its baseline styling; Tailwind v4, which Bambuddy has used since the interface was built, deliberately dropped that rule to match what browsers do on their own — and browsers give a button the ordinary arrow. From then on a button only looked clickable if whoever wrote it had said so by hand. Fifteen of about nine hundred and thirty had. None of the hundred and forty-nine dropdowns had, and of the checkboxes and radio buttons, nineteen out of a hundred and thirty. The rule is now restored once, centrally, rather than pinned onto individual buttons for the rest of the project's life: buttons, dropdowns, checkboxes, radio buttons, disclosure arrows and anything explicitly marked up as a button all point again. It sits at the bottom of the styling order, so the places that deliberately show a "not allowed" cursor on a disabled control still win, and a control that is genuinely disabled is left alone. Modal backgrounds are deliberately untouched: clicking one closes the dialog, but a full-screen sheet that claims to be a button is worse than one that says nothing. Separately, and behind the same report: the menu on an AMS slot listed Configure above Assign Spool on an empty slot and the other way round on a filled one, because the two are drawn by different code that had quietly drifted apart — both now lead with the spool action, and a test pins each side so they cannot drift again. The buttons in that menu centred their own text, which left their icons in a ragged column; they are aligned to the left edge now. Their hover shading was a ten-percent step that was very hard to see, and is now twice that. And the star on Add to favourites turns yellow as you hover it, so it previews what clicking will do.
  • A job queued to "Any {model}" now switches a printer on, like a job queued to one printer always has (#2786, reported by TheUltimateC0der) — Queue a print against a printer class -- Any X1C, or a Slicer Pipeline whose target type is Printer class -- with every printer of that class switched off at the wall, and nothing happened. The job sat pending, no smart plug was touched, and the only way out was to edit the item onto a specific printer, at which point Bambuddy powered that printer on immediately. The reporter's log holds that comparison exactly: thirteen minutes of the job being polled and passed over, then the edit, then a power-on on the very next check -- same job, same plug, same Auto Power On setting. Powering a printer on had only ever been written into the branch that handles a job pinned to one printer; the branch that picks a printer by model listed an offline one as a reason to keep waiting and never looked at its plugs. It does now. It also picks with a little more care than the older branch: a printer waiting for a plate-clear acknowledgment is passed over, because switching it on only leaves it idling behind that gate -- which is what the reporter's own log shows happening for the eighty minutes after their manual edit -- and a printer whose class the file cannot legally run on is never switched on at all. One printer comes up per queue check rather than a whole shelf at once, so several queued jobs wake several printers over the following minutes. Finally, a printer that is off and has no enabled Auto Power On plug now says that in the job's waiting reason instead of hiding behind the same "Offline" as the printers Bambuddy can bring back itself -- that distinction was the first question the reporter had to be asked.
  • A printer whose file service stops answering is named as such instead of quietly emptying your archives (#2780, reported by Utility9298 and AntonPalmqvist) — Two printers went on printing normally while every archive they produced arrived holding nothing but a filename: no filament totals, no layer count, no cover image, no timelapse. The Connection Diagnostic reported the file-transfer port as reachable, because it was — the printer accepted the connection and then answered it with something that was not TLS at all, and the sliced file could never be read back. Bambuddy said nothing about that on screen; it retried. Because each candidate location opened its own connection, one reporter's log carried 1813 identical handshake failures and another's 3511, against a printer that could not have answered any of them. This is not a model or a firmware problem — the same printers worked for days before and after the fault, and other installs run the same models untouched. It is the printer's own file service getting stuck, and a power-cycle clears it. Bambuddy now stops after the first failed handshake and leaves that printer alone for five minutes, so the log carries a handful of entries that say what went wrong and what to do about it rather than thousands that say neither. The Connection Diagnostic now completes a real handshake instead of merely opening the port, so a printer in this state reads as a warning that names a restart — not as a green tick. Scanning for a timelapse on such a printer reports the file service, where it used to return one error message that covered both "the printer is unreachable" and "this printer has no timelapse folder", and asking for a cover image says the same thing instead of the "no cover for this print" it used to claim. Printing is unaffected throughout: the control connection is a separate service, which is exactly why the fault was invisible.
  • Prints queued from a Slicer Pipeline or the Library are checked for enough filament again (#2779, reported by wylyn3d) — A job needing 20.5 g was dispatched onto a spool holding 9 g, and the printer started. The same file, printed from the Print dialog, was correctly refused. The check that stands between the queue and the printer reads the sliced file to learn how much each slot needs, and it looked for that file in the wrong place: a file in the Library records where it lives relative to Bambuddy's data directory, and this one check read that as a path from wherever the process happened to be running. It found nothing, and a source it cannot find has always meant "nothing to verify" rather than "stop" — so the job passed a check that never actually ran. Every path that queues a Library file was affected: Slicer Pipeline jobs, which are always Library-backed, and anything added through the Library's Add to queue. Both the automatic dispatcher and the Play button on the queue were equally blind, so the deficit could not be caught by starting the job by hand either. Prints queued from print history were never affected, and neither was the Print dialog, which finds the file its own way. Two things changed: the check now resolves a Library file the same way the eleven other places that read one already did, and a source file that is configured but missing now writes a warning to the log naming the item and the path it looked at. That case still dispatches — the upload needs the same file moments later and fails there, where blocking would strand a queue on a file the user may have moved — but it no longer passes in silence, which is what let this go unnoticed. Covered by backend tests, including the reporter's exact 20.5 g against 9 g.
  • A Forgejo token limited to a single repository can now be used for backups (#2775, reported by AnthonyGrondin) — Forgejo v15 lets you mint an access token that only reaches one repository, which is the safest token you can give a backup: leak it and the damage stops at the repository it was made for. Bambuddy refused it. Test connection asked Forgejo who the token belonged to before it asked whether the token could reach the repository, and a repository-scoped token is not allowed to answer that question — it may only carry read and write on issues and repositories — so the check failed on a token that would have backed up perfectly well. The identity question is now asked but no longer decides: only an outright rejection of the token is conclusive, and everything else falls through to the repository check, which is the one that matters. Nothing else in a backup ever needed the wider permission — the push writes through the repository's own contents endpoint and a restore reads its commits, trees and blobs — so ordinary tokens are unaffected. The message shown when the repository cannot be reached now names the scope to look for and the possibility that the token is scoped to a different repository, rather than only explaining Forgejo's habit of reporting a private repository as missing. The hint under the token field is also per provider now: it read "fine-grained token with Contents read/write" for all four, advice that only ever applied to GitHub, and now names GitHub's, GitLab's, Gitea's and Forgejo's own scopes in every language Bambuddy speaks. Covered by backend and frontend tests.
  • Files queued from the Library are no longer missing from their owner's queue — On an installation with authentication turned on, a user whose permissions are scoped to their own work saw an empty queue after adding files from the Library, and adding more only added more nothing. The jobs were really there and really printed; they simply belonged to no one. Every queue item records who created it, and the "own queue" permissions decide what to show by comparing that against the signed-in user — but the Library's bulk Add to queue never wrote it down, so its items matched no one and were visible only to users who can see the whole queue. This affected the one path built for adding many files at once, which is where it was hardest to notice something was wrong: the file list on screen looked no different afterwards. The same omission applied to the queue endpoint of the webhook API, whose items are now credited to the owner of the API key that added them. Items that genuinely have no one behind them are unchanged and still belong to no one — jobs sent through a virtual printer, anything added while authentication is off, and keys created before API keys had owners. Covered by backend tests.
  • The drying popover no longer starts a cycle under a material you did not pick (#2774) — An AMS-HT loaded with Support for PLA/PETG offered PLA in the drying dialog's filament list, and the cycle that started was labelled Support for PLA/PETG on the printer's own screen. Opening the dialog prefills it from the spool that is loaded, and Bambu reports that spool's material as PLA-S — a name Bambuddy's table of drying temperatures does not carry. The temperature and duration fell back to PLA's 45°C for twelve hours, which is what the dialog showed and what was sent, but the material did not fall back with them: it stayed PLA-S, and the dropdown, handed a value that is not one of its options, displays its first option without saying so. So the list read PLA while PLA-S was what left for the printer. Anyone who opened the dialog and pressed Start without touching the material was affected; picking any entry from the list, even the same one it was already showing, made the two agree again. The same gap covered every composite — a spool of PETG-CF, PLA-CF, ABS-GF or PAHT-CF prefilled the dialog at PLA's 45°C, far short of what those materials want, and sent its full name as the material. Bambuddy now resolves a spool's material to an entry the list actually has before either value is set, so what the dialog shows and what the printer is told can no longer disagree. Support materials and composites resolve to the material they are built on, so PLA-S dries as PLA and PETG-CF as PETG at 65°C, and nylon is recognised under the several spellings Bambu gives it. Anything genuinely unrecognised still falls back to PLA, deliberately the coolest setting in the table — under-drying an exotic filament costs a cycle, where defaulting to the hottest would deform a PLA spool. This does not address the other half of that report: a printer that keeps showing the material set from its own screen even when Bambuddy names a different one, which needs a capture of what Bambu Studio sends before anything can sensibly be changed. Covered by frontend tests.
  • Configuring an AMS slot shows up on the printer card straight away, without a page reload — Setting a slot to a different filament from the printer card left the card showing the old one. Nothing was lost: the command reached the printer, the printer applied it, and reloading the page or waiting out the thirty-second fallback poll showed the new filament. It simply never arrived on its own. Bambuddy compares each status push from a printer against the last one it broadcast and stays quiet when nothing has changed, which is what keeps a machine mid-print from flooding every open browser tab several times a second. The comparison looked at each AMS tray's slot number, material and load state — and Configure Slot writes none of those. It writes the filament id, the colour, the profile name and the calibration profile. So changing PLA to a different brand or colour of PLA produced a push that looked identical to its predecessor and was discarded, while changing PLA to PETG came through immediately because the material had moved. That is also why Reset always worked: it clears the material. The comparison now covers the filament identity as well, so every kind of slot change reaches the card. Those fields only move when someone configures a slot or swaps a spool, so this adds no traffic during a print — the amount of filament left, which does tick down continuously, is still deliberately excluded. Covered by backend tests.
  • "Any X2D" works on a printer that feeds from external spools instead of an AMS (#2771, reporter Nick-C130) — A fleet of five X2Ds with no AMS units, each printing PETG from its external spool holder, accepted a job sent to a named printer and refused the same job sent to Any X2D: the file uploaded, the printer answered "Failed to get AMS mapping table", and after three attempts the queue item failed. The two paths differ in one thing. A job queued for a named printer carries a filament mapping the browser worked out at the time you queued it; a job queued for a model has no printer yet, so the scheduler has to work the mapping out at dispatch — and its copy of that logic could not see an external spool on a dual-nozzle printer. On an X2D or H2D each filament in the sliced file names the nozzle it feeds, and Bambuddy will only offer a spool to the nozzle it is physically plumbed to. Which nozzle an external spool feeds was being read off a table the printer builds from its AMS units, so a printer with no AMS published an empty table, every external spool came back belonging to no nozzle at all, and the per-nozzle check discarded the only filament on the machine. Nothing matched, and the print went out claiming to use an AMS while carrying no mapping — which is the message the firmware was objecting to. The left and right external feeds identify themselves well enough to be routed without that table, and the printer reports its two nozzles directly, so both are now used. This is the same fault that was corrected in the browser last May for exactly this hardware; the scheduler kept the old logic, which is why the browser-resolved mapping worked and the scheduler-resolved one did not. Single-nozzle printers are untouched — they have no nozzle to route to and never took this branch. Separately, a job whose filament genuinely cannot be matched on a printer with no AMS now fails immediately and says which filament is missing and which nozzle wants it, instead of uploading several megabytes, collecting the firmware's error and failing anyway two retries later; where there is an AMS the firmware error still stands, because there the job can be recovered by loading a spool and pressing Resume. Covered by backend tests.
  • LDAP login works again on directories that define no POSIX group class (#2769, reporter peterskotte) — Every LDAP user on an lldap directory was rejected with "Incorrect username or password", including users whose credentials, search filter and group membership all checked out when tested by hand with ldapsearch, and on an install where Test Connection reported success. The password was never the problem and the directory never saw the request. When resolving a user's groups Bambuddy looks for POSIX groups alongside the usual memberOf ones, and both of those searches name the posixGroup object class. The LDAP client validates class names in a filter against the schema the server publishes, and rejects an unknown one while building the request, before anything is sent. lldap marks every account it creates as posixAccount, which is what makes Bambuddy look for POSIX groups in the first place, but defines no group class beyond groupOfNames — so the search was refused, the refusal travelled all the way out of the login routine, and the login route reports any LDAP failure as bad credentials. A directory with no posixGroup class has no posixGroup entries, which is precisely the answer those searches would have returned, so Bambuddy now treats the refusal as the empty result it stands for, notes it once in the log and carries on with the memberOf groups. The reporter's mapped group is one of those, so it resolves as configured. This is not a regression from the recent primary-group work, though that is the natural suspect: the memberUid search has named the same class since LDAP support first shipped, and it runs for every user whether or not they have a gidNumber, so login has never worked against a directory of this shape. Test Connection passed throughout because it asks only whether any entry exists, a form of filter that carries no class name to validate. Nothing changes for Active Directory or for an OpenLDAP that loads the standard NIS schema — both define the class, and their POSIX groups are still read. Wiki updated. Covered by backend tests.
  • Spoolman no longer charges a Bambu Studio print to the wrong spool (#2768) — A sliced file numbers its filaments 1, 2, 3, 4, and which AMS tray each of those came from is a separate decision made when the job is sent. Bambuddy learns that decision one of two ways: it made the choice itself, for a print started from Bambuddy, or it read the print command as it crossed the local network, for a print sent from a slicer. A job dispatched from Bambu Studio while the printer is signed in to Bambu's cloud satisfies neither — the command travels through Bambu's own broker and never appears on the network Bambuddy is listening to. With nothing recorded, the Spoolman writer fell back to assuming the AMS was loaded in slicer order: filament 1 from the first loaded tray, filament 2 from the second. The reporter's X1C was loaded in the order 2, 4, 1, AMS-HT, so every one of the four was deducted from the wrong spool. It also changed what the print looked like afterwards: on completion Bambuddy stamps the archive with the material and colour of the spools it charged, so the print showed the right filament while it ran and switched to a different one the moment it finished — which is how the reporter noticed. The printer knew the answer all along. It publishes the running job's slot-to-tray assignment in its own status, and Bambuddy's built-in filament inventory has read that field for as long as it has resolved mappings at completion; only the Spoolman writer, which resolves at print start instead, never learned to. It now consults the same two fallbacks at the same moment: the printer's report first, and failing that a colour match of the sliced filaments against the loaded trays, which covers the A1, A1 Mini, P1S and P2S — those models publish no such field, so their owners were on the positional guess no matter how the print was sent. Reading the field at completion rather than at print start is deliberate: a printer keeps publishing the last job's mapping while it sits idle, so consulting it early risks stamping the previous print's mapping onto this one. A mapping Bambuddy or the slicer actually recorded is never second-guessed, so nothing changes for prints started from Bambuddy, from the queue, or over LAN. Cancelled and failed prints take the same correction, since partial usage is charged through the same mapping. The resolved mapping and where it came from are now logged at both print start and completion, so the next report of a wrong deduction can be read straight out of a support bundle. Wiki updated. Covered by backend tests.
  • A drying cycle the printer abandons now says so, and says what the printer reported (#2770, reporter tchavei) — An H2D started a twelve-hour PETG dry at 65°C and the AMS gave up on it twenty minutes in, with 700 of the 720 minutes still on the clock. It then cooled off, humidity climbed back over the threshold, auto-drying started another twelve-hour cycle, and that one was abandoned the same way — a loop the reporter's AMS temperature history shows running all morning. The log had one line to say for it: AMS 0 drying complete, which is exactly what it says for a dry that ran its full twelve hours. Nothing in a support bundle told the two apart, and the remaining time — the one number that does — was written into the line as the previous value, where it reads like a duration rather than a shortfall. Bambuddy did not stop that cycle. Every stop it sends is logged with the full command as it goes out, and there was none, so ending it was the printer's decision — and the only account of why lives in three things Bambuddy already receives and parses but has never written down: the drying phase and sub-phase the AMS reports in its status word, the firmware's own cannot-dry reason codes (which distinguish an overheating unit from one being starved of power by a missing external supply), and whatever HMS errors are live at that moment. A cycle that ends with most of its countdown left now logs all three, alongside how much of the requested duration actually ran and how much was asked for. A cycle that reaches its configured duration keeps the single line it has always had, so a normal dry does not start reporting diagnostics nobody needs, and a cycle Bambuddy itself ends — the print-takes-priority stop, or the Stop button — now says so by name rather than being reported as an unexplained early end, since a stop is short of its duration too and looks identical in the telemetry. This is diagnostics only: nothing about when drying starts or stops has changed, and the repeated restart itself is not addressed here — what the firmware objects to has to be established before Bambuddy can sensibly decide how long to wait before trying again. Covered by backend tests.
  • A drying cycle no longer reports itself finished a minute after it starts (#2759) — Starting the dryer on an AMS 2 Pro holding two PETG and two PLA spools and picking PLA showed "PLA 45°C" for about a minute, then switched to "PETG 65°C" for the remaining twelve hours. Bambu never echoes back which filament or temperature a cycle is running, so the badge reads the target Bambuddy cached when it sent the command — and that cache had been thrown away. Between accepting the command and settling its countdown the firmware publishes one update with the remaining time at zero while the unit is still in its Checking phase; the reporter's log caught 720 minutes, then 0, then 719. Bambuddy read the zero as the cycle ending. Losing the cached target left the badge to guess the filament from the first loaded slot, which happened to be PETG, and its RFID-recommended 65°C — a confident wrong answer for a cycle running PLA at 45. The same false ending also armed smart-plug auto-off-after-drying, so anyone with that switched on had power scheduled to cut one minute into a twelve-hour dry. A remaining time of zero is now only treated as the end of a cycle when the AMS also reports an idle phase, which the firmware already publishes alongside it; stopping a dry early still ends it immediately, and a unit that reports no phase at all still ends its cycles as before. The fallback guess has been tightened to match, in both directions. It names a filament only when every loaded spool agrees on one — on a mixed unit the badge shows the countdown alone rather than naming a spool the cycle isn't drying — and it no longer guesses a temperature at all. A unit loaded entirely with PLA does tell you what is being dried, but not at what temperature: that is picked freely when the cycle is started, so the spools' RFID-recommended value is never evidence of it, and a second AMS loaded only with PLA and drying at 45°C still read "PLA 55°C" whenever the cached target went missing. The badge now names a temperature only when Bambuddy sent it, and shows the filament and countdown without one otherwise. Covered by backend and frontend tests.
  • A print that never starts now says AMS drying was running, instead of blaming the SD card (#2758) — Sending a job to an X2D with two AMS units mid-drying failed silently: the file uploaded, the printer accepted it and then simply stayed idle. Bambuddy waited out the start watchdog, re-uploaded the whole 3MF, waited again, and after three attempts gave up with advice to check the printer's screen and the SD card — while Bambu Studio, asked directly, said it could not start the job because of the drying. Bambuddy now watches the AMS drying telemetry it already receives across the dispatch window and, when a job never starts while a unit was drying, names the units in the failure message and records the correlation in the log from the first attempt rather than only after the retries are spent. This is deliberately a diagnosis and not a rule: the printers concerned support drying continuing through a print, so drying and printing are not in conflict as such, and the report also involved one AMS drying without its external power supply — which would make the start-of-print calibration a power problem rather than a drying one. Stopping the cycle automatically would therefore be acting on a guess, and could tear down drying the hardware was happy to continue. Until it is known which of the two is the real obstacle, Bambuddy tells you what it saw and leaves the call to you. The message for a stalled dispatch with no drying involved is unchanged. Wiki updated. Covered by backend tests.
  • A hand-written systemd service left the Virtual Printer unable to start, with nothing obvious to blame (#2549, reporter Ru3ck3) — The Virtual Printer binds ports 990 and 322, both below 1024, which a service running as a normal user may not do without the CAP_NET_BIND_SERVICE capability. Without it the rest of Bambuddy works perfectly and only the Virtual Printer is dead: its sockets never open, the slicer never finds the printer, and the sole trace is one line in the journal. The reporter lost days to this before someone on Discord spotted the missing line. The install script has carried it since March, but the three other places that define the same service did not — the manual-install template, the combined Bambuddy plus SpoolBuddy installer, and the unit the wiki tells you to paste. All three have it now, and the wiki no longer claims the capability is always included when its own instructions omitted it. Bambuddy also diagnoses this itself: Diagnose on the virtual printer card previously reported only that nothing was listening on port 990, which reads identically to an ordinary port conflict. It now checks whether the process actually holds the capability and, when that is what is wrong, says so and gives the line to add. The check stays quiet when the port is answering, since fronting it another way (an iptables redirect is the documented alternative) is a legitimate setup, and it stays quiet when the capability is held, so a port that failed for some other reason is not misattributed. Existing installs are unaffected until reinstalled; the diagnostic tells you whether yours needs the line. Translated in all locales; wiki updated. Covered by backend tests.
  • A refused AMS filament setting now says so in the log (#2756, reporter Jostxxl) — Configuring a slot publishes an ams_filament_setting command, and the printer answers it with a verdict. That answer was received and then thrown away at debug level, so a printer that refused the write left no trace at the log level support bundles are collected at. The reporter hit exactly that: six manual Configure Slot attempts on one X1C, every one returning success, every one read back by the #2582 verification as still holding the previous profile, and nothing anywhere to say what the printer had made of the command. A refusal is now logged with the printer's own result and reason alongside the AMS and tray it concerned. Only refusals are promoted — unlike the K-profile and drying commands this one is not rare, since every spool assignment and every K-profile re-apply sends one, and logging each acknowledgement would bury the line worth reading. The developer-mode probe is excluded as well: it sends this same command to the external slot specifically to watch it be refused on P1 firmware, so its failure is a measurement rather than a fault. Diagnostics only — nothing about which commands are sent or how they are built has changed. Covered by backend tests.
  • Live updates stopped arriving while the Bambuddy tab was in the background (#2754, reporter mic4rd) — The progress percentage in the tab title froze whenever you switched away and jumped straight to the current value the moment you came back, which defeats the point of putting it in the title. There were two causes, and the first fix only got one of them. Every printer status arriving over the WebSocket was written into the browser's cache from inside an animation-frame callback, and a browser gives a hidden tab no frames at all — those callbacks are not slowed down, they are held, so the connection stayed up, the messages kept arriving, and every one of them parked in a queue that only ran when the tab was shown again. The same applied to the archive, inventory and spool refreshes, and to the queue carrying every non-status message, which stalled completely. Removing the frames fixed that stall but not the report, because the write still went through a 100 ms timer that batches rapid updates — and a timer is exactly what a browser throttles in a tab you are not looking at, to roughly once a second, and to about once a minute once the tab has been hidden for five minutes. The reporter's screenshot showed a tab title reading 2% beside a page at 40%. That batching exists to stop a burst of messages causing a rendering pile-up, and a hidden tab is not rendering, so there is nothing to protect there: while the tab is hidden the value is now written straight through, and the batching still applies while you are looking at it. Worth knowing if you use Windows: a browser window completely covered by another window counts as hidden, not merely unfocused, which is why this could bite without ever switching tabs. Covered by frontend tests that reproduce a hidden tab, including one that never advances the clock — the earlier tests passed by simulating the very timer the browser was throttling.
  • The bug-report button no longer covers the controls in the bottom-right corner (#2750, reporter goodjaltman) — On a phone the floating red button sits on top of whatever else is in that corner, which turns out to be most things: the scroll-to-top button on Profiles was ~83% underneath it and, since both sit at the same stacking level, which one you could actually tap came down to the order they happened to render in. The floating camera window parks there, as do the Group Edit save bar, the bulk-selection toolbars, and — because the button is pinned to the viewport rather than the page — the per-card action buttons on File Manager and Archives simply scroll underneath it. The reporter asked for a switch to hide the button, but it is the only way into the report form, and that form is not just a text box: it runs the printer connection diagnostic, scans your logs against the known-issue catalog, optionally captures five minutes of debug logging and attaches a support bundle. Hiding it doesn't produce smaller reports, it produces reports with nothing attached. So the button moves instead of disappearing. Once the window is narrow enough that the sidebar collapses into a menu button, the

Changelog truncated — see the full CHANGELOG.md for the complete list.

Don't miss a new bambuddy release

NewReleases is sending notifications on new releases.