Bambuddy 0.2.4.5
⚠ Upgrade Notes — Read Before Updating
0.2.4.5 is a substantial patch release on the same 0.2.4 code base — no schema breaks beyond the documented column additions for the new API-key scopes (auto-migrated on first start, dialect-branched for SQLite and Postgres), no Docker entrypoint changes, no Vite/proxy quirks. The in-app Apply Update button in Settings → System → Updates works for Docker and for any native install already on 0.2.4.1 or later.
Four behavior-change callouts to know about before you upgrade:
-
API-key permission model switched from denylist to allowlist (security fix). Two new scope flags were added —
can_manage_libraryandcan_manage_inventory— and existing keys that hadcan_queueset are auto-backfilled with both new scopes on first start so library upload and inventory write keep working for keys you'd previously created as "queue-only". A hardened "read-only" key (nocan_queue) does not silently gain new writes on upgrade. Review the Settings → API Keys page after upgrade if you run integrations against specific keys. -
External folder mounting is now disabled by default and requires explicit operator opt-in via
BAMBUDDY_EXTERNAL_ROOTS(security fix). If you currently use the File Manager → external folder feature (mounting host paths like a NAS share, USB drive, or/mnt/library), you must setBAMBUDDY_EXTERNAL_ROOTS=/path/one:/path/twoin your env /docker-compose.yml/ systemd unit before upgrading, or all mounted external folders will be rejected on next start. The variable takes a colon-separated allowlist of absolute paths; the mount route is also now gated on the adminSETTINGS_UPDATEscope rather thanLIBRARY_UPLOADbecause mounting host paths crosses user boundaries. Bambuddy-owned dirs (data dir, log dir, static dir, archive dir) are hardcode-rejected even if added to the allowlist. See wiki → Docker → External library folders for examples. -
Slicer-API sidecar users: bump
BAMBU_VERSIONinslicer-api/.envto02.07.01.57and rebuild. If you run the optional OrcaSlicer API sidecar for server-side slicing, the upstreambambu-studio-apiimage switched from the no-longer-published Fedora AppImage to the Ubuntu 22.04 AppImage. The new default inslicer-api/.env.example/slicer-api/docker-compose.ymlmatches the new build path. After pulling this release:cd slicer-api docker compose --profile bambu build --no-cache bambu-studio-api docker compose --profile bambu up -dBambuddy users who don't run the slicer-API sidecar are unaffected.
-
Virtual-printer FTP passive port range widened from 50000–50100 (101 ports) to 50000–51000 (1001 ports) for the non-proxy path. Docker bridge-mode users mapping the old range need to update to
50000-51000:50000-51000. Docker host-mode and bare-metal users are unaffected. Proxy-mode VPs keep the old range (they pre-bind the printer-side range exactly).
Make a backup before upgrading via Settings → Backup → Create Backup. Native install with update.sh snapshots the database automatically and rolls back on failure. Docker and fully-manual paths don't.
Docker
docker compose pull
docker compose up -d
docker-compose.yml doesn't need refreshing unless you map the VP passive FTP port range (see above) — no other entrypoint, volume, or env-var conventions changed since
0.2.4.
Native install — recommended path
sudo BRANCH=main /opt/bambuddy/install/update.sh
Snapshots the database first and rolls back on failure.
Native install — manual path
sudo systemctl stop bambuddy
cd /opt/bambuddy
sudo -u bambuddy git fetch --prune --tags --force origin
sudo -u bambuddy git checkout main
sudo -u bambuddy git reset --hard origin/main
sudo /opt/bambuddy/venv/bin/pip install -r requirements.txt
sudo systemctl start bambuddy
Windows install
0.2.4.5 ships Windows installer (#1529). Existing installs upgrade in place via the Service / Update entries on the installer; fresh installs use the new GUI installer end-to-end.
Highlights
0.2.4.5 is a security-led release. The big-ticket security work — API-key permission allowlist, path-traversal hardening across the upload / import / file-write surface with a fifth CI backstop, a WebSocket auth gate and audit-driven sweep, a vitest dev-dep bump, and a PyJWT floor bump for four upstream advisories — sits alongside a Windows-first installer (#1529), Turkish (#1571) and Korean (#1587) locales, OS-aware system theme detection (#1418), and the second wave of virtual-printer hardening (cipher-pin parity on every slicer-facing TLS context #1610, multi-slicer response routing, MQTT brute-force rate-limit, sticky-keys for seven more fields).
On the fix side, the biggest reports closed in this cycle: multi-plate archives now report filament / time / cost honestly (#1593), card timestamps render in the browser's local timezone instead of UTC (#1602), the multi-run accuracy badge is suppressed when it would be apples-to-oranges (#1608), the cross-distro TLS handshake regression on hardened-policy hosts is fixed at every VP TLS context (#1610), inventory and AMS handling tightened across transparent filament (#1545), profile-only mismatch popups (#1552), per-spool weight sync (#1530 / #1459), the SpoolBuddy tare banner (#1536), and the OIDC email claim auto-provisioning gap (#1569). VP _pending_files / temp-file leaks, queue-position assignment, DELETE orphan cleanup, sync-from-db race, and slicer-options cache bounds all landed as one slicer-surface audit bundle (#1558) plus follow-ups.
Security
-
API-key permission model rewritten as an allowlist (critical). The three documented API-key scopes ("Read Status", "Manage Queue", "Control Printer") were enforced only inside the legacy
/api/v1/webhook/*router; every other route fell through to a 17-entry admin denylist that granted any valid key access to every non-admin endpoint regardless of which scope flags were ticked. Fix: a new explicit mapping pins every non-admin permission to exactly one scope; unmapped permissions return 403 ("administrative operations") regardless of scope. Two new scope flags ship in the same release —can_manage_library(gates library upload / update / delete and MakerWorld import) andcan_manage_inventory(gates inventory create / update / delete and SpoolBuddy kiosk writes). Existing keys withcan_queueset are auto-backfilled with both new scopes; "read-only" keys don't gain new writes. A new CI test fails the build on any future permission added without a scope classification, so the surface can't silently grow again. See Upgrade Notes above for the migration story. -
WebSocket auth gate. The proactive WebSocket audit caught that
/api/v1/wswas broadcasting every printer-status / archive / inventory event to anyone who could reach the HTTP port. The endpoint is now auth-gated like the REST surface; events broadcast only to authenticated subscribers. -
Path-traversal hardening across the upload / import / file-write surface. A private report against
POST /api/v1/projects/import/filetraced two attacker-controlled strings being joined to the library directory with no resolve + containment check (linked_folders[*].namefrom the requestproject.jsonand per-entryzf.namelist()paths from the ZIP itself). Concrete escalation: drop a.pthinto the venv'ssite-packagesfor code execution on next restart, overwrite the JWT signing-secret file to forge an admin token, or overwrite~/.ssh/authorized_keyson native installs. Fix is structural: a newsafe_join_under(parent, *parts)helper resolves both sides and asserts containment; both vectors now route through it. Adjacent fixes in the same audit:GET /api/v1/archives/{id}/photos/{filename}had no traversal guard and was serving arbitrary paths;ArchiveService.attach_timelapseaccepted printer-FTP-listing-supplied filenames with..segments under the compromised-printer threat model. The audit sweep marked every Path-arithmetic site underbackend/app/api/routes/ANDbackend/app/services/with an explicit# SEC-PATH-OKannotation or routed it through the helper; a new CI test (the fifth security backstop) AST-walks both directories and fails the build on any future unsafe-shape join that's neither helper-routed nor marker-annotated. -
External folder mounting restructured to an opt-in allowlist + admin-only scope. External folder mounting (host paths like a NAS share or USB drive surfaced into Bambuddy's File Manager) was previously gated on
LIBRARY_UPLOAD(any non-admin user with that scope) and validated against a small denylist of system directories — meaning anything not on the denylist could be mounted, including the Bambuddy data directory containing other users' archives, the log directory, or arbitrary NFS / SMB mounts. The route now requires the adminSETTINGS_UPDATEscope and accepts only paths inside the newBAMBUDDY_EXTERNAL_ROOTSenv-var allowlist. The default empty allowlist disables the feature outright. Bambuddy-owned directories are hardcode-rejected even if the operator adds them to the allowlist. This is a breaking change for installs that currently use external folder mounting — see Upgrade Notes above for the restoration path. -
VP MQTT brute-force rate-limit. Bambuddy's virtual printer exposes an 8-character access code on the slicer-facing MQTT port; without rate-limiting it was brute-forceable by anyone who could reach the VP's bind IP. A new per-IP sliding-window limiter (5 failures / 60 s) now blocks further CONNECTs from a failing IP for the rest of the window; successful auth clears the IP's failure history.
-
VP access codes now compared in constant time. Both the FTP
PASShandler and the MQTT CONNECT handler used Python's==operator on the 8-character access code. Closes the timing side-channel without changing the protocol surface. -
VP FTP uploads capped at 4 GiB.
cmd_STORnow rejects uploads exceeding 4 GiB, deletes the partial file, and replies 426. Without the cap a runaway or malicious client could drive RSS or disk to exhaustion; 4 GiB is well above any realistic multi-plate.gcode.3mf. -
vitest bumped 3.2.4 → 4.1.8 (development dependency) to pick up an upstream CVSS 9.8 advisory. No production-runtime impact, but the floor moves so fresh installs and CI pick up the fix.
-
PyJWT bumped to >=2.13.0 to pick up four upstream advisories. Audit confirmed Bambuddy's usage is unaffected by every behavioural change in 2.13.0 (HMAC empty-key reject can't trigger, OIDC decode uses raw-key path not PyJWK,
jwks_uriis HTTPS from discovery, nob64=falseusage,enforce_minimum_key_lengthnot opted into); 229 auth/MFA/OIDC integration tests + 78 auth unit tests pass on 2.13.0;pip-audit --strictis clean. -
Trivy DS-0026 silenced on
Dockerfile.testviaHEALTHCHECK NONE. The test image runspytestand exits — no service to probe. Adding a healthcheck would have b
een cargo-cult noise.
New Features
-
Windows installer (#1529, contributed by @vmhomelab). Installer for Windows hosts — fresh install, service install / uninstall, in-place update, and a Troubleshooting entry. Existing native Windows users can upgrade via the Update entry on the installer.
-
Turkish (
tr) locale (#1571, contributed by @samedyuksel). Full Turkish translation across all keys, joining the existing 9-locale set. -
Korean (
ko) locale (#1587, contributed by @hijae). Full Korean translation across all keys. -
System theme detection — sidebar toggle and Settings selector follow the OS dark/light preference (#1418, contributed by @TempleClause via PR #1501).
ThemeModegains a third valuesystemalongsidedarkandlight. The provider listens towindow.matchMedia('(prefers-color-scheme: dark)')and tracks the OS preference in real time. The sidebar toggle now cyclesdark → light → system → darkwith the icon hinting at the next stop; Settings → Appearance gained a 3-button Dark / Light / System selector. Existing users' persisted preference is untouched — anyone ondarkorlightstays there and simply gains an extra stop in the cycle. -
MQTT auth rate-limit on the virtual printer. Per-IP sliding-window limiter (5 failures / 60 s) on VP MQTT CONNECT, brute-force-resistant. See Security section above for the full description.
-
Per-slicer MQTT response routing for multi-slicer VP setups. Pre-fix: when slicer A sent a bridge-forwarded command to a non-proxy VP bound to a target printer, the printer's response was fanned out to every connected slicer. Now responses are routed to the originating slicer only, falling back to broadcast for printer-initiated unsolicited pushes (push_status etc.) and for sequence_ids the routing map never saw. Bounded at 256 entries with FIFO eviction.
-
VP child-service readiness barrier. Each VP child sub-service (FTP, MQTT, Bind, SSDP) now exposes a
readyevent that fires after the socket actually binds;start_serverawaits all of them with a bounded 5 s timeout. Closes a race whereis_runningreported true while the child sockets were still binding, which a quick poll from the diagnostic route or the VP card could observe.
Changes
-
Bug-report template tightened + new Area dropdown. 170 issues have been closed
invalid(61 of them in the last 30 days alone — roughly 1 in 5 of all closed issues), nearly always because the reporter hadn't run the in-app diagnostics or checked the documented troubleshooting page. The Connection Diagnostic checkbox, Support Package attachment, and a new "Troubleshooting steps already taken" textarea are now required. The oldBambuddy / SpoolBuddy / Bothdropdown is replaced with two required dropdowns —ProductandArea(15 options covering the actual feature surface) — and an auto-label workflow applies the matchingarea:*label on every issue open/edit. -
VP virtual-printer FTP server streams uploads straight to disk instead of buffering. Pre-fix:
cmd_STORaccumulated every chunk in a list and calledwrite_bytesat the end. Peak RSS for a multi-GB.gcode.3mfwas ~2× the file size and could OOM-kill a low-memory host. The streaming rewrite writes each 64 KiB chunk inline as it arrives, bounding peak memory at one chunk regardless of total upload size. Same change adds the 4 GiB hard cap described above. -
VP virtual-printer FTP passive port range widened from 50000–50100 to 50000–51000. Docker bridge-mode users mapping the old range need to update — see Upgrade Notes above.
-
VP MQTT bridge sticky-keys: 7 more fields preserved across incremental pushes. Pre-fix: a single 1 Hz incremental push (which only carries changed temps / fan / wifi_signal) wiped any field not in the sticky-keys allowlist. The cached state lost
upgrade_state,xcam,hw_switch_state,nozzle_diameter,nozzle_type,online, andams_statusafter a single tick — BambuStudio's Send pre-flight reads several of these and could refuse Send because the cached push said "unknown firmware state". Same shape as #1228 (storage indicators) and #1558 (live-progress fields). Sticky-keys carry-forward is now also acopy.deepcopy(was reference) so a future merge can't corrupt both copies. -
VP target-printer DHCP IP / serial refresh now restarts proxy VPs. Pre-fix: when a target printer's IP changed (DHCP renewal, network reconfiguration), the running proxy VP kept forwarding to the stale IP forever; the user had to manually toggle the VP to refresh.
sync_from_dbnow re-evaluates the proxy target each cycle and restarts the VP when the IP or serial actually changes. -
VP
queue_force_color_matchsetting takes effect immediately. Pre-fix: toggling the per-VP "Force exact color match" setting via the UI silently no-op'd becausesync_from_db's "changed" predicate didn't include the field. Now restarts the running instance on toggle. -
VP MQTT client session errors elevated from DEBUG to WARNING. Production never sees the message at DEBUG; legitimate slicer-side errors (TLS handshake failure, protocol violation) deserve to be visible in the default log level.
-
VP MQTT periodic status push: one-line per-minute counter per active slicer connection (#1548 follow-up). Replaces the noisy per-tick log line with a single per-minute summary, while still surfacing connection-drop events at WARNING.
Fixed
UI / rendering
-
Print-run log, spool usage history, camera-token list, and SpoolBuddy device "last calibrated" timestamps now render in the browser's local timezone instead of UTC (#1602, reported by @maziggy, confirmed by @IndividualGhost1905 with a UTC+3 reproduction). Same shape as the #504 timezone-offset bug — four sites the #504 sweep didn't catch because they were added afterwards or were missed.
-
Archive card's Print Time + accuracy badge are now consistent for multi-run / multi-plate archives (#1608). The card was showing one run's actual duration next to a whole-file estimate, producing badges like "+188%". Multi-run archives now show "Estimated 5h 6m" with no badge; single-run archives keep the badge.
-
Cancelled prints have their own stats bucket and no longer drag down the Success Rate gauge (#1390 follow-up).
-
Cancelled bucket icon now uses the semantic warning token (orange) instead of an unrelated palette colour (#1390 follow-up).
-
Quick Stats: user-cancelled prints now have their own bucket and no longer drag down the Success Rate gauge (#1390 follow-up, reported by @IndividualGhost1905).
Slicer / library
-
Sliced
.gcode.3mffiles now render in the 3D preview and expose a Preview-3D action in the file row (#1543, reported by @Vlado-Tarakan). -
External-folder
.gcode.3mffiles now show thumbnails, and every ingest path stores the same canonicalfile_typefor sliced outputs (#1600, reported by @maziggy). -
Deleted local profiles no longer linger in the SliceModal preset dropdown; new manual "Refresh" button surfaces cloud-side deletions without waiting for the 5-minute cache (#1581, reported by @Lloydcat).
-
STL thumbnail noise on first generation: matplotlib cache + font_manager scan no longer log three WARNING lines on first STL upload (reported by @maziggy).
-
Bulk-upload ZIPs of stub / empty STL files no longer spam the log with thousands of warnings (reported by @maziggy).
-
Print filenames with FAT32-illegal characters now rejected at rename / upload / queue time instead of failing at FTP (#1540, reported by @anthonyma94).
Archive / stats
-
Multi-plate
.gcode.3mfarchives and reprints no longer under-report filament, time, and cost — project stats and parser both fixed (#1593, reported by @needo37). 3-plate file printed plate-by-plate over 9 runs was showing 1/9th of the real material consumption. -
Source-3MF upload on "fallback" archives no longer crashes with HTTP 500 (and stops orphaning files outside the data volume) (#1531, reported by @d3nn3s08).
-
Fallback archives now carry MQTT-derived filament type + colour when the 3MF can't be downloaded (#1533, reported by @JmanB52D), plus a follow-up fix for real prints (#1533 follow-up).
Inventory / Spoolman
-
Transparent / clear filament now selectable and rendered as transparent end-to-end in the built-in inventory (#1545, reported by @Synec5, confirmed by @CMW-ISS).
-
Assigning a spool no longer shows a profile-mismatch warning when only the slicer profile differs; the warning now states the AMS slot will be reconfigured (#1552, reported by @anthonyma94).
-
External-spool usage is now tracked when the AMS has empty slots in between loaded ones (#1607, reported by @ahmtcnby).
-
SpoolBuddy weight sync no longer silently lands on a stale local row when Spoolman is enabled (#1530, reported by @chesterakl).
-
SpoolBuddy Tare status banner no longer sits at "Waiting for device..." forever (#1536, reported by @flom89).
Virtual printer
-
Queue / Review / Archive virtual-printer modes now complete the TLS handshake on hardened-distro hosts (#1610). Real Bambu printers offer only the plain-RSA AES-GCM suites
AES256-GCM-SHA384/AES128-GCM-SHA256; a system crypto policy that strips them left the slicer's ClientHello with no overlap. The #620 cipher-suite fix only patched the printer-facing context; this release patches every slicer-facing VP TLS context (bind / MQTT / proxy-server / FTP). -
VP "Send file" IP rewrite now also fires for VPs without a dedicated bind IP (#1429 follow-up, residual case confirmed by @Mape6).
-
VP "Send file" no longer redirects from Bambuddy to the physical printer's SD card once the printer powers on, and the mode button labels now match the wire values stored in the DB (#1429).
-
VP queue mode no longer blocks BambuStudio Send while the target printer is mid-print (#1558, reported by @phieb).
-
VP
_pending_files/ temp-file leak fixed on every error path across the three file handlers. -
VP queue position now picks
MAX(position)+1instead of hardcoded1; duplicate position=1 entries no longer pile up on non-empty queues. -
VP DELETE route now cleans orphan
PendingUploadrows and the on-diskupload_dir; previously these accumulated. -
VP
MQTTBridge._refresh_loopno longer leaks the raw_message_handler on exceptions in the IP-encoding branch. -
VP
sync_from_dbserialised byasyncio.Lock; concurrent PUT calls (browser racing the auto-save trigger) no longer race the inner start/stop. -
VP
_slicer_print_optionscache bounded at 128 entries with FIFO eviction. -
VP MQTT bridge sticky-key carry-forward now uses
copy.deepcopy; a sticky key carried over from the previous cache no longer shares nested dicts with the next push. -
VP MQTT no longer drops idle slicer connections at exactly 60 s (#1548, reported by @hollajandro); honours client-negotiated keepalive instead of the hardcoded 60 s.
-
VP diagnostic now probes both bind ports 3000 (plain) and 3002 (TLS).
-
VP FTP
stop()now awaits cancelled sessions instead ofsleep(0.1); a session mid-write or mid-TLS-handshake can no longer outlive the stop call. -
VP per-VP TLS certificate auto-regenerates when the shared CA is rotated.
-
VP
tailscale.py::get_statusnow catchesasyncio.TimeoutError; a stuck Tailscale subprocess no longer surfaces an uncaught exception. -
VP
certificate.pyCA save uses correct parent directory; previously the CA write could fail because only the per-VP subdirectory had been created. -
VP
_extract_plate_idfailures now log at debug instead of swallowing silently; a malformedMetadata/slice_info.configis now traceable.
Dispatch / prints
-
A1 no longer auto-replays the previous print after a power cycle when the library row's filename has a doubled
.gcode.3mf(#1542). The dispatch SD-cleanup path now aligns with the upload path; doubled-extension library rows no longer leave ghost prints. -
Connected-edge reconciliation closes the missed-PRINT-COMPLETE loop that produced ghost replays on smart-plug power cycles (#1542 follow-up, reported by @vixussrl-ui).
-
Paused prints no longer inflate maintenance hours (#1521, reported by @TempleClause).
track_printer_runtimewas counting both RUNNING and PAUSE states. -
Webhook printer-status / stop / cancel routes 500'd on every connected printer because the route treated the
PrinterStatedataclass as a dict (#1584).
Cloud / auth
-
Bambu Cloud sign-in failures caused by an upstream Cloudflare challenge now surface an actionable message instead of "Invalid response from Bambu Cloud" (#1575, reported by @cliveflint).
-
OIDC auto-provisioning now reads the standard
emailclaim forUser.emailwhenEmail Claimis set to a non-email identity claim (#1569, reported by @anderl1969).
Notifications
- ntfy notifications: honest User-Agent + actionable error when the server is behind a Cloudflare challenge (#1534, reported by @apizz). User-Agent is now
Bambuddy/<version>instead of the bare httpx default.
Maintenance
- Custom maintenance type "documentation URL" now persists on create (#1596, reported by @BurntOutHylian — with the exact root cause pre-triaged in the issue body).
Internal / CI
-
Path-traversal CI backstop now recognises markers on the closing-paren line (project-wide convention).
-
Backend test sharded 4-way +
-n autofor ~3.5× wall-clock speedup in CI; pip cache mount in the test image. -
Unit tests no longer re-run inside the test image (duplicated the parent CI job).
-
Trivy DS-0026 silenced on
Dockerfile.testviaHEALTHCHECK NONE(see Security).
Credits
External contributors this release: @TempleClause (#1418 / PR #1501 system theme detection), @vmhomelab (PR #1529 Windows installer), @samedyuksel (PR #1571 Turkish locale), and @hijae (PR #1587 Korean locale).
Thank you!