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

pre-release5 hours ago

Note

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

  • 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_id stamp 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 with library:delete_own could create folders and delete their own files, but the emptied folder sat there until an admin removed it. Users with library:delete_own can 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 require library: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-status endpoint 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.app relay 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 nested data object — the same place HA automations put mobile push options like priority, ttl, channel, and group. ttl: 0 + priority: high make Android pushes arrive immediately instead of batched, and channel gives 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/slim stats feed now carries each run's energy_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 an awaiting_plate_clear field, and every transition is additionally published on a new retained topic bambuddy/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 at false. 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 existing POST /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.

Fixed

  • 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.com web origin, which is where — and only where — Bambuddy posts the two-factor code; the endpoint refused the request with 403 CSRF error: missing_cookie before 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 returns missing_cookie, GET /api/csrf mints a bbl_csrf_token cookie, a POST carrying only that cookie returns missing_header, and a POST carrying the cookie plus an x-bbl-csrf-token header 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 to api.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 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.