Updated CHANGELOG

This commit is contained in:
maziggy
2026-08-15 15:32:27 +02:00
parent 95e28e39ce
commit fa5504ee46
+74 -1
View File
@@ -5,13 +5,86 @@ All notable changes to Bambuddy will be documented in this file.
## [1.2.5.3] - 2026-08-15
### Added
- **Restore selected categories from a Git backup commit (#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.
- **Edit the full print-parameter set from the slice dialog** — Slicing from Bambuddy meant taking a process preset as it came; any change meant a round trip through Bambu Studio. The slice dialog now carries OrcaSlicer's full process tree — pages, groups, labels, tooltips, ranges and defaults, extracted from the slicer's own sources rather than restated by hand. Which fields are available depends on the others, and those rules are evaluated from the slicer's own `enable_if` expressions through a small recursive-descent interpreter — no `eval`, so it runs under the Content Security Policy — with enum comparisons checked against each option's declared values. Anything the interpreter cannot decide is left editable rather than greyed out, on the principle that a field you can change is a better failure than one you cannot. Overrides apply after the source's support configuration (#1881) and the designer's carried tweaks (#2622), so an explicit choice always wins, and an untouched panel sends exactly the request it sent before. Adds `slice_engine` as a setting separate from `preferred_slicer`, since where slicing runs is a different question from which binary drives it.
- **A new G-code and model preview, built on the slicer's own renderer** — Sliced files previewed through a vendored copy of PrettyGCode in an iframe, which drew each move as a screen-space line. A line has no thickness in the scene, so it cannot occlude the layer behind it — which is why prints came out stringy and shimmered wherever layers crossed. Being a separate application in a frame, it could be neither themed nor translated, and it carried its own machinery for noticing when a proxy refused the embed. The viewer is now built on libvgcode, the renderer OrcaSlicer draws its own preview with, vendored from three-slicer under the same AGPL licence we ship under. The parser is ours, because upstream renders its own kernel's output and ships no G-code parser at all, and it was written against a real BambuStudio plate rather than against the OrcaSlicer and PrusaSlicer annotations that file does not use. The model preview was rebuilt alongside it: the camera is solved from the subject's bounding sphere against both fields of view instead of a flat multiplier, so a model fills a tall narrow panel instead of floating in the middle of it, and lighting moved from two directional lamps over flat ambient on a Phong material to a MeshStandard material lit through a generated room environment with ACES tone mapping — which is what stops a saturated filament colour clipping to white on its lit side and draining the hue. Translated in all locales.
- **Choose which rack nozzle each filament prints from on an H2C (#1784)** — The Vortek rack holds six hotends, and a multi-colour plate is sliced to use a different one per colour so it can skip the purge. Which of the six each colour takes is not recorded in the 3MF: the same plate, sliced and sent twice from Bambu Studio with a different choice each time, produces two files that differ only in rounding in the last digit of a few extrusion figures. The filament grouping, the toolchange stream, the 120 nozzle-change markers and `project_settings.config` are identical. The choice travels only in the dispatched nozzle mapping, and Bambuddy had no way to state it, so those plates went out with no nozzle assignment at all and the printer chose for itself — which is what levelled on one hotend and printed with another, millimetres above the plate. Every rack-bound filament now carries a position picker beside its AMS slot dropdown, listing all six with the nozzle each holds. A position that is empty, or holds the wrong diameter or flow type, is shown greyed out with the reason rather than hidden, so someone looking for position 4 finds it. The choice is per filament *group* rather than per slot, because a group is one hotend: two filaments the slicer grouped together share it and cannot point at different positions. Translated in all locales; wiki updated.
- **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.
- **The Print Log shows how much filament a run used, and lets you choose its columns (#2636, reporter @ajbastien)** — The log view listed which filament a print used — a colour dot and "PLA" — but never how much, which is the number you actually want when reading back a month of prints. The amount was already on every row the API returned; the table was simply hardcoded to seven columns. **Filament Used** now has a column of its own, on by default. **Cost**, **Energy**, **Energy Cost** and **Finished** join it as columns you can switch on via a new **Columns** picker above the table, which also reorders them by drag or arrow keys and remembers the layout per browser. Every column header now sorts, too — click to sort, click again to reverse, with dates and amounts opening largest-first and text A-Z. The sort runs over the whole log rather than the page on screen, since a table that pages server-side would otherwise answer "the most expensive print among these 25"; changing it returns you to page 1, and rows with nothing in the sorted column are held at the end in both directions so sorting by cost or energy never opens on a screenful of blanks. That last part is explicit rather than left to the database: Postgres sorts empties high and SQLite sorts them low, so the same click would otherwise land differently depending on which one you deployed. These are per-run figures rather than the file's estimate: a print that failed part-way is scaled to the progress it reached, tracked spools win over estimates, and a multi-plate project dispatched a plate at a time counts only the plate that ran — so a row can legitimately differ from the whole-file total on the archive card. Energy arrives from a background task a moment after the print ends, so a just-finished run shows a dash until the measurement lands, which is the honest answer rather than a zero. Note the per-archive **Print Log** button opens a different, smaller table that has shown grams all along; this brings the two into agreement. Translated in all locales; wiki updated. Covered by frontend tests.
- **Auto-orient and auto-arrange when slicing server-side (#2548, reporter @ceokingcobra)** — A slice in Bambuddy always kept the placement the file arrived with, so several parts dropped onto one plate could come back overlapping, and a model resting on a face that needs supports stayed on it. Bambu Studio's two layout buttons simply had no counterpart here. Two checkboxes below the build-plate override now run the same passes before the slice: **Auto-orient objects** turns each object onto the side that prints best — the slicer scores candidate rotations on overhang area, contour and unprintability, and a test part here came out 8% faster with nothing else changed — and **Auto-arrange on the plate** lays the objects out so they no longer overlap, which is also the fix for a source file whose coordinates fall off the edge of a smaller target bed. Both are per-slice and off by default, and neither is stored on a preset or a pipeline: they rewrite placement an author may have chosen deliberately, so they are something you ask for rather than something that happens to you. They stay available on the "Slice as designed" path, unlike the preset dropdowns and bed type — these act on the geometry rather than the print config, so where the settings came from is irrelevant to them — and they carry across the automatic retry that falls back to a file's embedded settings after a slicer crash, which would otherwise hand back an un-arranged result you had in fact asked for. Arrange is project-wide inside the slicer, so pairing it with **Slice all plates** would collapse every plate's objects onto a single bed; Bambuddy takes the same per-plate loop it already used for cross-class re-slices and merges the outputs, since that hazard belongs to the flag and not to the one case that first ran into it — including on the "Slice as designed" path, and with the crash-retry suppressed there, because a retry is a single call that would have handed back one consolidated plate for a job you asked to slice as several. Orient needs no such handling — it rotates objects where they stand and never moves one between plates. A cross-class re-slice still arranges whether or not the box is ticked, because that pass is what keeps it clear of the H2D's per-nozzle dead zones. An unticked box is sent by leaving the field off the request entirely: the sidecar treats any value that is present as true, so a literal "false" would have auto-arranged every slice in Bambuddy. Translated in all locales; wiki updated. Covered by backend and frontend tests.
- **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.
- **Server-side slicing on an ARM64 host (#1900, contributor Felix Reissmann)** — The slicer sidecar ships as an amd64 image, so ARM64 installs had no server-side slicing at all and the compose file's own header said so. There is now an override file that pins the sidecar's platform to `linux/amd64` and runs it under emulation, with the setup written where it is read rather than a hop away in the wiki: binfmt has to be registered on the host or the container dies with "exec format error", and emulation costs roughly three to six times native slice time. The override only applies while both `-f` flags are on the command line and every other instruction in the README is written bare, so an ARM64 user following the update steps would have dropped the platform pin without noticing — a manifest error today, and a silent switch off emulation once native ARM64 images ship. The quick start now writes `COMPOSE_FILE` into `.env` instead, so the rest of the file works unchanged on ARM64; verified both ways. A separate x86_64 machine stays the recommendation — emulation is the fallback for people who have no second machine, not a replacement for one.
- **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.
- **Open a multi-plate sliced file on the plate you asked for** — Previewing a sliced multi-plate 3MF from the File Manager showed a plate nobody picked. The library route took no plate parameter at all, so the one the viewer has always put in the URL was dropped — FastAPI discards unknown query parameters silently — and both routes then fell back to the first `.gcode` member of the zip, whose order is whatever the slicer wrote. The reported file stores `plate_2.gcode` ahead of `plate_1.gcode`. Nothing that opens the viewer from the File Manager passed a plate either, so there was no way to ask for a different one. Plate resolution now lives in one place and both routes share it: an explicit choice that does not exist returns a 404 rather than rendering something else, and the default is the lowest-numbered plate rather than the first one in the archive. The viewer gained a plate switcher and keeps the choice in its URL, so a link to one plate survives a reload, and filament colours follow the switcher — they were previously taken from the first plate whichever one was on screen.
- **Show the plug that actually powers the printer in the card's Power row (#2830)** — A printer card has one Power row: a plug name, its draw, and the auto-off and on/off buttons. Which plug filled it was decided by nothing at all — the endpoint returned the first row the database handed back that was not a Home Assistant script, from a query with no `ORDER BY`. For the reporter that was an enclosure exhaust fan, added before the outlet their X1C is plugged into. The card showed the fan's name with `--` for watts, offered to switch the printer off by cutting the fan, and demoted the metered outlet to the small Home Assistant button row. The fan was marked as not powering the printer and hidden from the card, and neither setting was consulted here, though the same flag has decided the scheduler's power-on pick since #2629. The candidates are now ranked: switchable at all, controls printer power, enabled, shown on the card, reports power, lowest id. The first rules out a script, which can only be run, and a monitor-only MQTT plug, which the control endpoint rejects — and an MQTT plug is exactly the kind that reports watts, so without that ahead of the power tiebreak the row could land on a plug whose on/off button answers with an error. The last is not cosmetic: with no `ORDER BY`, a plain `UPDATE` on PostgreSQL can move a row and silently swap which plug the card calls the printer's power. None of the criteria excludes a plug outright, because a printer whose only plug is hidden, disabled or monitor-only still needs its Power row — that row is where the on/off button and the Home Assistant buttons live.
- **The Printers page remembers its status and location filters (#2833)** — Pick a location, navigate away, come back, and every printer was showing again. Both filters were plain component state, and they were the only preferences on that page that were not remembered: sort order, card size, view mode, collapsed sections and hide-disconnected all persist, each with the same shape. These two now do too. A saved filter needs a way out, though. The location dropdown only renders while at least one printer has a location, so a saved location that was later renamed or removed would match nothing and take its own dropdown off screen with it — an empty page and no control to undo it. A location that is not among the available ones now resets to all, and the same for a status the dropdown does not offer. That check waits for the printers query to resolve, because the list is undefined while it is in flight and acting on that would throw the saved filter away on every page load. Search stays unpersisted on purpose: a box that silently refills itself on return is a surprise rather than a convenience.
- **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.
- **Uploaded archives can be named after the filename you sent (#2610, contributor @Person2099)** — A 3MF carries a `print_name` inside its metadata, and that is the name an archive gets. It is often the wrong one: whoever originally sliced the file baked their own title into it, and that title survives every rename afterwards, so a file you deliberately named after the job it belongs to still lands in the archive list under a stranger's label. Bambuddy could already prefer the filename — the FTP review flow and virtual-printer dispatch both do, driven by the virtual printer's **Archive name source** setting — but the upload endpoints could not, so anything sent through the API was stuck with the embedded title. `POST /archives/upload` and `POST /archives/upload-bulk` now take an optional `prefer_filename_for_name` query parameter that names the archive after the uploaded file instead; on the bulk route it applies to every file in the batch. It is off by default, so nothing changes for existing callers or for the Web UI's own upload, which does not set it. A per-request parameter rather than a setting, because the caller — typically an integration naming files after its own jobs — is the only party that knows whether the filename it sent is the meaningful one. Both parameters are documented in the OpenAPI schema, so they show up in `/docs`. Covered by backend tests.
- **API clients can resolve user ids to names (#1894)** — Archives, the queue and statistics all report ownership as a numeric id, and statistics accept it as a filter, but nothing let an API key discover whose id was whose. The only user listing returns emails, roles, group membership and full permission sets, so it is administrative and rejects keys outright. There is now a slim user listing returning id and username only, behind its own read permission mapped to status-read. It grants no data a key could not already reach: for API-keyed requests the filter-by-user guard already short-circuits and the created-by filter is already honoured for every id — what was missing was the ability to address the filter, not permission to use it. The full listing stays admin-only. `/auth/me` was fixed alongside it, having answered an API key with a synthetic record rather than the key owner's own.
- **Record who queued a file from the Library and through the webhook API** — The queue's own-work permissions filter on who created an item, but only three of the paths that create queue items were setting it. The Library's bulk **Add to queue** required the create permission and then discarded the user it had just resolved, so every item it made was ownerless — and invisible to the person who added it if their permissions are scoped to their own work. That is the one path built for adding many files at once, which is where it was hardest to notice. The webhook endpoint has no request user, but an API key records its owner, which is the acting identity everywhere else a key is used, so its items are credited to that owner. Keys minted before per-user ownership have no owner and their items stay ownerless.
- **Forgejo tokens scoped to a single repository are accepted (#2775)** — The Forgejo connection test asked who the token belonged to before asking whether it could reach the repository, and treated a 403 there as fatal. A Forgejo v15 repository-scoped token may carry only read or write on issues and repositories, so it fails that call — and was rejected despite reaching its own repository perfectly well, which is all a backup needs: the push path uses the Contents API and restore reads commits, trees and blobs, all under the repository itself. That was the only call in the whole provider layer that asked about the user. The probe stays, because a 401 from it is genuinely conclusive and names a bad token before the repository call has to guess — Forgejo v15 and later hide a private repository behind a 404 rather than a 403, so the repository call cannot always tell those apart. Every other status now falls through to the repository check.
- **The MQTT debug log now records the commands sent to a printer, not only what it reports back** — **Printer → 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.
### Changed
- **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 H2C nozzle rack card sizes itself to its contents and numbers its slots** — The rack card shared a row with the nozzle, bed and chamber readings but was given a 190-pixel floor plus twice their growth share to draw six fixed 28-pixel chips. On anything wider than a compact card it claimed several hundred pixels and left most of them empty, and the width came out of the cards that needed it — a combined dual-nozzle reading was wrapping "220 / 220" onto two lines beside a mostly blank rack. It now takes its content width and gives the remainder back, while still giving way on a narrow card rather than overflowing. Each slot also carries its physical rack position, 1 to 6, below the chip, 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. The chips also scale with the card now: they were a hard-coded 28 pixels at every size while the body type and icons around them grow by 20% at L and 40% at XL, so setting the card larger left a shrunken strip beside neighbours that had grown around it, with the diameter figures pressing against chip edges that had not moved.
- **The process-settings panel shows the preset's own values, and says why when it cannot** — The panel baselined every field on the option schema's compiled-in defaults, so a preset setting a 0.42 mm line width displayed 0 — the C++ default meaning "derive from the nozzle". Every field was affected; the line-width group just made it obvious. Bambuddy cannot answer this on its own: a standard-tier pick is only an `inherits` stub on our side, and local or cloud presets are deltas whose remainder lives in the profile tree bundled inside the running sidecar. The values now come from the sidecar's own resolve endpoint, which runs the same resolver a slice does against the same profiles, so what the panel shows cannot disagree with what a slice produces. Deliberately not the local profile resolver, which walks OrcaSlicer's published tree and can differ from the image actually installed. An untouched field shows the preset's value and reverting returns to it, and the modified marker compares against that baseline too, so fields the preset moved off the C++ default are no longer flagged as user edits. When the values cannot be read, the panel now names which of the four causes applied instead of collapsing them into one message. The overwhelmingly common one has an obvious fix and is not an edge case: an install pulls its sidecar as `latest` whatever channel Bambuddy is on, so a current Bambuddy talking to a sidecar that predates the resolve endpoint is the normal state rather than a misconfiguration, and those users would otherwise have seen an amber warning on every slice with nothing pointing at the sidecar image.
- **Pool AMS Filament Backup spools in the print dialog's filament check** — The dialog weighed each plate against the spool in the slot it mapped to and knew nothing about AMS Filament Backup, so a two-plate job needing 1441 g of ABS was refused against a 1000 g spool while the identical full spool in the next slot went uncounted. The dispatcher has pooled matching spools since #1762 and would have run the print — "Print anyway" was always the right answer to this warning. The rule for which spools back each other up now lives in one place that both the dispatcher's pool and the dialog's check draw on. The dialog groups on the keys it is handed rather than resolving spools a second time, which is what let the two answers drift apart in the first place, and which also gives the check to Spoolman users.
- **API keys can read and run slicer pipelines (#1425)** — Every pipeline endpoint answered 403 to an API key whatever scopes the key carried. The first pipeline PR parked all three permissions on the admin denylist until the run dispatch existed to decide about; it landed later and the parking was never revisited. Pipeline read now rides status-read, and pipeline run requires queue and library-manage together — a run slices into the library and then queues prints, so mapping it to either flag alone would hand that flag the other one's authority. A 403 now names every flag the key is short of. Pipeline write stays admin-only: a key can run the recipe, not rewrite it or clear the log. Opening the run route also needed the cloud-owner fallback the direct slice route already makes, since a pipeline can carry Bambu or Orca Cloud presets and resolving those reads a token off a user record an API-keyed request does not have.
- **Sort the inventory by colour (#2729, reporter @macwhiz)** — The Color column showed a swatch on every row but ignored clicks on its header, so the only way to find a shade was to sort by colour *name* — and filament names don't sort into anything useful, with "Abyssal Purple" landing at the top of the list nowhere near "Purple". The column sorts now, by the colour itself: ascending walks the rainbow, then the browns, then the neutrals from white through the greys to black, with any spool that has no colour recorded held at the end. The issue asked for a straight hue-then-saturation-then-lightness sort, and that turned out not to survive a real inventory — a grey is only a shade off neutral but still has a hue, and that hue can be anything. Measured against a 30-spool inventory, it put Titan Gray in among the blues and a warm grey next to the reds, while brown, being a dark orange by hue, split the oranges in half. So the colour family leads and the continuous sort the issue asked for runs inside each family, using the same classification that already names a colour when it isn't in the colour catalog — which means the Color column and the Color Name column can never disagree about what counts as brown or grey. Within the neutrals, hue is discarded rather than sorted on, for the same reason it isn't trusted to pick the family in the first place: they run light to dark instead. Multi-colour spools sort by their primary colour, since a spool has to sit in exactly one place. Works the same for internal and Spoolman-backed inventories, and in both the table and card views. Wiki updated. Covered by frontend tests.
- **Chamber temperature can now be set up to 65 °C, not 60 (reported on Discord)** — Every field in Bambuddy that takes a chamber target stopped at 60 °C: the per-filament chamber map and the per-print chamber override in **Preheat & Heat Soak**, the chamber quick-select presets, and the chamber temperature control on the printer card. 60 is the ceiling for the X1E, which was the only heated-chamber model when that limit was written; the H2 series (H2C, H2D, H2D Pro, H2S) and the X2D heat to 65, so the top of their range was simply unreachable — an ABS or PA profile calling for 65 had to be run at 60. The ceiling is now 65 everywhere, held in one constant on each side rather than repeated as a literal at every call site, so the four surfaces cannot drift apart again. X1E owners are unaffected: its firmware clamps a higher request to its own maximum. Wiki updated. Covered by backend 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.
- **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 bug icon moves into that top bar and the corner is left alone; above that width nothing changes. That threshold is the one the layout already switches on, so there is no new breakpoint and no third state to reason about, and it covers tablets and half-width desktop windows rather than only phones. The report form itself is now a proper bottom sheet on phones, which also fixes it hanging 16 pixels off the left edge of the screen — it was sized to the full viewport width and then inset from the right, so a strip of the form was simply unreachable on anything under about 460 pixels wide. The scroll-to-top button on Profiles has been nudged clear of the corner as well, for the wide layouts where the floating button stays. Wiki updated. Covered by frontend tests.
- **The Docker update command is copyable, and knows where your compose file lives (#2664, reporter @pchulpjoost)** — Settings → Updates told Docker users to run `docker compose pull && docker compose up -d`, a command that does nothing unless you are already standing in the directory holding your compose file — which is exactly what you came to the page not knowing. There is now a copy button beside it, and the command includes the `cd` when Bambuddy knows where to send you. It can work that out on its own only when you bind-mount a subdirectory of that folder (`./data:/app/data` gives it away); the shipped compose file uses named volumes, which resolve to a path under Docker's own storage and say nothing about where your file is. Compose does record the answer on every container it creates, but reading it back needs the Docker socket mounted into Bambuddy — root-equivalent access to your host in exchange for a convenience string, which is not a trade worth offering. So there is a **Compose directory** field in the same panel, saved with the rest of your settings, and a `BAMBUDDY_COMPOSE_DIR` variable in the compose file for anyone who would rather keep it there. Leave it empty and the command is printed without the `cd`, rather than with a guessed path that fails on paste. The field accepts path characters only: it is the one setting whose whole purpose is to be pasted into a root-capable terminal, so a value like `/opt/bambuddy; rm -rf /` would otherwise render as a perfectly plausible update command. Translated in all locales; wiki updated. Covered by backend and frontend tests.
- **Server-side slicing is no longer offered for STEP files** — The Slice action appeared on `.step` and `.stp` and the endpoint accepted the job, but neither slicer can load one from its command line. So the file was read, converted and uploaded before failing as "the input model file to the slicer can not be parsed", which reads as a corrupt model rather than an unsupported format. The endpoint now refuses a STEP up front with a message saying to export it as STL or 3MF, and the Slice and pipeline buttons no longer appear on one. **Open in Slicer** is unchanged and still hands a STEP to the desktop application, which opens it fine — that was always the working path. The two predicates are now separate so they cannot drift back together.
- **Form controls follow the page's colour scheme** — The parts of a form control the browser draws itself — a number input's stepper, a date field's calendar button and popup, a select's dropdown, scrollbars, the autofill tint — were painted in the light appearance on every theme. Bambuddy switches theme by swapping CSS variables under a class, which the browser cannot see, so it assumed the page was light and matched the steppers to a white background that was not there. Declaring the colour scheme alongside the variables fixes all of them at once, in both directions. Three date and time fields that had been pinned to dark by hand to work around this no longer need to be — being pinned, they were wrong on the light themes.
- **A print stage Bambuddy cannot name is now logged at INFO** — The stage table is hand-maintained and every new model adds to it, so a printer occasionally reports a number that is not in it and the card reads "Unknown stage (72)". An H2C did exactly that, where the table runs to 66 and then jumps to 74. Stage transitions were logged only at DEBUG, which is off in normal running, so the only record that it had happened was the card itself — and by the time anyone looked, the printer had moved on. The asymmetry is the point: a stage we can name is worth DEBUG, and the one we cannot is the interesting one. An unnamed stage is now logged once per stage number per session at INFO, with the model, the stage it came from and the print state at the time, which is what naming it afterwards needs. Named stages are unchanged, so a normal print logs nothing new.
- **The Slicer Bundles notice is gone from Settings** — Bundle import was withdrawn in 0.2.5 and the panel was kept behind as a static notice pointing at the alternatives. It has been on screen for several releases, it was shown to everyone running the slicer sidecar whether or not they had ever imported a bundle, and it was a card in **Settings -> Queue & Dispatch** that could not be acted on. The component, its render site and all thirteen locales' strings are gone, and the remaining columns rebalance to fill the space. No slicing behaviour is touched. Four docstrings that still described the removed feature as a live fallback were corrected at the same time.
### Fixed
- **An H2C levelled on one hotend and printed with another (#2800)** — An H2C ran its startup clean and bed levelling on one hotend, switched, and then printed several millimetres above the plate. The same job from Bambu Studio was fine. The H2C is the only model that mounts its nozzle from a rack of six, and a print command names that nozzle by physical rack position — the firmware reports those as 16 to 21 — not by the extruder index 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 pick and replays it (#1780); anything queued from the library, an archive, the webhook or a slicer pipeline carried none, so the field was omitted and the firmware chose for itself — and its choice need not match what the file was sliced for. The scheduler now derives the per-slot extruder assignment from the file it is about to send, and the MQTT layer resolves it against the rack position the printer reports live. Both halves are needed: the file knows which side a slot prints from, only the printer knows which hotend is in the carriage, and it can be swapped from the touchscreen between queueing a job and printing it. Two hardware-derived values were then corrected by the reporter's own A/B on real hardware. The rack is not on extruder 0 — a mixed-nozzle plate dispatched with the rack position on the wrong side printed several millimetres above the bed. And correcting only that is not enough, because the fixed hotend answers to physical id 1 rather than to its extruder index of 0, and forwarding the index produced a command the printer rejected outright. Translating both carriages gives the mapping the printer accepts, and the same sliced file then cleaned, levelled and printed on the correct nozzle at the correct Z through to completion. Both values agree with three native Bambu Studio captures from the same machine. The carriage assignment itself was inverted alongside: extruder index 0 was treated as the fixed hotend and index 1 as the rack, and it is the other way about. The printer's own telemetry, a Bambu Studio dispatch that completed, and the two constants failing to agree with each other all say so; the reasoning is now recorded at the constants so a future regression report is not re-litigated from scratch.
- **An H2C refused a multi-colour print as a hotend mismatch** — The print uploaded, the printer took the command, and stopped at once with HMS 0500-4047 — "the available hotend quantity or model does not match the sliced file". The nozzle mapping told the printer one of the plate's filaments went to no hotend while the AMS mapping named the tray it comes from, and the firmware will not start a job on that contradiction. Each filament in a 3MF names the group it belongs to, and on every other dual-nozzle Bambu the group number is also the extruder index, so it was read as one. On a rack machine it is not: the rack carriage holds six hotends to the fixed carriage's one, so the slicer writes a group per nozzle rather than per carriage. The failing plate carried groups 0, 1 and 2 against a two-entry map, and the filament in group 2 was dropped — indistinguishable downstream from a slot the plate does not print, which is what reached the wire. The group is now resolved through the table the file states for itself in its slice info. Files carrying no such table keep the direct index, so H2D slices are unaffected, and a filament that still cannot be placed drops the whole mapping with a logged reason rather than sending half an answer.
- **The nearest filament colour is picked, not the first one in tray order (#2804, #2823, contributor @grolmus)** — When the exact colour a plate asks for is not loaded, Bambuddy substitutes an eligible spool. It took the first eligible one in tray order, which is an arbitrary answer to a question that has a right one. It now ranks them, and the ranking is perceptual. The first pass measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required dark green, a purple was the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000, with the backend and frontend implementations kept structurally identical so they can be read side by side — verified against the published reference set to 1e-4 on all 31 pairs, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel box, so this only reorders spools that already qualified. Filament type matching now agrees between the interface and the scheduler as well: Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch — so the badge contradicted what the printer was about to do.
- **Archives that arrived empty, from printers whose file service could not answer (#2780)** — Two faults behind the same symptom. Bambuddy reads a print's 3MF, cover and timelapse over FTPS on port 990, which on every Bambu model serves external storage only. Under some configurations H2-series and P2S firmware keeps the sliced file on internal storage, where Bambu Studio put it over a different service, and then no path on 990 can find it. The print command has always said which of the two it used, and we discarded the field and swept anyway: around 110 connections per print, every one certain to fail, ending in an archive card with nothing on it and no stated reason. In the reporter's bundle all 35 dispatches to their H2C and P2S said internal storage, all 25 to their X1C said external, and all 44 empty cards belonged to the first two. The field is now read, the sweep is skipped when it cannot succeed, and the reason is recorded. A printer that uses the card is unaffected, and so is one we have no answer for — silence is not evidence, and reading it as bad news would break archives that work today. The answer is held per print and dropped when that print ends rather than kept as a standing fact about the printer, because plenty of prints never announce themselves: 14 of the 79 print starts in that bundle arrived with nothing on the request topic, started from the printer's own screen or picked up after a restart, and one slicer print to internal storage left standing would suppress the lookup for every screen-started print after it. Separately, two printers went on printing while every archive they produced held nothing but a filename: Bambuddy opened port 990, the printer accepted the connection and answered with something that was not TLS, and the connection failure was indistinguishable to every caller from "the file is not at this path". So the 3MF lookup walked all six filename variants across five directories with four retries each, the cover endpoint ran its own sixteen-path sweep, and the timelapse scan added four more, all against a printer that could not have answered any of them — one reporter's log carried 1813 identical handshake failures, another's 3511. The evidence says this is the printer's own file service getting stuck rather than a model, firmware or TLS-configuration problem: in one bundle the same two printers ran clean for two weeks and failed from a fixed date, in another an X2D served files for five days, flipped, then failed every connection for eight days with no successes, while the same models and firmware appear in roughly twenty other bundles with no occurrences at all. A TLS error on connect now opens a five-minute cool-off for that printer, so a wedged printer is contacted twice an hour instead of hundreds of times a minute, and the single warning that is logged names the remedy. The sweeps stop early while it holds, and the cover endpoint returns a status naming the file service instead of a 404 that reads as a missing file.
- **A completion for one print closed another print's queue item (#2829)** — Bambuddy has no run identifier tying a completion to a queue row, so it finds the row by printer and printing status alone. Any completion delivered for a printer therefore closed whichever job was printing on it: the job was marked completed while the printer was still working, left the queue for history, and stranded the rest of its batch, because the queue correctly refuses to dispatch onto a busy printer. The handler now checks the completion against the file the row was dispatched with, recovered from its archive, and leaves the row alone when they disagree. Only a positive disagreement refuses — no archive, no file name or no subtask name is unverifiable rather than wrong, and refusing those would strand the item and wedge the queue, which is the failure the loose lookup was avoiding in the first place. That check then had to learn how the printer writes the name. It does not echo it verbatim: it substitutes underscores for spaces, so a title with spaces came back with underscores, the check refused it, and the row stayed printing — and since every printing row counts as a busy printer and nothing else ever closes one, that printer's queue stopped until someone cancelled by hand. It also truncates long names and marks the cut, which would have done the same to any long title. The comparison is now on the canonical form — case, spaces and underscores — which is the rule the 3MF lookup in the same module has always used for the same names, and a truncation marker is treated as a prefix match. The check keeps its purpose: the same printer the same day still correctly refused a completion for its own calibration run. One comparison being stricter than reality should not be able to stop a queue indefinitely either, so the scheduler now closes a row itself when it has been printing for five minutes after its printer went terminal, with the status that state implies. A real completion arrives within seconds, so this only ever sees rows that were already stranded, and a disconnected printer never qualifies. It restores the queue only — notifications, billing and auto-off are not replayed minutes late.
- **Deleting a library file destroyed the jobs queued against it (#2819)** — Nothing tied a library file to the queue rows pointing at it, and the foreign key that describes the relationship cascades on delete — which SQLite does not enforce and PostgreSQL does. So the same fault had two faces: rows left pointing at a file that no longer existed, failing at the printer with "library file not found" days later, or rows deleted outright with no error and no history. Two routes led into it, and both are fixed by taking the queue off the file before the row goes. In the reported case, a quantity above one on the printer card's upload-and-print flow puts a cleanup-after-dispatch flag on every copy and the clone carried the library file id onto each one, so the first dispatch consumed the file the rest were waiting on; the copies are now pointed at the archive that dispatch just created, which holds its own copy of the file.
- **A library-backed job was dispatched onto a spool that could not finish it (#2779)** — A job needing 20.5 g was dispatched onto a spool holding 9 g and the printer started. The source resolver returned the library file's stored path verbatim, but that column holds a path relative to the data directory — so it resolved against the process working directory, found nothing, and the deficit check treated a missing source as "nothing to verify" and reported no deficit. Every library-backed queue item was affected: slicer pipeline jobs, which are always library-backed, and everything added through the Library's bulk **Add to queue**. Both callers share the resolver, so the Play button on the queue was as blind as the auto-dispatcher. Archive-backed items were never affected, and neither was the print dialog, which resolves the file on its own path. The library branch now uses the same idiom as the eleven other readers of that column: absolute stays, relative joins the data directory.
- **A job queued to a printer class never powered a printer on (#2786)** — Queue a print against a printer class — "Any X1C", or a slicer pipeline whose target is a printer class — with every printer of that class switched off, and nothing happened. The job sat pending and no smart plug was touched, while the same file pinned to a specific printer powered that printer on within one queue check. The reporter's log holds both halves: thirteen minutes of the item being polled and passed over, then a change to a specific printer, then "attempting to power on via smart plug" on the very next tick. Same item, same plug, same auto-on setting. Powering a printer on had only ever been written inside the branch that handles a pinned printer. The model-based branch below it walks the same queue, but its matcher classes an offline printer as a reason to keep waiting and nothing on that path ever looked at plugs. It now wakes a printer of the class the same way the pinned path does.
- **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.
- **"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.
- **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.
- **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.
- **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.
- **Bambu Cloud's anti-robot challenge is explained instead of repeated (#2790)** — A reporter tried to connect to Bambu Cloud and got "we need you to confirm you are not a robot" as an error toast, with no challenge anywhere to answer and nothing to click. That sentence is Bambu's, not ours: their anti-abuse layer had flagged the network and was answering the sign-in with a challenge body. Bambuddy had no idea what that was. The reply is well-formed JSON, so the Cloudflare detection — which triggers on an unparseable body, on Cloudflare markers, or on particular statuses — never fired on it, and the login fell through to the generic error path, which lifts the message out of the body and hands it to the interface verbatim. 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. The challenge is now recognised for what it is and explained, with what to do about it.
- **Interactive controls show a pointer cursor again (#2791)** — Hovering most of Bambuddy gave an arrow rather than a hand. Not everywhere, which is what made it read as sloppiness rather than as 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 in its corner did not, and dropdowns went either way with no pattern behind it. The pattern was there. Tailwind v3's base styles set a pointer cursor on buttons; v4 dropped it to match the browser default, which for a button is an arrow. Bambuddy has been on v4 since the frontend was built and never had a base rule restoring it, so a button only looked clickable where someone had written the class by hand — 15 of 934 buttons had, 0 of 149 selects, and 19 of 130 checkbox and radio inputs. The 233 ad-hoc usages are why it looked arbitrary instead of uniformly broken. One base rule now covers buttons, selects, checkboxes, radios and summary elements.
- **A refused frame no longer leaves the browser's own error page inside Bambuddy (#2787)** — A reporter uploaded an STL, sliced it, and got a frowny icon and "refused to connect" when previewing the sliced file, while the STL's own preview worked. That is the browser's blocked-by-response page drawn inside our layout shell, and the split between the two previews is where the cause is: an STL or source 3MF renders in the page, a sliced file opens the G-code viewer, which is the only thing in Bambuddy that frames a Bambuddy page. Our own headers permit that frame and the frame is same-origin, so a refusal means a stricter header was added after we replied — a reverse proxy, a security add-on, an auth gateway — none of which the user could see. Bambuddy now says which header blocked it and points out that the viewer opens perfectly well in its own tab.
- **The Spool Inventory header no longer scrolls the whole page sideways (#2813)** — Five header buttons in a row that could neither wrap nor shrink came to roughly 600 pixels, so on a 390-pixel screen the header ran past the viewport and took the whole page with it, since the main element is the scroll container. They now stack on small screens and wrap, matching the pattern the Statistics, Settings and Archives headers and this page's own filter bar already use. Identical at 640 pixels and above. The System Information header had the same construction and gets the same treatment.
- **The Virtual Printer card header wraps instead of painting outside its border (#2808)** — Every item in the collapsed header refused to shrink, so the row was as wide as its contents and the card does not clip — the remote-interface address and the enable toggle painted outside the card border. The name's truncation could not save it, because a flex item does not shrink below its text by default. The metadata now wraps to a second line while the chevron, status dot and toggle stay outside that group. Wrapping rather than truncating, because the bind and remote-interface addresses are what the page exists to show. It needs three things at once to appear, which is why it took a report: both addresses set, a target named from discovery, and the three-column card grid.
- **The printer card thumbnail is back after navigating away and returning (#2826)** — The cover URL is cache-busted on the print name, which does not change while a print runs, so leaving the printers page and returning re-mounts with a byte-identical source — which the browser serves from its in-memory cache with no network request. That is why the reporter's network panel was empty while the placeholder sat there. The loaded flag could only ever be set by the load event, and a mount effect reset it unconditionally; for a cache hit those are two racing tasks with no ordering between them, and when the load event won, the effect undid it, with nothing to put it right afterwards because the URL does not change again for the rest of the print. The effect now reads the element's own state instead of assuming nothing has loaded, which settles it whichever task wins. Being a race is why it reproduced every time for the reporter and not at all here. Only the printer card was affected — archive thumbnails build a fresh URL every mount and always hit the network.
- **A slice failed on a model whose name contains a slash (#2832)** — A print's display name comes from inside the 3MF rather than from the filename, so a MakerWorld title arrives with its punctuation. The slice-to-archive sink used it verbatim for the output folder and the output file, and a slash in a folder name is not a character — it is another folder. Creating parents made the level it implied and the file's own join added a third that nobody had made, so the slice failed with a missing-path error on a path that half existed, and renaming the print first was the only way through. A display name is now reduced to a single path component before it becomes one. Characters a name cannot hold are replaced rather than dropped, so the folder still reads like the model's title, and the set is the one the SD card already rejects — which covers a Windows install too, where a colon fails the same way. The name shown in Bambuddy is untouched: a title is allowed its punctuation, and refusing the slash would reject the name this was reported about. The joins are now genuinely asserted to stay under the archive directory, which a stale marker had been claiming without it being true. The library sink takes the same embedded name and gets the same reduction — managed storage names the file after a UUID and never saw this, but an external folder writes the name as given.
- **A slice of a file on a network share was written to managed storage instead (#2810)** — Slicing always wrote to the managed library directory while giving the new row the source folder's id, so slicing a file on a NAS mount produced an output that showed up in the right folder in the interface and never reached the share — invisible from the web interface, which is why it did not reproduce. The destination is now resolved from the target folder the way uploads and moves already do, marked external, and stored with its absolute path. Collisions uniquify rather than failing, because a conflict would throw away minutes of CPU on a routine re-slice and overwriting a file on someone's NAS is worse. An external folder that cannot take the file falls back to managed storage rather than discarding the slice, and says why in a warning — silent fallback is what made this invisible.
- **A 3MF no longer switches off supports its process preset turned on (#2820)** — Since #1881 four support fields travel from the source file over the picked process preset, because the loaded settings are authoritative and Bambu's shipped presets all disable supports — so without the carry, a project exported with PVA in the interface slot sliced single-material. But the carry ran in both directions, and the off direction is the one nobody asked for: nearly every published model ships with supports off, so slicing one against a custom preset that deliberately enabled them stripped them back out. The reporter's preset enabled supports with a normal auto type; the slice came back disabled and tree auto, and only the style survived because it is not one of the four carried. The source can now switch supports on and never off. Nothing is lost, because every shipped preset has them off, so a preset that has them on is a deliberate choice.
- **An oversized model reads as an oversized model, not a slicer crash (#2802)** — The sidecar caps model uploads and reported a rejection as a bare 500 "file too large", because that error is not the sidecar's own error type and falls through to the default status. A 500 reads as a crash inside the slicer, and the one message Bambuddy had about request size was written for the status a reverse proxy sends, so it never appeared. The reporter tried three unrelated environment variables, stopped nginx, and moved from Windows to Docker, none of which the sidecar reads. The rejection is now matched by what it says rather than by its status, so an installation still on an older sidecar image gets the same explanation. The match is strict — the body must be only that message — because a genuine slicer failure is also a 500 and has to keep reaching the embedded-settings fallback. Old images are told to update, since they have no setting to change; current ones are told which setting to change. The update advice was wrong too: it told users to run a bare `docker compose pull`, and the sidecar is declared behind a compose profile, which compose skips silently — so the pull was a no-op for exactly the users the message was written for, and the restart policy kept the old container serving. The reporter pulled, restarted, raised the limit and got the same rejection, because the image never changed. Both commands now name the service, which enables the profile implicitly without pulling a second 220 MB image onto a host that never asked for it.
- **A preview slice no longer gives up on custom G-code the sidecar cannot parse** — Opening the slice dialog on an unsliced project runs a preview slice purely to ask the slicer which AMS slots the chosen plate consumes. Bambu Studio 2.8 writes a conditional into the machine's timelapse G-code but exports no definition for the variable it tests, so the template is unresolvable the moment it leaves Studio and an older sidecar stops with a placeholder parse error before producing any slice information. The preview returned nothing and the caller silently fell back to guessing from painted faces — on the H2D project this was found with, the guess dropped the support material, a whole slot off a four-filament plate. The preview is now retried once with just the named template emptied, still on the file's own settings. Keeping the embedded settings is what keeps the answer honest: overriding the process preset instead discards the project's support configuration, which loses that slot and moves the filament estimate by up to a factor of two. Measured against the same file, the retry reproduces all four slots gram for gram while a printer-and-process override returns three.
- **Bundled presets resolved to a generic start G-code block** — A bundled preset can keep a setting in a companion file the preset itself does not reference, which the desktop slicer finds by name. Walking only the inheritance chain never reached it, so every one of the 56 instantiable BBL machine presets resolved its start G-code to the 577-character generic block instead of its own 6.5 to 21 KB one. That block holds the AMS load and the claim-action calls, so a print sliced from it heats the bed, moves the toolhead and extrudes nothing. Companions are now folded into each ancestor as the chain is walked, at that ancestor's precedence, so a caller's own value still wins and the 0.2, 0.6 and 0.8 variants reach their 0.4 sibling's companion; they are found by listing rather than by a fixed set of keys. Covered against the shipped bundle rather than fixtures — a new end-to-end spec resolves all 56 presets inside the image and fails on any that still lands on the generic block.
- **AMS drying was torn down and restarted once per scheduler tick (#2801)** — A printer in FINISH with an unacknowledged plate and something pending in its queue stopped and restarted drying on every scheduler tick, for as long as the plate stayed unacknowledged; the reporter's Home Assistant history recorded about 2000 state changes over ten days. No cycle ever ran long enough to remove moisture, and cycles the user had started by hand on other AMS units of the same printer were torn down with them. Two concerns had become tangled. Plate-clear answers whether the bed is ready for the next job and says nothing about whether the AMS may heat, and the gap between a finished print and the acknowledgment is when drying is most useful — the printer is free and nobody is waiting on it. Leaving a plate unacknowledged is also how people hold the queue by hand, so the hold was costing them the drying it should have enabled.
- **Auto-drying re-armed into a threshold it could never reach (#2770)** — An H2D armed five twelve-hour drying cycles inside four hours, one of them six seconds after the previous one ended, and none ran more than a couple of hours. Two things combine. The firmware ends a cycle when it decides the filament is dry rather than when the clock runs out, and reports no fault doing it — across this printer's history the run length tracks how wet the spools were, from nearly the full twelve hours starting at 32% down to minutes once the unit sat at 10-13%. That part is the AMS doing its job. The loop is ours: an AMS reports a higher relative humidity while it is warm than once it has cooled, and the same unit read 10-13% cold against 15-20% through every cycle, so with the threshold set between the two the reading at the moment a cycle ended always re-armed the next one.
- **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.
- **The drying badge invented a temperature on a uniformly loaded AMS (#2759)** — A second AMS 2 Pro with no auxiliary power, loaded entirely with PLA and drying at the 45 °C the reporter picked, showed 45 °C and then switched to 55 °C. Bambu never echoes back a cycle's filament or temperature, so both come from the target cached when the command went out, and the fallback for a missing cache reads the loaded trays. The first pass narrowed that fallback to units whose spools agree on a filament, which fixed the mixed-unit case in the original report but left the uniform case answering with the spools' RFID-recommended drying temperature. Agreement across slots is evidence of what is being dried, because the dryer heats all of them; it is no evidence of the temperature, which is picked freely in the popover. The badge now says nothing rather than guessing.
- **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.
- **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 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.
- **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.
- **Photos and filament accounting on archives that arrive without a 3MF (#1820)** — Two faults behind the same kind of print: one that arrives without a retrievable 3MF, which on an H2S is any job started from the printer's own internal library. Such an archive has no file path, and the empty path's parent is the current directory, so every site that derived the archive's folder from it landed on the data directory itself. The finish-photo capture spotted that and wrote somewhere else; nothing else did. The photo was written in one place and looked for in another — reads failed, deletes dropped the name and left the file, and the notification attachment never found the image. Hand-uploaded photos worked only because upload and read agreed with each other rather than with the capture. The question now has one owner and all four sites ask it, with lookups still checking the old shared location so photos already uploaded there stay reachable. Separately, the remaining-percentage fallback that stands in for a missing 3MF can charge nothing for several reasons and did so without a word. The AMS reading is coarse and, on the reporter's printer, noisy: it rises mid-print, swings five points over a job, sits at 100% through a 36-minute print on a fresh spool, and goes negative on a nearly empty one — which the start-of-print gate rejects, dropping the only slot that was printing. Two of their prints went uncounted for two different reasons and both read as "no spools updated", which is also what a print with nothing to charge reports. Each case now names the slot and the two readings, on the Spoolman path and on the internal-inventory path alike.
- **Home Assistant notifications carry nested data through unchanged (#1441)** — The pass-through flattened anything below the top level of a notification's data, so a payload with a nested structure — the shape most Home Assistant actions expect for anything richer than a title and a message — arrived at the other end reshaped. Nested structures are now carried through as given.
- **AMS Filament Backup no longer charges a whole print to the substitute spool** — Everything the completion path needs in order to split a print's filament across the trays it fed from lived only in memory: the dispatched plate and its slot-to-tray mapping, the spool-assignment snapshot, and the tray-change log. A print that outlived a restart lost all of it and fell back to what the printer reports at completion, which with AMS Filament Backup on is the substitute tray — so the whole print was charged to the spool that only finished it, while the spool that ran dry was charged nothing. That context is now persisted, tray changes are appended as they happen, and both the session and the printer's tray-change log are restored at restart recovery, seeded from the current tray when there is nothing to restore.
- **The Print Log is reachable again once you have no archives** — The Archives page decided it had nothing to show before it checked which view you were on, so with zero archives the "No archives yet" card replaced every view including the log. The Print Log is a separate table that deliberately outlives the archives it refers to — deleting an archive only clears the reference, and clearing the log is its own action — so purging archives hid a history that was still in the database, with no way back to it short of re-adding an archive. The log view now renders its own empty state instead of borrowing the archive one. Wiki updated. Covered by a frontend test.
- **The Print Log's cost and energy figures were never sent to the browser** — Bambuddy has been recording what each run cost and how much power it drew, but the two Print Log endpoints built their responses field by field and never mentioned `cost`, `energy_kwh` or `energy_cost`. A field nobody names comes back as its default, so the values arrived as nulls — indistinguishable from a column that genuinely holds nothing, with no error and no log line to say otherwise. The same trap had already swallowed the failure-cause classification once before. Both endpoints now validate straight off the database row, which removes the opportunity to forget a field rather than fixing the three that happened to be missing. Existing rows need no migration: the data was always there. Covered by backend tests.
### Security
- **Cleared every remaining `npm audit` and `pip-audit` finding** — A full dependency pass across both ecosystems. `react-router` and `react-router-dom` move to 7.18.2: the router advisory had been carried as a documented exception in the audit gate because its only fix was a major version, and upstream backported it, so the exemption lapsed on its own — an entry only holds while the fix is a major. The allowlist is now empty; the machinery stays for the next one. `dompurify` moves to 3.4.13, which ships in the application but on a path that is unreachable here — no hooks are registered and the in-place mode is never used. `js-yaml` and `nanoid` are overridden, both development-only and reached through eslint and postcss; the eslint path calls only the safe loader, on a legacy configuration file this repository does not use. Verified with eslint, the production build and the full frontend suite on the new versions.
- **Patched two build-time frontend dependencies flagged by `npm audit` (GHSA-r28c-9q8g-f849, GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895)** — `postcss` 8.5.15 → 8.5.23 fixes a path traversal in its source-map auto-loader (`sourceMappingURL`) that could disclose arbitrary `.map` files, and `brace-expansion` (pulled in transitively by `eslint` via `minimatch`) is bumped through the existing `overrides` block (`^5.0.7` → `^5.0.9`) for a denial-of-service via unbounded expansion. The first `brace-expansion` advisory was answered in 5.0.8 by capping the length of the combined result, but that cap covered only the accumulator the results are merged into and not the two intermediate arrays that feed it — so a small brace pattern could still exhaust the heap, fatally and beyond the reach of a `try`/`catch`, or stall the event loop for minutes. 5.0.9 bounds both arrays as they are built. Both packages are build/lint-time tooling only — neither is part of the shipped app, so no running Bambuddy install was exposed. `postcss` moved within its existing range; `brace-expansion` needed the pin because `npm audit fix` can't lift `eslint` to the patched transitive on its own.
## [1.2.5.2] - 2026-08-02