Note
This is a daily beta build (2026-07-30). It contains the latest fixes and improvements but may have undiscovered issues.
Docker users: Update by pulling the new image:
docker pull ghcr.io/maziggy/bambuddy:daily
or
docker pull maziggy/bambuddy:daily
**Tip:** Use [Watchtower](https://containrrr.dev/watchtower/) to automatically update when new daily builds are pushed.
Added
- P2S/X2D accessory fans: left auxiliary cooling and chamber exhaust (#2691, contributor gzimbric, requested in #2660) — The P2S and X2D have two fans Bambuddy could not show or drive. The left auxiliary part cooling fan had no tile and no control at all, because the printer only reports it inside its air-duct data and never in the ordinary fan fields Bambuddy was reading. The chamber exhaust fan had the opposite problem: its tile appeared on every P2S whether or not the fan was fitted, so owners of a base machine had a control that did nothing. Both are add-on kits on the P2S and fitted at the factory on the X2D. Both tiles now appear only when the printer itself reports the hardware, so a base P2S looks exactly as it does today and a kitted one gains the fans it actually has. The left auxiliary fan is set from the same speed popover as the others, and the enclosure fan is labelled Exhaust on the P2S and X2D — matching the printer's own screen and Bambu Studio — while every other enclosed model keeps Chamber Fan. The confirmation message after changing a speed uses the same name as the tile that was clicked. The four tiles are ordered part cooling, left auxiliary, auxiliary, exhaust, so they read left to right in the same order as the physical fans. Both fields are also published through the status endpoint, the WebSocket feed and the MQTT relay, so external automations can read them. Translated in all locales; wiki updated. Covered by backend and frontend tests, including the case where the printer sends a partial fan report — a tile must not disappear mid-print just because one update didn't mention it.
- Live print progress in the browser tab (#2693, contributor Chachigo, requested in #1041) — Watching a print meant keeping the Bambuddy tab in view, or switching back to it every few minutes. Enable Print progress in tab under Settings → Appearance and the tab title becomes
42% · Bambuddywhile the favicon turns into a progress ring in your theme accent colour, both updating live over the WebSocket the rest of the UI already uses. With several printers running, the tab follows the one finishing soonest, tie-broken by highest progress; title and favicon return to their defaults as soon as nothing is printing or the toggle goes off. Off by default, and stored per browser (like the light/dark toggle) so a wall-mounted dashboard and a laptop can each have their own setting. Translated in all locales; wiki updated. Covered by frontend tests. - Telegram notifications can target a forum topic (#1518, reporter vmhomelab) — Telegram groups with Topics enabled always received Bambuddy's notifications in the General topic, because only Bot Token and Chat ID were configurable. Getting a per-printer split therefore meant creating a separate chat per printer. The Telegram provider now takes an optional Forum Topic ID — the last number in a topic's link (
t.me/c/1234567890/25) — and routes its messages into that topic, so a single group can carry one topic per printer. Left empty, the behaviour is unchanged. The ID is sent on both the plain-text and the thumbnail code paths, and is validated as a number in the form and again server-side, so a typo is reported instead of silently breaking only text notifications. Translated in all locales; wiki updated. Covered by backend and frontend tests. - Per-file print progress inside a project (#1897, reporter FedericoPuntelli) — Projects that consist of many distinct files each needing N prints (e.g. 13 plates × 10 sets = 130 prints) only had aggregate progress; finding out "how many times have I printed plate_7?" meant reading the Activity Timeline line by line. Projects now take an optional Copies per File target: each printable file in the project's linked folders shows an X / N badge with a mini progress bar (gray not started, amber in progress, green target reached), and the progress card gains a Complete Sets bar — the minimum per-file count, i.e. how many finished assemblies you can ship right now. Without the new target, files simply show a printed-count badge (3×). Counting matches the aggregate stats: only completed runs, attributed to a file by a new
library_file_idstamp on queue-dispatched archives, with content-hash and filename fallbacks covering historical prints. Also fixed along the way: files queued from a project-linked File Manager folder now attribute their prints to that project — previously only prints started from the project page counted toward project statistics. Translated in all locales; wiki updated. Covered by backend and frontend tests. - Users can now delete empty folders in the File Manager (#1781, reporter cadtoolbox) — Library folders have no ownership tracking, so folder deletion was gated entirely behind
library:delete_all— a regular user withlibrary:delete_owncould create folders and delete their own files, but the emptied folder sat there until an admin removed it. Users withlibrary:delete_owncan now delete folders that are truly empty: no subfolders, no files — including trashed ones, since deleting a folder would silently drop another user's trash-restorable files. External folders (operator-configured mounts) and folders linked to a project or archive still requirelibrary:delete_all, even when empty. The folder tree's Delete entry enables accordingly, with a "You can only delete empty folders" tooltip on non-empty ones; the bulk-delete API applies the same rule. Translated in all locales; wiki updated. Covered by backend and frontend tests. - AI failure detection is now visible on the printer cards (#1546, reporter Jeff-GebhartCA) — Previously the live Obico classification (safe / warning / failure, smoothed score) was only visible under Settings → Failure Detection, so tracking how detection matched an ongoing print meant flipping between the Printers screen and Settings. Each printer card's badge row now shows an AI badge whenever detection is enabled for that printer, like the other health badges: gray Idle while no print is being watched, then green Safe, amber Warning, or red Failure while a print is actively monitored. The tooltip carries the current score, and clicking opens a modal (like the HMS error badge) with the live status, score, frames analyzed, and the detection service's last error — plus a shortcut to the full settings. Toggling detection on or off updates the cards immediately. Printers excluded from monitoring and setups without failure detection show nothing. Served by a new lightweight
/obico/printer-statusendpoint readable with printer permissions alone (the existing settings-gated endpoint is unchanged and keeps configuration private). Translated in all locales; wiki updated. Covered by backend and frontend tests. - Bark is now a notification provider (#1495) — Bark is the open-source, account-free iOS push app (self-hostable via bark-server), popular especially with Chinese-speaking users. Configure it with just the device key from the app; the server URL defaults to the official
api.day.apprelay and accepts a self-hosted instance. Optional settings: notification Group, Sound, and iOS Interruption Level — Time Sensitive breaks through scheduled summaries, Critical bypasses Silent mode and Focus (useful for print-failure alerts), Passive delivers silently. Send failures wrapped in an HTTP 200 body by bark-server are detected and reported properly. Translated in all locales; wiki updated. Covered by backend and frontend tests. - Home Assistant notifications can carry custom data fields (#1441) — When a notification provider targets an HA notify service (e.g.
notify.mobile_app_myphone), a new optional Data (JSON) field is forwarded as the service call's nesteddataobject — the same place HA automations put mobile push options likepriority,ttl,channel, andgroup.ttl: 0+priority: highmake Android pushes arrive immediately instead of batched, andchannelgives printer alerts their own notification channel/sound. The field is JSON (not key=value lines) so numbers stay numbers (ttl: 0) and nested options work. Validated on both ends: the UI rejects malformed JSON before saving, and the sender fails loudly with a clear message rather than posting a half-built payload. Only included when configured — the default persistent-notification path is unchanged, as its schema rejects unknown keys. Translated in all locales; wiki updated. Covered by backend and frontend tests. - Energy usage now feeds the statistics that previously only knew about filament (#1432) — Bambuddy has measured per-print energy via an attached smart plug for a while (the plug's lifetime counter is captured at print start and the delta stored with the print), but two stats surfaces ignored it. First, the Most Expensive record on the Statistics page ranked prints by filament cost alone, so a cheap-filament print with hours of heated-chamber time could never win; it now ranks by filament + measured energy cost (prints without a smart plug simply compete on filament cost, as before). Second, Filament Trends gained an Energy Over Time chart — per-day kWh (per-hour for short ranges, per-week for long ones), with the range's total kWh and energy cost in the header. The chart only appears when the selected range actually contains measured energy data, so setups without smart plugs see no change. The
/archives/slimstats feed now carries each run'senergy_kwh/energy_cost. Translated in all locales. Covered by backend and frontend tests. - The plate-clear gate is now visible over MQTT, and can raise a notification (#2525, reporter daschaefer) — When a print reaches a terminal state, Bambuddy holds the queue until someone confirms the build plate is clear. That gate was visible only in the Web UI: the printer's own MQTT push reports nothing beyond
RUNNING/PAUSE/FAILED/FINISH/IDLE, so an external automation could not tell "finished" from "finished and still waiting for a human". The per-printer status topic now carries anawaiting_plate_clearfield, and every transition is additionally published on a new retained topicbambuddy/printers/{serial}/plate_clear({"awaiting": true|false, …}). Retained and published from the flag itself rather than from printer telemetry, so a subscriber learns the current state of every printer the moment it connects — and the state stays correct after Auto Off powers a printer down, which stops telemetry entirely and would otherwise leave the status topic frozen atfalse. Publishing is edge-triggered: the queue re-asserts the flag on every dispatch, and no subscriber should see a "plate cleared" for a plate that was never dirty. A matching Plate Clear Required notification event was added, off by default on every provider because it fires after every print at the same moment as the print-complete alert. Acknowledging still goes through the existingPOST /printers/{id}/clear-plate. Translated in all locales; wiki updated. Covered by backend tests. - Re-slicing a model designed for another printer can now keep the designer's print settings (#2622, reporter kpp39) — Published models often deviate from the stock Bambu profile on purpose: five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a different printer discarded all of it, because the picked process preset overrides the file's embedded settings — that override is exactly what makes cross-printer re-slicing work, so it could not simply be dropped. "Slice as designed" (#2611) was no help here: it is all-or-nothing and only offered when your printer already matches the design's target. The slice dialog now shows a Keep the designer's settings panel listing precisely which print settings the author changed away from the stock profile, and what each was set to, with a checkbox per setting. Design-intent settings — wall count, infill density and pattern, layer and first-layer height, supports, seam position, brim, ironing — are ticked by default. Printer-specific ones — every speed and acceleration, jerk, fan speeds, temperatures, prime-tower geometry — are listed with a badge but start unticked, because a value tuned for the author's machine can be merely wrong on yours or outside the range your printer's profile accepts, which fails the slice outright. Nothing is guessed: Bambu Studio records the deviating-settings list inside the 3MF itself, so the panel shows the author's own change list. Only ticked settings are sent, only the process slot is carried (your filament picks are untouched), and the panel is hidden for files that change nothing. Translated in all locales; wiki updated. Covered by backend and frontend tests.
- The size-S printer card now shows remaining time, ETA and layer progress (#2674, reporter jakestatefarm1101-alt) — Size S existed for exactly one job: watching a whole fleet on one screen. But it rendered only the printer name, a status pip and a progress bar — every other block on the card is gated behind the expanded view — so it could not answer the question that view is for, "which printer finishes first". Dropping to S to fit more printers meant losing the information you dropped down to compare. The compact card now carries one line of metrics under the progress bar while a print is running: remaining time, ETA in your configured 12/24-hour format, and layer progress — the same values the Medium card already shows, using the same formatters and the same ETA styling so the two read alike. Each value is omitted individually when the printer doesn't report it, and the row holds its height when nothing is printing so cards don't shift as prints start and finish. Card dimensions and grid density are otherwise unchanged. Frontend-only. Wiki updated. Covered by tests.
Changed
- Debug logs now record what the printer reports between the last layer and the end of a print (#2547, reporter anthonyma94) — The finish photo wants a moment that Bambu firmware does not obviously announce: printing done, toolhead parked, filament unload not yet started. Bambuddy has been driving that capture from
stg_cur=22("Filament unloading"), which turns out to fire on no model at all — across 247 support bundles there is not a single stage-22 capture, including the window in which it was the only trigger in the code, where all 104 captures on A1, A1 Mini, H2C, H2D, P1S, P2S, X1C and X2D fell through to the after-the-fact fallback. Choosing a replacement was not possible from the bundles we had, because outsidestg_curandmc_print_sub_stageevery stage and action field the printers send is dropped unread, and the most promising candidates (print_real_action,mc_action,mc_stage) are absent from A1, A1 Mini and P1S payloads entirely. With debug logging enabled, Bambuddy now dumps those raw fields for the window between the last object layer and the end of the print — opening on the first end-of-print signal (last layer reached, progress at 99+, or no remaining time), logging only what changed frame to frame, and closing on the state transition — so a single debug bundle per model can show whether any firmware marks that moment. Diagnostics only: nothing reads these values, they are printer telemetry with nothing identifying in them, and at normal log levels the probe does no work at all. Covered by tests for the window boundaries, the frame budget and the guarantee that the probe cannot break status ingest.
Fixed
- A model sliced for PETG printed as PLA, and the print dialog then refused to match PETG (#2712, reporter kpp39) — Slicing a MakerWorld model with a PETG profile produced G-code the printer wanted PLA for, and the Filament match step offered no way to correct it. Root cause. The list of filaments the slice dialog shows is positional: the first row is the printer's first slot, the second row its second, and so on down to the slicer itself. For a model that already carries slicing information, Bambuddy listed only the slots that print — which is the right answer when you are starting a print and Bambuddy has to match spools in the AMS, and the wrong one here. The reported model declares four filaments and paints with the fourth alone, so the dialog showed a single row; the PETG chosen in it became the first slot, and the fourth — the one the model actually prints with — kept the profile baked into the downloaded file. The result was a genuine PLA print, so the Filament match step was right to insist on PLA. Fix. When picking profiles for a slice, the dialog now lists every slot the project declares, with the ones this plate doesn't print with shown greyed out as before, so each row lines up with the slot it stands for and a choice made in the fourth row reaches the fourth slot. Starting a print is untouched and still asks only for the spools the job needs. Covered by tests for a source whose only printed slot is the fourth, for the print path keeping the shorter list, and for the chosen profile arriving in the right position.
- A finished slice produced a stream of a dozen "Sliced ..." notifications — One slice reported itself complete over and over, a notification every second and a half for as long as twenty seconds. Root cause. While a slice runs, Bambuddy asks the server how it is getting on every 1.5 seconds — but it never waited for an answer before asking again. Slicing a large project keeps the server busy for seconds at a time, so those questions piled up unanswered, each one still believing the job was running. When the server caught up it answered all of them at once, and every single answer was treated as the moment the slice finished: one notification each, one list refresh each. The bigger the project, the longer the pile and the more notifications. Fix. Bambuddy now waits for an answer before asking the next question, so nothing can pile up and a busy server isn't asked to do more work while it is already behind. A job's completion is also recorded once and only once, and a check that was already in flight when the tracker restarts now stops instead of finishing its work — either of which is enough on its own to keep a duplicate off the screen. Covered by tests for a server stalled across many intervals, for two slices finishing where one restarts the tracker, and for the same job being tracked twice in a row still reporting both times.
- Slicing a single-plate project failed on filament slots the plate never prints with (#2711, reporter kpp39, also seen by phi-schi) — Sending a MakerWorld project to the slicer was rejected with "filament preset ... (slot 1) is not compatible with printer ...", naming a slot the model doesn't use, and the slice modal deliberately locks the dropdowns for unused slots so there was no way to correct it by hand. Root cause. A project can declare more filaments than any one plate paints with — the reported model declares four and uses one — and the slicer validates every filament it is handed, not just the ones the print touches. Bambuddy already rewrote those unused entries to match a slot the plate really uses, but only when the plate number was part of the request. The modal omits it for single-plate projects, since there is no plate to choose, so the rewrite never ran for them — which is every model imported from MakerWorld. The three idle slots therefore arrived carrying whatever the source file had baked in, in this case profiles for an entirely different printer, and the slicer refused the job on the first one. Fix. A missing plate number now means the first plate, which is what it means everywhere else in the slicing path, so single-plate projects get the same treatment multi-plate ones already had. Also fixed: "Slice all plates" reached the same code, where it isn't a plate number at all. On a project with a dedicated support filament that combination could rewrite every colour to the support material and silently slice a multi-colour model in one filament — it is now excluded, since across all plates no slot is unused. Covered by tests for a single-plate slice with no plate number in the request, for slice-all leaving every slot untouched, and for the support-filament case specifically.
- A database hiccup during dispatch could leave a queue item stuck and the next print of that file filed under the wrong archive — When PostgreSQL briefly refused a connection in the middle of dispatching a queue item, the dispatch failed part-way through and left two things behind. Root cause. Bambuddy tells itself to expect a print just before it sends the print command, because the printer can report the job before the send even returns — but nothing undid that expectation when the command was never sent. The entry does expire after two hours, which is far longer than it takes to react to a failed job by pressing print again: that reprint was folded into the old archive and inherited its filament mapping and plate instead of getting a fresh one. Separately, releasing the row's dispatch lock is best-effort and needed the same database that had just refused, so it gave up after one attempt and the item stayed invisible to the scheduler until a restart. Nothing was ever sent to the printer — the failure happens before the print command — so this cost a stuck item, not a wrong print. Fix. An expectation is now withdrawn whenever the print command doesn't go out, which also covers two cases that were silently leaking before: a job cancelled during dispatch, and a print command the printer rejects. The dispatch lock is retried rather than abandoned after one try, and any lock left behind is released on the next quiet moment instead of surviving until a restart. Covered by tests for the withdrawal being an exact inverse of the registration, for a confirmed print keeping its expectation, for two dispatches not disturbing each other, and for the lock recovering from both a brief and a sustained database outage.
- Bambuddy can be configured to want more database connections than PostgreSQL will give it — The connection pool's own ceiling is 100 per worker process by default, while a stock PostgreSQL allows 100 in total and reserves 3 of those for administrators. Nothing checked the two against each other, so the mismatch only appeared as a failure somewhere unrelated once the connections ran out — which is how the dispatch failure above happened. Fix. Bambuddy now compares the two at startup and, when the pool could ask for more than the server allows, logs a warning naming both numbers, how many connections are already open, and the settings to change. Both figures are also included in the support bundle, so it is possible to tell a misconfigured limit apart from connections being held too long. Nothing is adjusted automatically: the pool is sized before any connection exists to ask the server with, and the right ceiling depends on how many worker processes you run and what else shares the server. Unchanged on SQLite, which has no such limit, and a server that declines the question is ignored rather than delaying startup. Covered by tests for the warning content, the reserved-slot arithmetic, the values reported to the bundle, and startup surviving a refused probe.
- An unusable layer number from a printer could drop its connection (#2702 follow-up) — Reading the current layer from a status message assumed it would always be a number. Anything else raised an error out of the routine that reads those messages, and nothing above that point catches it, so the connection's listener stopped and the printer looked silent until the staleness check rebuilt it — losing not just the layer number but the print-start and print-finished detection carried in the same message. An unusable value is now ignored, holding the last known layer rather than substituting zero, which the firmware uses to signal a cancellation. This is the same containment applied to the layer total in this release, three lines away in the same routine. Covered by tests.
- The layer count stayed empty for a whole print, and First Layer Complete notifications read
1/0(#2702, reporter sn8key) — On a P1S, a job started from Archives or through the Virtual Printer showed no layer information in the Print Status panel, and the Discord First Layer Complete notification went out as1/0instead of1/33. The count usually appeared some minutes into the print, which made it look intermittent, and the reporter noticed it filling in at the exact moment they submitted a bug report. Root cause. Bambuddy applies the printer's reported layer total as soon as it arrives, and separately clears that total when it detects a new print starting, so a leftover count from the previous job cannot become the denominator for this job's filament split. Both happen while processing the same status message, in that order — and the message announcing a new print is frequently the same one carrying the new print's layer total, so the value was stored and then immediately discarded. That is not merely late but unrecoverable: Bambu firmware sends only fields whose value has changed, so having published the total once, the printer never mentions it again. It could only come back in a full status refresh, which Bambuddy requests when it connects and when you press Force Refresh, but not during a print. Submitting a bug report happens to trigger one, which is why that appeared to fix it; and an install whose connection drops from time to time recovered by itself within minutes, which is why the fault looked random and why a stable connection made it worse rather than better. Everything that shows a layer count reads this single value, so the finish-photo trigger that fires on the last layer and the mid-print filament split were affected the same way. Fix. The new-print reset now keeps the total that arrived alongside the starting message, while still discarding anything left over from the previous job. If the starting message carried no total, Bambuddy asks for a full refresh once, and once more if layers are being laid down and there is still no total — by which point the printer certainly knows it. That is at most two extra messages per print, and never a per-layer retry. One incidental fix: an unusable layer total —null, or anything that isn't a number — previously raised an error out of the routine that reads status messages. Nothing above that point catches it, so the whole message was thrown away and the connection's listener stopped, leaving the printer to look silent until the staleness check rebuilt the connection. Such a value is now ignored, and the refresh described above then fetches the real total. Covered by tests for the same-message case, the case where the total arrives a message earlier, the previous job's total still being discarded, the one-shot bound, the refresh answer not re-triggering the reset, and the malformed values. - Support bundles could contain a printer-status file that no tool could open (#2702) — Every support bundle includes a redacted copy of the printer's last status message, so that a given model and firmware's actual field layout is available when diagnosing a report. For printers whose AMS flow-calibration factor had a particular number of decimal places, that file was not valid JSON. Root cause. Redaction ran over the finished text of the file, and one of its patterns recognises Bambu serial numbers by shape — a shape the digits of a decimal number can also take. A calibration value of
0.0199999995529652came out as0.[SERIAL], which is not a number, so the file failed to parse from that point on. Fix. Redaction now walks the structure and rewrites text values only, leaving numbers, true/false and empty values exactly as the printer reported them, so the file always parses. Nothing is redacted less thoroughly than before. Covered by tests using the value from the bundle that exposed this, and for the walk leaving keys, booleans and nested containers alone. - Setting Spoolman options over the API with a true/false value returned a server error —
PUT /settings/spoolmanaccepts a free-form body, and sending the natural JSON form for a switch —{"spoolman_enabled": true}rather than{"spoolman_enabled": "true"}— came back as a 500 with nothing useful in it. The shipped UI always sends strings, so this only affected people driving Bambuddy from a script or a Home Assistantrest_command, which is exactly where a real boolean is the obvious thing to send. Root cause. Settings are stored as text and every reader compares them as text, but the submitted value went in untouched. Deciding whether Spoolman had just been switched on called a string operation on it, which a boolean does not have; and the raw boolean was also written straight to a text column, which SQLite quietly turns into 1/0 while PostgreSQL refuses it outright — so the stored result depended on which database the install used. Fix. Boolean-ish settings are now converted to a canonicaltrue/falseon the way in, accepting real booleans,1/0, and the usual spellings (True,yes,on) case-insensitively, since this is a documented API that scripts talk to. A value with no sensible reading, such as"banana", now returns a 400 naming the field instead of being stored as-is and silently treated as off. Two details are preserved deliberately: a blank value still means "use the default" for the two options that default to on, and reading a stored value stays as strict as it has always been elsewhere in the codebase, so no existing row changes meaning. Text options are checked too, so a JSON object can no longer be stored as its own printed form. One incidental improvement: a value stored asTrueby an earlier API call showed as off in the UI, which compares case-sensitively, while the backend treated it as on — canonical storage removes that disagreement. Covered by tests across the accepted spellings, the rejected values, the blank-means-default behaviour, and the read path. - External-camera timelapses and finish photos came out empty when the live view was open (#2707, reporter bitbarista) — On a printer with an external camera, watching the live view while a print ran meant the layer timelapse recorded almost nothing and the finish-photo notification went out with no image attached. The reporter measured zero of 87 layer captures on one print and zero of 105 on another, both watched from start to finish. A USB camera allows exactly one program to hold it open, so every capture taken during a live view failed outright rather than merely degrading. Root cause. Bambuddy already knew not to do this for the printer's built-in camera: a snapshot taken while somebody is watching reuses the viewer's frame instead of opening a second connection (#1348, #1271). That rule was never extended to the external-camera paths — and could not have been, because the buffered frame it depends on was only ever published by the built-in paths. The live external stream tracked when frames arrived but never kept one, and the stream hands out frames already wrapped for the browser, so there was nothing for a consumer to reuse. Fix. The external stream now publishes each frame as it goes past, and every one-shot consumer — layer timelapse, the finish photo and its fallback, the notification snapshot, Obico polling and the plate check — reuses that frame instead of opening a second handle. If a viewer is attached but no frame has arrived yet, that single attempt is skipped rather than competing, because kicking the viewer off is worse than missing one frame. Two things improve as a side effect:
/camera/snapshotand the finish-photo fallback chain can now serve an external camera's live frame, where before they found an empty buffer, and the buffer is released when the last viewer of that printer leaves. Covered by tests for the frame plumbing, a buffering failure being unable to break the live stream, and each consumer in all three states — viewer with a frame, viewer without one, and nobody watching. - A long-running camera stream could eventually stall itself, with nothing in the log to explain it (#2707) — ffmpeg's error output was only read when something had already gone wrong, which meant that for the entire life of a working stream nobody read it. ffmpeg writes a startup banner, its analysis of the incoming video, and then a progress line at a steady rate, and the operating system only buffers a fixed amount of that before it stops the writer. Once that happened ffmpeg would block trying to write the next line, stop producing frames, and the stream's own timeout would fire — reported as
RTSP read timeoutand a reconnect, with no indication that Bambuddy had starved it. How long it takes to reach that point is unmeasured and clearly long: one stream ran 21 minutes 36 seconds without trouble, so this is a limit that was being ignored rather than a fault anyone has reported. Fix. A streaming ffmpeg's error output is now read continuously and the most recent portion kept, so the limit cannot be reached. The kept portion is what gets logged when a stream does fail, which is more useful than before: it holds what ffmpeg said as things went wrong, where reading the buffer on demand returned whatever it had printed first — usually the startup banner, which is then discarded as noise. Credentials are masked on this path through the same single funnel as every other camera log. Covered by tests for continuous draining, the size bound, keeping the newest output, credential masking, and the ownership handover with the shutdown path — reading the same output from two places at once is an error, so only one owner reads it at a time. - Reopening the camera quickly could leave the new stream invisible to Bambuddy (#2707) — Closing a camera view and opening it again straight away could leave the newly started stream unregistered, even though it was running and showing frames. The consequences were all indirect, which is what made it hard to spot: Bambuddy believed no viewer was attached, so Obico polling and snapshots would open a second camera connection and fight the live view — precisely what the guards added in #1348 and #1271 exist to prevent; the background cleanup task saw a camera process with no stream attached to it and killed the live stream as an orphan, usually within a minute; and pressing Stop reported that it had stopped nothing while the view was still running. Root cause. Each printer's fan-out stream was registered under a key derived from the printer alone, so every successive stream for that printer reused the same key, and the departing stream's cleanup removed whatever was registered under it — including its own replacement. The same cleanup also cleared the printer's most recent camera frame unconditionally, discarding the new stream's frame. It needed the old and new streams to overlap, which the four-second teardown fixed above made easy to hit. Fix. Each stream now gets its own registry key, so one stream can only ever clean up after itself — the same approach the external-camera path already uses (#2675) — and the shared per-printer frame is only released when no stream for that printer is left running. Covered by tests for both halves, including one that drives the real cleanup path with a second stream already registered.
- Closing the camera held the printer's camera connection for four more seconds, then logged an error that wasn't true (#2707) — Every time a camera view closed, the log recorded
ffmpeg didn't terminate gracefully, killingand thenffmpeg did not exit within 2.0s of SIGKILL; abandoning wait. Both waits expired every single time, so each close cost a fixed four seconds — and because Bambu firmware allows exactly one camera connection, that was four seconds in which nothing else could use the camera: reopening the view, a snapshot, Obico, or the diagnostic. Root cause. ffmpeg is started with its output and error streams as pipes, and the shutdown path had stopped reading them. A process whose output pipe is full blocks mid-write, and ffmpeg's shutdown signal only sets a flag that its main loop checks on the next pass, so the polite request could never be acted on and the grace period was dead time. The forced kill did work — but Python cannot report an exit while a pipe is still unread, so the second wait expired too and Bambuddy concluded the process was stuck when it had already gone. Measured at 4.00s per close before, ~0.15s after. Fix. Both pipes are now drained while the process is being stopped, which makes the polite shutdown effective and the exit observable. The forced-kill path and its time limit remain as backstops, so a genuinely wedged process still can't hang a stream, a Stop request, or the cleanup task. This also corrects the conclusion recorded for #2580: that 12-hour hang was the unbounded form of this same self-inflicted stall rather than a stuck ffmpeg, so bounding the wait had capped the symptom without removing the cause. Covered by tests that drive a real subprocess — the fault lives in Python's pipe bookkeeping, so a stand-in object would pass against the broken code — including one that verifies a process ignoring the polite signal still has its forced exit observed rather than abandoned. - Two snapshots taken at the same moment opened two competing camera connections (#2705, reporter gzimbric) — Bambu firmware allows exactly one camera connection at a time. Bambuddy already knew this: a snapshot taken while somebody is watching the live view reuses the viewer's frame instead of opening a second socket. What nothing covered was two snapshots overlapping with no viewer attached at all — an Obico poll and a printer-wall refresh landing 200 ms apart, each correctly concluding it wasn't competing with a viewer, and then colliding with each other. On the reporter's P2S this knocked over the live stream that was feeding the camera wall, which was then reaped for having received no frames for 58 seconds. Eight paths take one-shot frames independently — Obico polling,
/camera/snapshot, the finish-photo capture and its disk-writing sibling, plate detection, the camera connection test, and the diagnostic — so any pair of them could overlap, and a shorter Obico interval widened the window. Fix. Simultaneous captures for the same printer now share one connection: the first opens it, everyone arriving while it is in flight gets the same frame. Every consumer here wants "a recent frame" rather than a frame stamped at its own microsecond, so identical bytes are the right answer. This shares captures, it does not cache them — a request arriving after the previous capture finished still takes a fresh frame, because plate detection and the finish photo judge a running print from these images and a stale frame there is worse than a slow one. Each caller keeps its own deadline (they range from 10 to 30 seconds) rather than inheriting whichever one happened to open the connection, giving up alone leaves the capture running for whoever else is waiting on it, and a capture that fails doesn't hand its failure to callers that never got an attempt of their own — they retry, which by then competes with nothing. One visible consequence: when the Diagnose tool shares a capture this way its frame-capture stage is labelledcoalesced_capture, because the pass is real but the timing shown is mostly time spent waiting, and a diagnostic must not report on a connection it never opened. Wiki updated. Covered by tests for the reported collision, the five-callers-one-connection case the reporter verified on live hardware, per-printer isolation, staying coalescing rather than becoming a cache, registry cleanup, a failed capture not poisoning its followers, bounded retry, a follower abandoning its wait without sabotaging the capture, and cancellation from either side. - Auto-matched filament showed a green tick when the colour was plainly wrong (#2687, reporter pchulpjoost) — The Filament Mapping panel reported a slot as matched, with the header reading (Ready), while the swatch beside it showed the slice wanted dark red and the tray it had picked held Dark Green. Manually selecting that very same tray from the dropdown correctly reported the colour mismatch, which is what made the disagreement so visible. Root cause. Auto-match ranks candidate trays by filament preset ID (
tray_info_idx) first, and when exactly one loaded tray carried the preset the slice asked for, that tray was accepted as a definitive match on the assumption "same preset means same spool, so the colour must agree too". The preset ID names the variant, not the spool —GFA00is PLA Basic,GFA01PLA Matte,GFA17PLA Translucent, in every colour Bambu sells it. So a user with one Matte spool loaded matched every Matte requirement regardless of colour, and the colour comparison was never reached. This is why the report came in for PLA Matte in particular: generic PLA Basic is usually loaded several times over, which sent the match down a different path that did compare colours correctly. Fix. The colour verdict is now taken from the tray that was actually selected, never from which rule selected it, and the automatic and manual paths share one comparison so they cannot drift apart again. The preset still decides selection, because the Basic/Matte/Silk distinction matters (#2650) — a wrong-coloured tray of the right variant is still chosen, but it is now reported as an amber Color mismatch instead of a green tick, and you can print anyway or pick another slot. A near-enough shade still counts as a match, and a 3MF that specifies no colour for a slot is satisfied by any colour rather than being flagged. Dispatch behaviour is unchanged: Force color match already required an exact colour before sending a job, so nothing was ever printed in the wrong colour because of this — the panel was simply telling you it was fine when it wasn't. Frontend-only. Wiki updated. Covered by tests for the unique-preset wrong-colour case, agreement between the auto and manual verdicts, the near-shade and colourless-requirement cases, and the multi-preset path that already worked. - P1-series archives kept the worse finish photo when the timelapse arrived late (#2704 follow-up) — When a print records a timelapse, Bambuddy prefers the video's last frame as the finish photo: the firmware stops recording after the toolhead parks but before the end G-code drops the bed, so it frames the finished print properly, where a live camera grab at that moment catches an already-lowered plate. Bambuddy waited 60 seconds for the video and then gave up, because the print-complete notification is waiting on that photo and holding a notification for minutes is worse than sending it with the live grab. On P1-series printers the video usually arrives later than that — they write MJPEG AVI instead of H.264 MP4 and serve it slowly, so across the support bundles their median was 33 seconds but the 90th percentile was 167 and the slowest observed was 546; every other model finished inside 26 seconds. The result was that the printers most in need of the better photo were the ones that never got it. Fix. The notification still goes out on the same 60-second bound with the live grab, so nothing gets slower. If the video was still on its way when that bound expired, Bambuddy now keeps waiting in the background and adds the extracted frame to the archive when it lands, at the front of the photo list so opening the gallery shows it first. The live grab is kept rather than replaced — the notification that already went out links to that exact file, and removing it would leave a broken image in Discord or Telegram. Covered by tests for the ordering, the longer budget, idempotency and the cases where the video never arrives.
- Timelapses that never got attached, and a Scan button that could not find them (#2704) — Timelapse was on for the print, the video never arrived in the archive, and pressing Scan for Timelapse afterwards turned up nothing. Measured across 247 support bundles, this was not rare: of 457 automatic scans only 262 ever attached a video. Root cause, part one. The scan looked four times, at 5, 10, 20 and 30 seconds, then stopped. The printer writes the video only after the print ends and a long print makes a large file, so it often arrived after the last look — the attempt that found the video was the first one 272 times and then 17 / 13 / 13, a flat tail against the cutoff rather than a decaying one. What ran after those four attempts was a fallback that searched for the print's name inside the video filename; Bambu firmware only ever writes
video_<timestamp>, so in 247 bundles it fired 159 times and matched exactly zero. Root cause, part two. The manual Scan button had no such snapshot to work from and matched by filename timestamp, by FTP modification time, or by there being exactly one video on the printer — all of which read a clock the printer cannot set, because a printer in LAN Only mode never reaches Bambu's time server. The reporter's P1S was six and a half days out, which defeats every one of those. Fix. The automatic scan now polls for several minutes instead of giving up after about a minute, and the name-match fallback is gone. The list of videos present when the print started is saved with the archive, so the comparison survives a Bambuddy restart mid-print and the manual Scan button can use it too — same clock-independent comparison, no timestamps anywhere. When a previous print's video lands late and two files look new, the one already attached to another archive is ruled out by name rather than by picking whichever the printer listed first, which could attach the wrong video. Bambuddy now deletes a timelapse from the printer once it has been archived, which keeps the printer's folder down to unclaimed videos and stops P1-series cards filling up with AVIs; your copy is in the archive, where you can watch, edit, download or remove it. That delete only happens after the transfer has been checked against the size the printer reported — which also fixes a silent truncation: an FTPS transfer that ended early produced a partial video that was attached as though it were complete. Because the first look happens seconds after the print ends — while the printer may still be writing the video — the file is also re-checked afterwards and only accepted once it has stopped growing, so a partial video is never mistaken for a finished one and the printer's copy is never removed on the strength of one. Wiki updated. Covered by tests for candidate selection, the download check gating the delete, the poll bounds, baseline persistence and the manual scan. - A printer that refuses Bambuddy's access code now says so, instead of reconnecting silently forever (#2698, reporter djepsylon) — A printer whose access code or serial was wrong produced no explanation anywhere: the connection attempt was refused, and the only trace was a warning every 30 seconds reading
MQTT disconnected: rc=Unspecified error— the same line you get from a printer that is simply switched off. The failure branch of the MQTT connect callback set "not connected" and discarded the reason code the printer had just sent, so the one piece of evidence that would have named the cause never reached the log, the support bundle, or the UI. In the report behind this fix, one of three printers had been in that loop for the entire capture and nothing said why. Fix. A refused connection is now logged with the printer's own reason ("Not authorized", "Bad user name or password") and, for those two, the remedy — the access code is regenerated every time LAN Only or Developer Mode is toggled, so it has to be re-read from the printer's screen. The reason is kept on the connection, so the Connection Diagnostic's Printer credentials check now states plainly that the printer refused the credentials when that is what happened, and falls back to hedged wording when all Bambuddy knows is that there is no session — previously it asserted "the access code is most likely wrong" even for a printer that was merely rebooting or already at its connection limit. The same reason is returned by the pre-add connection test. The access code itself is never written to the log. Translated in all locales; wiki updated. Covered by tests for both refusal codes, the clearing of the reason on a successful reconnect, the diagnostic's reason plumbing, and the two UI variants. - A2L AMS filament showed as "?" in Bambu Studio through the Virtual Printer, and manual filament picks reverted (#2697, reporter qoatzelcoat) — Every slot of the A2L's AMS Lite rendered as an empty question mark in the slicer's Device tab while Bambuddy's own AMS card showed type, colour and spool correctly; setting a filament by hand in Studio held for a second and then snapped back to "?". Root cause. The A2L reports its AMS Lite as physical unit id 16, but packs the slots' presence bits at bit base 24 — so Bambuddy normalises the id to 6 at the MQTT ingest boundary and every internal reader gets the right bits. The Virtual Printer's bridge, however, parses the printer's raw payload itself (by design — the slicer-facing cache has to keep the physical ids, since Bambu Studio addresses the Lite as 16) and so still held id 16 when it ran the shared empty-slot cleanup. That cleanup read bits 64-67, where nothing is ever set, concluded all four slots were empty and wiped
tray_type,tray_color,tray_info_idxand the RFID fields from the copy sent to the slicer — once per second, which is also why a manual pick could not survive. Fix. The presence-bit helper now folds the physical id 16 onto the same bit base as the normalised 6, so it computes bits 24-27 whichever id reaches it; the cached ids the slicer sees are left untouched. Only the A2L was affected — every other AMS type already reached the helper with an id whose bit base was correct, and Bambuddy's own printer card was correct throughout. Confirmed against the reporter's debug log, which shows the cleanup clearing slots at bits 64-67. Covered by tests pinning the bit base for both ids and a bridge-level regression test built from the reporter's capture. - Bambu Cloud sign-in with a TOTP (authenticator app) account always failed with "Invalid code" (#2696, reporter cmerkle) — Every TOTP verification was rejected regardless of the code. Bambu Lab added double-submit CSRF protection to the
bambulab.comweb origin, which is where — and only where — Bambuddy posts the two-factor code; the endpoint refused the request with403 CSRF error: missing_cookiebefore evaluating the code at all, and Bambuddy surfaced that as "Invalid code". Reproduced against the live endpoint with a deliberately invalid key: a bare POST returnsmissing_cookie,GET /api/csrfmints abbl_csrf_tokencookie, a POST carrying only that cookie returnsmissing_header, and a POST carrying the cookie plus anx-bbl-csrf-tokenheader reaches application logic. Bambuddy now performs that handshake before submitting the code. Note that landing on the sign-in page first — the intuitive fix — does not work: that page sets only Cloudflare's__cf_bm. Also fixed: a CSRF refusal no longer masquerades as a wrong code; it now says the code was never checked, so nobody else loses an evening to clock drift and leading-zero theories. Only TOTP sign-ins were affected — every other cloud call, including the email-code two-factor path, goes toapi.bambulab.com, which is not gated, and existing stored tokens kept working throughout. Covered by tests that pin the exact header name and the origin used per region. - Spools added as stock could not be edited or duplicated without assigning a slicer preset, and that preset then overwrote the spool's own manufacturer (#1905, reporter rocstat1979) — A spool created via Quick Add, a CSV import or an RFID scan has no slicer preset, brand or subtype. Reopening it in Edit Spool demanded all three before anything could be saved — so changing the storage location, the cost or the note was impossible — and Copy Spool had the same gate with no Quick Add toggle to waive it. Worse, the preset you were forced to pick auto-filled material, brand and subtype from the preset name, silently rewriting a hand-entered manufacturer ("Elegoo" → "Generic") so the spool no longer appeared where it had been filed. Fix. Editing and copying now require only what the backend requires — the material; the preset, brand and subtype fields stay fully visible and editable (nothing is hidden the way Quick Add hides it), and the required-field markers no longer advertise a rule that isn't enforced. Selecting a preset now fills only fields that are still empty or that a previously selected preset had filled, so values you (or the saved spool) provided survive; switching between presets still replaces what the earlier one contributed. Also fixed: the brand and material dropdowns no longer filter themselves down to the brand/material pairs known to the color catalog and slicer presets — a real combination like Elegoo ASA looked impossible to enter because Elegoo was catalogued only for PLA. Both lists now always offer everything known, with the catalog-paired entries ranked first under a Suggested heading and the rest under All, and a spool's own custom brand or material is always present in its dropdown. The same change applies to the SpoolBuddy write-tag form, which shares these fields. Lastly, the Quick Add layout no longer leaks out of create mode: quick-adding a spool and then opening Edit used to strand the edit form in the reduced layout with no toggle to leave it. Frontend-only. Translated in all locales; wiki updated. Covered by validation and form-interaction tests.
- The spool PA-Profil (Pressure Advance) picker only ever offered the 0.4mm K-profile, hiding nozzle-specific profiles for the same filament on multi-nozzle printers (#2618) — When a printer had two K-profiles for one filament differing only in nozzle size (e.g. PAHT-CF at 0.4mm K=0.042 and 0.6mm K=0.028), the Edit Spool → PA-Profil tab (and the SpoolBuddy write-tag page, which shares the picker) showed only the 0.4mm entry ("1 match, K=0.042"), regardless of the nozzle actually installed. Root cause. Both surfaces fetched a printer's calibrations with
getKProfiles(printer.id), which defaults the nozzle filter to0.4— and the printer/MQTT layer filters strictly by that diameter, so the 0.6mm profile was never retrieved. (The AMS-Slot config dialog was already fixed for this in #1899; these two pickers were not.) Fix. The picker now queries every nozzle the printer reports installed (0.4,0.6, …) and merges the results, falling back to0.4only when the printer hasn't reported its nozzle hardware. Each profile row now also shows a nozzle-diameter badge so two identically-named profiles are distinguishable. Frontend-only. Covered by tests for the nozzle enumeration and the two-profile rendering. - Print-archive backups to a Gitea or Forgejo instance hosted under a URL path prefix could not be configured — the repository URL failed to parse (#2642, reporter M1ndHunteR) — Self-hosted Gitea/Forgejo is often served under a subpath (
ROOT_URLlikehttps://host/gitea), so repositories live athttps://host/gitea/owner/reporather than at the host root. Root cause. The Gitea backend (shared by Forgejo) assumed the repo sat directly under the host: URL parsing required exactly two path segments after the hostname, so a subpath URL's three segments (gitea/owner/repo) matched nothing and raised "Cannot parse repository URL". Even had it parsed, the API base was derived from scheme+host only, yieldinghttps://host/api/v1instead ofhttps://host/gitea/api/v1, so every API call would have 404'd. Fix. The Gitea/Forgejo backend now treats the final two path segments asowner/repoand keeps any leading segments as a base-path prefix, deriving the API base as{scheme}://{host}{prefix}/api/v1. Root-hosted instances are unaffected (empty prefix). GitHub/GitLab are untouched. Covered by parse and API-base tests for both providers. - The Print Queue's History tab showed a count of all prints but only ever displayed the first 50, with no way to reach the rest (#2682, reporter pchulpjoost) — The History header read e.g.
History (311 items), but only 50 rows rendered and there was no "load more" control, so 261 finished prints were unreachable. Root cause. The full history is already loaded client-side (the queue endpoint has no limit) and sorted correctly — the header counts the whole list — but the row builder hard-sliced it toitems.slice(0, 50), a fixed cap with no accompanying control. Nothing was missing server-side; it simply wasn't drawn. Fix. History now paginates: it draws the 50 most-recent prints and, when there are more, shows a Show more button (with aShowing X of Ycount) that loads the next 50, repeating until the whole history is on screen. The page size resets to the first page only when you re-sort or change the location filter — deliberately not on the periodic queue poll, so an expanded view doesn't collapse mid-scroll. Frontend-only; batch grouping and per-row actions are unchanged. Covered by a test asserting the 50-row cap, theShowing 50 of 60count, and that Show more reveals the remainder. Wiki updated. - LDAP Distinguished Names weren't redacted from the support bundle / bug report (#2681, reporter MaxBareiss) — With LDAP auth in use, the debug log carried lines like
LDAP authentication successful for user: … (DN: CN=Joe Schmoe,CN=Users,DC=ad,DC=example,DC=com, …). A DN's leafCNis the user's real name — PII on par with the email address Bambuddy already redacts — and it passed straight through into an uploaded support bundle. Fix. The log sanitizer (used by both the support bundle and the in-app bug report) now redacts LDAP DNs to[DN]wherever they appear — the auth line, ldap3 exception strings, and group DNs alike — matching a run ofattr=valueRDN components (CN/OU/DC/UID/…) so ordinarykey=valuelog text isn't affected. As primary hygiene the LDAP service also no longer logs the raw DN on successful auth (the username plus group count is enough). Covered by tests, including the exact reported line and non-DNkey=valuelines that must be left intact. Redaction list on the Bug Report wiki page updated. - An external USB camera could stay locked (LED stuck on) after closing the live view, blocking reopen (#2675, reporter bitbarista) — Closing an external USB (V4L2) camera's live view abruptly — tab/popup closed, or a dropped connection — could leave the backend's
ffmpegprocess running and holding/dev/videoNopen. The camera LED stayed lit and the next attempt to open the view (or click Test) failed or took 10-30+ seconds while the newffmpegfought for exclusive device access. Root cause. This is the same class of leak as #776 (fixed for the built-in RTSP path), but the external/USB path was never wired into that fix. #776 added the_active_streams/_disconnect_events/ spawned-PID registries so both the/camera/stopendpoint and the periodic orphan janitor could find and kill leaked ffmpeg — but external streams registered into none of them, so for USB cameras both were structurally blind:/camera/stopreturned{"stopped": 0}even while a stream was genuinely running, and the janitor's/procnet matched onlyrtsp(s)://bblp:cmdlines, never a USBffmpeg. Cleanup ran only via the stream generator's ownfinally, which an abrupt disconnect can skip. Fix. External USB (and external-RTSP) streams now register theirffmpegprocess into the same registries the built-in path uses, so/camera/stopterminates them promptly (now{"stopped": 1}) and the janitor reaps any that leak within its cleanup interval. The/procsafety-net scan also now recognises USB (-f v4l2)ffmpeg, so orphans surviving an app restart are caught too; a leaked process that hangs on a still-locked device (rather than exiting) is registered before the startup probe so it can still be killed. Covered by tests: the stream hands its process to the registry, the stop endpoint and janitor both reap a registered external stream, and the/procscan matchesv4l2while ignoring unrelatedffmpeg. Thanks to bitbarista for the precise diagnosis. (Reported alongside a working fix; implemented here.) - A broken slicer sidecar silently produced tiny corrupt files that were queued and printed anyway, and a reverse-proxy 413 wasn't self-explanatory (#2671, reporter Austinzveare) — With the slicer-API sidecar behind a reverse proxy, slicing produced ~28-byte files that "did nothing" (and could still be sent to the printer), while a separate proxy attempt failed with a bare 413 Request Entity Too Large that the recommended nginx fix didn't seem to resolve. Root cause. Bambuddy's slice client only validated the sidecar's HTTP status, not its body. When the sidecar — or a proxy in front of it — returned
200 OKwith a body that wasn't a real 3MF (a stock/misconfigured sidecar, a proxy error page, a truncated response, or an OrcaSlicer/Bambu Studio CLI crash that emitted no output), Bambuddy wrote that tiny blob straight to a.gcode.3mf, stored it as a valid sliced file (the 3MF-parse failure was swallowed as merely "no thumbnail"), and let it be queued and FTP'd to the printer. Separately, a genuine 413 comes from the reverse proxy in front of the sidecar rejecting the multi-MB upload (model + profiles), not from the slicer — so raising the body limit on the wrong proxy layer had no effect. Fix. The slice client now validates the sidecar's output: when a 3MF export was requested, the response body must be a real ZIP (3MF container) or the job fails loudly with an actionable message ("…the body is not a valid 3MF (N bytes) — check the sidecar URL and any proxy in front of it") instead of persisting a corrupt file. A 413 now yields a targeted message naming the fix — raiseclient_max_body_size(or equivalent) on the proxy directly in front of the sidecar. Covered by tests: a 200 with a non-3MF body raises a server error (both the profile and embedded-settings paths), a 413 surfaces the reverse-proxy guidance, a valid 3MF still slices, and raw-gcode preview output is not zip-validated. Wiki troubleshooting updated with both scenarios. - File Manager "sort by recent activity" didn't match
ls -t, and there was no way to see a file's modified date (#2680 / #1770 follow-up, reporter Kingbuzz0) — For external (mapped/NAS) folders the folder tree's activity sort and the file pane's date sort put things in a seemingly random order — some entries roughly right, most not — instead of the real newest-first order shown byls -tor Windows Explorer. Root cause. Nothing captured the files' actual on-disk modification time. The sort keyed off Bambuddy's own databaseupdated_at/created_attimestamps, which for a bulk external scan are all the same instant (the scan time), so a whole block of files tied and sorted arbitrarily; only the few rows Bambuddy had later touched individually looked "partially correct." The folder tree also only bubbled up immediate child-file activity, so a file added deep in a subtree never lifted its parent folders. Fix. External scans now record each file's and each directory's real filesystem mtime (os.stat().st_mtime), refreshing it on every re-scan so a file edited over the mount re-sorts correctly. The folder tree's "recent activity" is now a recursive newest-descendant roll-up — a freshly-added file anywhere inside a folder lifts every ancestor — and both the tree sort and the file pane's date sort use the real mtime (falling back tocreated_atfor managed uploads that have none). A new toolbar toggle shows/hides each item's last-modified date in the right-hand pane (grid and list views), and the same toggle puts a date on every row of the folder tree as well, nested folders included. Folders show last activity rather than "last modified" — the newest timestamp among the folder itself, its files and everything below it, which is exactly the value the tree sorts on — so a folder holding a file you touched an hour ago reads as an hour old even though its own directory mtime is older. Folders with no activity at all show no date instead of a placeholder. Existing external folders backfill their mtimes on the next scan. Covered by tests: scan captures real file/folder mtimes, a re-scan refreshes a changed file, and a deep file bubbles its subtree's root ahead of a sibling with only a middle-aged file. - An AMS-HT slot kept showing the removed filament and never cleared (#2670, reporter needo37) — After the #2594 fix, every empty-slot clearing path skipped AMS-HT units, so once a spool was removed the HT slot on the printer card stayed stuck on the old filament (Bambu Studio correctly showed it as Empty). The root cause was the HT's presence signal: firmware reports it as a single consecutive bit in
tray_exist_bitsat16 + (ams_id − 128)(HT-A = bit 16, HT-B = bit 17, …), not the regularams_id × 4position — so the bitmask cleanup skipped the HT entirely, and the HT'sstatefield is firmware-variant and can't be used instead. Confirmed against a live H2D capture (loaded HT reports the bit set, empty reports it clear) and cross-checked with the OrcaSlicer reference. Fix. The bitmask cleanup now understands the HT's real bit position and clears an empty HT slot the same way it clears a regular one, using firmware's own authoritative presence bit — so a loaded HT is never wrongly cleared (its bit stays set, keeping the #2594 fix intact). The AMS change detection now hashes the merged state, so a removal signalled only by the bitmask still unbinds the slot's spool assignment; and the websocket status now carries the presence bit so the card renders "Empty" (not "?") consistently. Verified for both single- and dual-HT setups. - The print dialog clipped the per-filament gram usage when the material name was long, especially on mobile (#2669, reporter apizz) — In the Print dialog's Filament Mapping, each required filament shows its name and the grams the job needs, e.g.
Bambu PLA Basic (281.2g). The name and the gram figure lived in a single fixed-width column that truncated as one unit, so a long name (e.g.Polymaker PLA Matte) pushed the(…g)off the end and cut it off — partially on a wide screen, entirely in mobile portrait. The gram usage is the more important number here (it's what tells you whether a spool has enough left), so hiding it was the wrong thing to drop. Fix. The gram usage is now pinned and never shrinks or truncates; only the material name truncates (with the full name on hover), so the(…g)stays fully visible at every width. Applied to both the Specific-Printer and "Any [model]" mapping panels. Frontend-only, no behaviour change beyond layout. Covered by a test asserting the gram figure renders in its own non-truncating element separate from the truncating name. - A printer's nozzle size got overwritten to the wrong value (often 0.8mm), then blocked prints as a nozzle mismatch (#2663, reporter huykent) — A1 printers with a 0.4mm nozzle intermittently showed 0.8mm (or no size at all) on the dashboard, and since 1.2.5 that wrong value made the nozzle-mismatch guard (#1899) refuse to dispatch the job — "File sliced for a 0.4mm nozzle, but the printer has 0.8mm installed." It was intermittent and could flip after a job was sent. Root cause. Bambuddy fetches K-profiles by probing every nozzle size in turn — it sends an
extrusion_cali_getrequest for 0.2, 0.4, 0.6 and 0.8mm. The printer's response to each echoes the requested nozzle diameter at the top level, and the MQTT handler passed everyprintmessage — including these K-profile responses — through_update_state, which treats a top-levelnozzle_diameteras the installed hardware. So the last size probed (0.8) clobbered the real nozzle size in memory; a later genuine status push would correct it, and the next K-profile fetch would break it again, which is why it flickered and "changed after the job was sent." The raw MQTT status always reported the correct 0.4 — only the derived hardware-nozzle field was corrupted. Fix.extrusion_cali_getresponses are now handled only by the K-profile parser and no longer fed to_update_state, so they can't touch the nozzle hardware state — mirroring the existing guard that already stopsget_accessoriesresponses from doing the same thing. The installed nozzle size now comes solely from the printer's real status push, where it was always correct. No configuration or migration needed: the value lives in memory and self-corrects on the next status push after updating. Covered by tests: a 0.8mm K-profile response leaves a 0.4mm nozzle untouched, the response's profiles are still parsed intostate.kprofiles, and a genuine status push still sets (and corrects) the nozzle. - The print queue couldn't be reordered on a phone, and the reorder controls were invisible in portrait (#2667, reporter aporlebeke) — On mobile there was no way to reorder the queue: in portrait the reorder controls simply weren't visible, and even in landscape (where the desktop drag handle appears) touch-dragging didn't move anything. Root cause. The drag grip and selection checkbox on every pending row are
hidden sm:flex, so below the 640px breakpoint (phone portrait) they disappear entirely — there's no affordance to grab. Above it (landscape phone/tablet) the grip shows, but it carriedtouch-action: manipulationand the only drag sensor is dnd-kit'sPointerSensorwith an 8px activation distance, so on touch the browser claimed the vertical gesture as a scroll before the drag ever started. The whole reorder mechanism was effectively mouse-only. Fix. Pending rows now get tap-friendly up/down arrow buttons on mobile (the "arrow select" the reporter asked for), shown belowsmwhere the drag handle is hidden. They move a row one step among its siblings — standalone items, whole batches, and items within a batch, in both the flat and per-printer layouts — and persist through the samePOST /queue/reorderpath as drag, so arrows and drag agree. Arrows appear only in the manual "position" sort (with shortest-job-first off), where a position actually has meaning, and are gated on the samequeue:reorderpermission; the up arrow on the first row and the down arrow on the last are shown disabled. Separately, the desktop drag handle'stouch-actionis nownone, so mouse-style drag also works on touch (landscape phones, tablets). Reuses the existingqueue.moveUp/queue.moveDowntranslations (already present in all locales). Covered by tests: the controls render for pending items, moving the first item down persists the swapped order, and the boundary arrows are disabled. - 3D Preview plate thumbnails were broken (401) in File Manager when login was enabled (#2661, reporter fbordonaro) — Opening a multi-plate 3MF via File Manager → 3D Preview showed broken-image icons for every plate thumbnail, and the network tab showed
GET /api/v1/library/files/<id>/plate-thumbnail/<plate>returning 401 "Valid camera stream token required." The Slice dialog displayed the same file's thumbnails correctly, which is what made it look inconsistent. Root cause. The plate-thumbnail endpoints (both archive and library) are gated behind a camera stream token passed as a?token=query param, because an<img>tag can't send anAuthorization: Bearerheader. Every place that renders these thumbnails is supposed to append the token via thewithStreamToken()helper —PlatePickerModal(the Slice dialog's multi-plate picker) and the Print modal'sPlateSelectorboth do — but the 3D Preview dialog (ModelViewerModal) rendered the rawthumbnail_urlwith no token, so with auth enabled the browser fetched without one and got a 401. Fix.ModelViewerModalnow wraps the plate thumbnailsrcinwithStreamToken(), matching the two existing call sites. The token is already synced app-wide (the same global the working pickers read), andwithStreamToken()is a no-op when auth is off, so nothing changes for non-auth setups. Covered by a component test asserting the plate thumbnail<img>carries the?token=query param. - Force color match dispatched a print onto the wrong PLA variant — Matte jobs went to Basic and Silk printers alike, and the wrong AMS slot on a printer holding two same-colour variants (#2650, reporter MartinNYHC) — With Force color match on, a job sliced for White PLA Matte was dispatched to every printer that had any white PLA loaded — the ones holding White PLA Basic and White PLA Silk+ included — so a matte model came out glossy on the wrong machine. Root cause. Bambu's MQTT status reports every PLA sub-variant as
tray_type == "PLA"; the Basic/Matte/Silk distinction is carried only intray_info_idx(GFA00= Basic,GFA01= Matte,GFA06= Silk, …), which the 3MF'sslice_info.configalso records per filament. Three places dropped it: the Virtual-Printer queue built each force override as{slot_id, type, color, force_color_match}without the parsedtray_info_idx; the scheduler's eligibility check (_get_missing_force_color_slots) compared loaded trays on(type, colour)only — so(PLA, #FFFFFF)matched Basic, Matte and Silk indiscriminately and all three printers looked eligible; and the AMS slot mapper clearedtray_info_idxwhen applying the override, so even on the correct printer it could pick a different-variant tray of the same colour. Fix. The force override now carries the 3MF'stray_info_idx; a slot counts as satisfied only when a loaded tray matches type and colour and the variant (identicaltray_info_idx, or either side lacks one); and the slot mapper now keeps the variant for force-colour overrides so it pins the matching tray. A blank idx on either side (custom/third-party spools report none, and older 3MFs carry none) falls back to the historical type+colour behaviour, so those setups are unaffected, and a manual filament swap (a preference override) still clears the idx so it matches the swapped-in spool rather than the old one. A job sliced for GFA01 now goes only to a printer with GFA01 loaded, and lands on that printer's GFA01 tray. The printer-card queue-compatibility hint (which printers show a pending job as runnable) now applies the same variant rule. Covered by scheduler tests (Matte requirement rejects Basic/Silk, accepts Matte, blank loaded idx falls back, requirement without an idx unchanged; the mapper pins the GFA01 tray over a same-colour GFA00 on both the 3MF and no-3MF paths; a preference swap still matches by colour), a Virtual-Printer test asserting the override carriestray_info_idx, and frontend tests for the variant-aware queue hint (rejects other variants, accepts the match, blank-idx and no-variant-data fall back).
Security
- Patched two build-time frontend dependencies flagged by
npm audit(GHSA-r28c-9q8g-f849, GHSA-mh99-v99m-4gvg) —postcss8.5.15 → 8.5.23 fixes a path traversal in its source-map auto-loader (sourceMappingURL) that could disclose arbitrary.mapfiles, andbrace-expansion(pulled in transitively byeslintviaminimatch) is bumped through the existingoverridesblock (^5.0.7→^5.0.8) for a denial-of-service via unbounded expansion. Both are build/lint-time tooling only — neither is part of the shipped app, so no running Bambuddy install was exposed.postcssmoved within its existing range;brace-expansionneeded the pin becausenpm audit fixcan't lifteslintto the patched transitive on its own. - Pinned
react-routerto its most-patched 7.x (7.18.1) and documented the one remaining, unreachable advisory (GHSA-qwww-vcr4-c8h2) — Staying current on the 7.x line matters: 7.18.1 clears 14 advisories that older 7.x releases carry, several reachable in a browser SPA (open-redirect XSS in<Link>/useNavigate, route-matching DoS). The single advisory that still flags 7.18.1 — a CSRF bypass — applies only to React Router's RSC mode, which requires the server runtime (react-router/server, not installed); Bambuddy is a Vite SPA usingBrowserRouter, so the vulnerable path is unreachable. There is no non-major fix (the patch landed only in the 8.3.0 major, andreact-router-domhas no 8.x — adopting it would mean migrating every import toreact-routerplus a React peer bump), soreact-router/react-router-domare pinned to 7.18.1 and the finding is carried as a documented, fail-closed exception in the CI audit gate: a different react-router advisory still fails CI, and the exemption is dropped automatically the moment a non-major fix ships.npm audit fix --forceis deliberately avoided — its suggested "fix" is a downgrade to 7.11.0, which reintroduces those 14 advisories.