Note
This is a daily beta build (2026-07-27). 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.
Fixed
- 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). 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.