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

pre-releaseone hour ago

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 to 0.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 to 0.4 only 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_URL like https://host/gitea), so repositories live at https://host/gitea/owner/repo rather 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, yielding https://host/api/v1 instead of https://host/gitea/api/v1, so every API call would have 404'd. Fix. The Gitea/Forgejo backend now treats the final two path segments as owner/repo and 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 to items.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 a Showing X of Y count) 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, the Showing 50 of 60 count, 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 leaf CN is 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 of attr=value RDN components (CN/OU/DC/UID/…) so ordinary key=value log 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-DN key=value lines 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 ffmpeg process running and holding /dev/videoN open. The camera LED stayed lit and the next attempt to open the view (or click Test) failed or took 10-30+ seconds while the new ffmpeg fought 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/stop endpoint 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/stop returned {"stopped": 0} even while a stream was genuinely running, and the janitor's /proc net matched only rtsp(s)://bblp: cmdlines, never a USB ffmpeg. Cleanup ran only via the stream generator's own finally, which an abrupt disconnect can skip. Fix. External USB (and external-RTSP) streams now register their ffmpeg process into the same registries the built-in path uses, so /camera/stop terminates them promptly (now {"stopped": 1}) and the janitor reaps any that leak within its cleanup interval. The /proc safety-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 /proc scan matches v4l2 while ignoring unrelated ffmpeg. 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 OK with 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 — raise client_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 by ls -t or Windows Explorer. Root cause. Nothing captured the files' actual on-disk modification time. The sort keyed off Bambuddy's own database updated_at/created_at timestamps, 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 to created_at for 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_bits at 16 + (ams_id − 128) (HT-A = bit 16, HT-B = bit 17, …), not the regular ams_id × 4 position — so the bitmask cleanup skipped the HT entirely, and the HT's state field 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_get request 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 every print message — including these K-profile responses — through _update_state, which treats a top-level nozzle_diameter as 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_get responses 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 stops get_accessories responses 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 into state.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 carried touch-action: manipulation and the only drag sensor is dnd-kit's PointerSensor with 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 below sm where 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 same POST /queue/reorder path 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 same queue:reorder permission; the up arrow on the first row and the down arrow on the last are shown disabled. Separately, the desktop drag handle's touch-action is now none, so mouse-style drag also works on touch (landscape phones, tablets). Reuses the existing queue.moveUp / queue.moveDown translations (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 an Authorization: Bearer header. Every place that renders these thumbnails is supposed to append the token via the withStreamToken() helper — PlatePickerModal (the Slice dialog's multi-plate picker) and the Print modal's PlateSelector both do — but the 3D Preview dialog (ModelViewerModal) rendered the raw thumbnail_url with no token, so with auth enabled the browser fetched without one and got a 401. Fix. ModelViewerModal now wraps the plate thumbnail src in withStreamToken(), matching the two existing call sites. The token is already synced app-wide (the same global the working pickers read), and withStreamToken() 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 in tray_info_idx (GFA00 = Basic, GFA01 = Matte, GFA06 = Silk, …), which the 3MF's slice_info.config also 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 parsed tray_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 cleared tray_info_idx when 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's tray_info_idx; a slot counts as satisfied only when a loaded tray matches type and colour and the variant (identical tray_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 carries tray_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)postcss 8.5.15 → 8.5.23 fixes a path traversal in its source-map auto-loader (sourceMappingURL) that could disclose arbitrary .map files, and brace-expansion (pulled in transitively by eslint via minimatch) is bumped through the existing overrides block (^5.0.7^5.0.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. postcss moved within its existing range; brace-expansion needed the pin because npm audit fix can't lift eslint to the patched transitive on its own.
  • Pinned react-router to 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 using BrowserRouter, so the vulnerable path is unreachable. There is no non-major fix (the patch landed only in the 8.3.0 major, and react-router-dom has no 8.x — adopting it would mean migrating every import to react-router plus a React peer bump), so react-router/react-router-dom are 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 --force is deliberately avoided — its suggested "fix" is a downgrade to 7.11.0, which reintroduces those 14 advisories.

Don't miss a new bambuddy release

NewReleases is sending notifications on new releases.