The Edit Spool "PA-Profil" tab and the SpoolBuddy write-tag page fetched a
printer's calibrations with getKProfiles(printer.id), which defaults the nozzle
filter to 0.4. The printer/MQTT layer filters strictly by that diameter, so on
a multi-nozzle printer a same-filament 0.6mm K-profile was never retrieved and
the picker showed only the 0.4mm entry ("1 match"). (The AMS-Slot config dialog
was already fixed in #1899; these two pickers were not.)
Add installedNozzleDiameters(status) and a shared fetchPrinterCalibrations()
that queries every reported nozzle diameter and merges the results, falling
back to 0.4 when the printer hasn't reported nozzle hardware. Each profile row
now shows a nozzle-diameter badge so identically-named profiles are distinct.
Gitea/Forgejo served under a ROOT_URL path prefix (e.g. https://host/gitea)
place repos at /<prefix>/owner/repo. The Gitea backend assumed a host-root
layout: parse_repo_url required exactly two path segments (so subpath URLs
raised "Cannot parse repository URL") and get_api_base dropped the prefix,
yielding https://host/api/v1 instead of https://host/gitea/api/v1.
Treat the final two path segments as owner/repo and keep leading segments as
a base-path prefix; derive the API base as {scheme}://{host}{prefix}/api/v1.
Root-hosted instances are unaffected. Forgejo inherits the fix; GitHub/GitLab
are untouched.
- postcss 8.5.15 -> 8.5.23 (GHSA-r28c-9q8g-f849, source-map path traversal)
- brace-expansion override ^5.0.7 -> ^5.0.8 (GHSA-mh99-v99m-4gvg, DoS)
react-router: pin react-router-dom to exact 7.18.1 (direct dep) and react-router
to 7.18.1 via overrides (transitive). 7.18.1 is the most-patched 7.x -- it clears
14 advisories that older 7.x releases carry, several reachable from a SPA (open-
redirect XSS in Link/useNavigate, route-matching DoS). The one remaining advisory,
GHSA-qwww-vcr4-c8h2, is RSC-mode-only; Bambuddy is a Vite SPA using BrowserRouter
with no RSC runtime (@react-router/server not installed), so the path is
unreachable. The only version that fully clears npm audit is the 8.3.0 major
(no react-router-dom 8.x exists; it needs migrating 50 import sites plus a React
peer bump), deferred as its own change.
Because a version pin can't stop npm from reporting the theoretical 7.11.0
downgrade as fixAvailable, the ci.yml (hard) and security.yml (nightly issue)
audit gates gain a narrow, documented allowlist keyed on the GHSA id. It resolves
the react-router-dom -> react-router advisory chain and stays fail-closed: a
different advisory on react-router still fails the gate, and an isSemVerMajor
guard drops the exemption the moment a non-major fix ships, forcing us to take it.
- postcss 8.5.15 -> 8.5.23 (GHSA-r28c-9q8g-f849, source-map path traversal)
- brace-expansion override ^5.0.7 -> ^5.0.8 (GHSA-mh99-v99m-4gvg, DoS)
react-router: pin react-router-dom to exact 7.18.1 (direct dep) and react-router
to 7.18.1 via overrides (transitive). 7.18.1 is the most-patched 7.x -- it clears
14 advisories that older 7.x releases carry, several reachable from a SPA (open-
redirect XSS in Link/useNavigate, route-matching DoS). The one remaining advisory,
GHSA-qwww-vcr4-c8h2, is RSC-mode-only; Bambuddy is a Vite SPA using BrowserRouter
with no RSC runtime (@react-router/server not installed), so the path is
unreachable. The only version that fully clears npm audit is the 8.3.0 major
(no react-router-dom 8.x exists; it needs migrating 50 import sites plus a React
peer bump), deferred as its own change.
Because a version pin can't stop npm from reporting the theoretical 7.11.0
downgrade as fixAvailable, the ci.yml (hard) and security.yml (nightly issue)
audit gates gain a narrow, documented allowlist keyed on the GHSA id. It resolves
the react-router-dom -> react-router advisory chain and stays fail-closed: a
different advisory on react-router still fails the gate, and an isSemVerMajor
guard drops the exemption the moment a non-major fix ships, forcing us to take it.
The Print Queue History tab reported the full count in its header (e.g.
"History (311 items)") but the row builder hard-sliced the list to
items.slice(0, 50) with no control to load the rest, so everything past the
50th finished print was unreachable. The whole history is already loaded
client-side (the queue endpoint has no limit) and sorted -- it just wasn't
drawn.
History now renders progressively: the first page (50) plus a "Show more"
button and a "Showing X of Y" count that loads the next page until the full
list is on screen. The visible count resets to one page only on a deliberate
re-sort or location-filter change -- not on the periodic queue poll, which
produces a fresh array each tick and would otherwise collapse an expanded
view mid-scroll.
Frontend-only; batch grouping and per-row actions unchanged. Two new i18n
keys across all 12 locales. Covered by a test asserting the 50-row cap, the
count label, and that Show more reveals the remainder. Wiki updated.
With LDAP auth in use, the debug log carried the full user DN on successful
auth -- e.g. "(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 already
redacted, and it passed straight into an uploaded support bundle. The log
sanitizer (shared by the support bundle and the in-app bug report) had no DN
pattern; DNs also leak via ldap3 exception strings and group-mapping logs.
- sanitize_log_content: redact LDAP DNs to [DN] -- a run of >=2 attr=value RDN
components (CN/OU/DC/UID/...). The value class excludes <>;+ (RFC 4514 requires
them escaped in a value) so the final comma-unbounded component doesn't swallow
trailing log text such as "-> GroupName". Ordinary key=value lines are untouched.
- ldap_service: stop logging the raw DN on successful auth (username + group
count suffices), keeping the PII off disk even before bundle sanitization.
Closing an external USB (V4L2) camera view abruptly could leave its ffmpeg
running and holding /dev/videoN open -- LED stuck on, and reopening the view
failed or took 10-30s fighting for exclusive device access. Same class of leak
as #776 (built-in RTSP path), but the external path was never wired into that
fix: external streams registered into none of the _active_streams /
_disconnect_events / spawned-PID registries, so /camera/stop returned
{"stopped": 0} for a live USB stream and the orphan janitor's /proc net matched
only rtsp(s)://bblp: cmdlines. Cleanup ran only via the stream generator's own
finally, which an abrupt disconnect can skip.
- Thread an on_process callback + stop_event through generate_mjpeg_stream into
_stream_usb / _stream_rtsp; register the process before the startup probe so a
process that hangs on a locked device (not just one that exits) is reapable.
- Register external streams into the shared registries under a unique
{printer_id}-ext-{token} id so /camera/stop and cleanup_orphaned_streams find
and kill them; stop_event prevents the reconnect loops from respawning.
- Extend the /proc safety-net scan to also match USB (-f v4l2) ffmpeg, excluding
still-active streams and unrelated ffmpeg.
The slice client only checked the sidecar's HTTP status, not its body. When the
sidecar -- or a reverse 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 emitting no output), Bambuddy
wrote that tiny blob to a .gcode.3mf, stored it as a valid sliced file (the
3MF-parse failure was swallowed as "no thumbnail"), and let it be queued and FTP'd
to the printer -- producing the ~28-byte files that "did nothing" and then failed
at print time. Separately, a genuine 413 comes from the proxy in front of the
sidecar rejecting the multi-MB upload (model + profiles), so raising the body
limit on the wrong proxy layer had no effect.
- Factor the duplicated status handling in slice_with_profiles /
slice_without_profiles into one _handle_slice_response.
- When a 3MF export was requested, validate the body is a real ZIP; otherwise
raise SlicerApiServerError with an actionable message instead of persisting
a corrupt file.
- Special-case 413 with a message naming client_max_body_size on the proxy
directly in front of the sidecar (Cloudflare cap noted).
The folder tree's "sort by recent activity" and the file pane's date sort
put external (mapped/NAS) files in a near-random order instead of ls -t's
newest-first. Nothing captured the files' on-disk mtime: the sort keyed off
the DB updated_at/created_at, which for a bulk external scan is the same
scan instant for every row, so a whole block tied and sorted arbitrarily;
only rows Bambuddy had later touched individually looked "partially right."
The tree also bubbled up only immediate child-file activity, so a file added
deep in a subtree never lifted its parent folders.
- Add nullable fs_modified_at to LibraryFile and LibraryFolder (dialect-
branched migration, mirroring the #2615 dispatching_at pattern).
- External scan records each file's and directory's real os.stat().st_mtime
and refreshes it on every re-scan, so a file edited over the mount
re-sorts and existing installs backfill on the next scan.
- list_folders computes each folder's activity as a recursive newest-
descendant roll-up (post-order), so a fresh deep file lifts every ancestor.
- Folder tree sort and the file pane's date sort now use the real mtime,
falling back to created_at for managed uploads with none.
- New toolbar toggle shows/hides each item's last-modified date in the right
pane (grid + list), with strings in all locales.
Store the mtime as naive UTC to match the other timestamp columns so activity
comparisons never mix naive and aware values on either dialect. Covered by
integration tests (mtime capture, re-scan refresh, deep-file recursive bubble,
folder mtime) and a frontend test proving fs_modified_at is preferred over
created_at.
After #2594 every empty-slot clearing path skipped AMS-HT units, so a removed
HT spool never cleared on the printer card while Bambu Studio correctly showed
Empty. Root cause: the HT presence bit is packed as a single consecutive bit in
tray_exist_bits at 16 + (ams_id - 128) (HT-A=16, HT-B=17, ...), not the regular
ams_id*4 position -- so the bitmask cleanup, which skipped id>=128, never
touched it. The HT's state field is firmware-variant (loaded reports 11 on H2D,
9 on the #2594 firmware) and it keeps echoing stale tray_type after removal, so
the bitmask is the only reliable, firmware-independent signal. Confirmed against
a live H2D capture (loaded=0x10f7f, empty=0xf7f) and the OrcaSlicer
DevFilaSystem.cpp reference (is_exists = tray_exist_bits >> (16 + (ams_id-128))).
- apply_tray_exist_bits: handle AMS-HT (128-135) at 16+(ams_id-128) instead of
skipping it; clears an empty HT and, because a loaded HT keeps its bit set,
never wrongly clears one (keeps the #2594 fix intact). Ids outside the known
regular (0-15) and HT (128-135) ranges are left untouched rather than guessed.
- Build the AMS change-hash from the merged state, not the raw payload, so a
removal signalled only by the bitmask (firmware still echoing tray_type) still
flips the hash and fires on_ams_change to unbind the spool_assignment row.
- Emit the exists presence bit in the websocket status serializer (REST already
did) so the card renders "Empty" instead of "?" where state is ambiguous.
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)". Name and
grams shared one fixed-width column with truncate on the whole string, so a
long name pushed the "(...g)" off the end and clipped it -- partially on a
wide screen, entirely in mobile portrait. The gram usage is the number that
matters (does the spool have enough left?), so it shouldn't be the part that
gets dropped.
Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name
truncate, with the full name on hover. Applied to both the Specific-Printer
(FilamentMapping) and Any-model (PrinterSelector) panels. Layout only.
Fetching K-profiles probes every nozzle size in turn (extrusion_cali_get
for 0.2/0.4/0.6/0.8mm), and each response echoes the requested diameter at
the top level. _process_message passed every "print" message -- including
these responses -- to _update_state, which treats a top-level
nozzle_diameter as the installed hardware. So the last size probed (0.8)
overwrote the real nozzle in memory; a genuine status push corrected it and
the next K-profile fetch broke it again, which is why it flickered between
0.8, empty and the correct 0.4. Since 1.2.5 the #1899 mismatch guard then
refused to dispatch, failing prints with a bogus "printer has 0.8mm".
Handle extrusion_cali_get responses only via the K-profile parser and skip
_update_state for them, mirroring the existing get_accessories guard. The
nozzle size now comes solely from the real status push. In-memory only --
affected printers self-correct on the next push after updating.
The print queue couldn't be reordered on a phone. In portrait the drag
grip and selection checkbox are hidden below the sm breakpoint, so there
was no reorder affordance at all; in landscape the grip shows but it
carried touch-action: manipulation while the only dnd-kit sensor is a
PointerSensor with an 8px distance, so touch gestures scrolled instead of
starting a drag. Reordering was effectively mouse-only.
Add tap-friendly up/down arrow buttons to pending rows on mobile (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. Arrows appear only in the manual
"position" sort (shortest-job-first off) where position has meaning, are
gated on queue:reorder, and the first row's up / last row's down render
disabled. Also switch the desktop drag handle's touch-action to none so
mouse-style drag works on touch (landscape phones, tablets). Reuses the
existing queue.moveUp / queue.moveDown translations.
The File Manager 3D Preview dialog (ModelViewerModal) rendered plate
thumbnails with the raw thumbnail_url. The plate-thumbnail endpoints are
gated behind a camera stream token passed as ?token= (an <img> can't send
an Authorization header), so with auth enabled the browser fetched without
a token and got 401 "Valid camera stream token required" — broken image
icons for every plate. The Slice dialog's picker (PlatePickerModal) and the
Print modal's PlateSelector already append the token via withStreamToken(),
which is why the same file's thumbnails showed there.
Wrap the thumbnail src in withStreamToken(), matching the other two call
sites. The token is synced app-wide and withStreamToken() is a no-op when
auth is off, so non-auth setups are unchanged.
Follow-up to 0f203ce: force color match now picks the right AMS slot, not
just the right printer. The slot mapper cleared tray_info_idx when applying
an override, so on a printer holding two same-colour PLA spools of different
variants (Basic GFA00 / Matte GFA01 / Silk GFA06) it could map to the wrong
one. It now keeps the variant for force_color_match overrides (both the 3MF
and no-3MF fallback paths) so the matcher pins the matching tray, and falls
back to type+colour when that variant isn't loaded. A manual filament swap
(a preference override) still clears the idx so it matches the swapped-in
spool rather than the old one.
The printer-card queue-compatibility hint applies the same variant rule.
Force color match dispatched onto the wrong PLA sub-variant: a job sliced
for White PLA Matte was treated as an exact match by printers loaded with
White PLA Basic or Silk+, because Bambu reports every variant as
tray_type "PLA" and the distinction lives only in tray_info_idx
(GFA00=Basic, GFA01=Matte, GFA06=Silk).
Two places dropped the field: the VP queue built each force override
without the parsed tray_info_idx, and _get_missing_force_color_slots
compared loaded trays on (type, colour) only.
Carry tray_info_idx into the override and require it to match when both
the override and a candidate tray have one; a blank idx on either side
(custom/third-party spools, older 3MFs) falls back to the historical
type+colour behaviour, so those setups are unaffected.
**Bambuddy 1.2.5**
**⚠ Upgrade Notes — Read Before Updating**
**About the version number.** This is the successor to **0.2.4.9** — the first digit moved from **0** to **1** (0.2.5 became 1.2.5). It is a normal next release on the same code base, not a rewrite; the jump in the leading digit is only a versioning-scheme change. The in-app Apply Update button in Settings → System → Updates works for Docker and for any native install already on the 0.2.x line.
1.2.5 folds in the whole beta cycle, so it is a large release. There are no breaking schema changes beyond auto-migrated column additions (dialect-branched for SQLite and Postgres), and every migration was run against both engines.
**Behaviour-change callouts to know about before you upgrade:**
- **Bed levelling, flow calibration, and nozzle-offset calibration are now three-way Off / Auto / On, and new prints default to Auto.** This matches Bambu Studio (Auto lets the printer skip a calibration it did recently). Your existing queued prints and saved workflow defaults are migrated automatically — anything that was "on" becomes "On (force)", anything "off" stays "Off" — so nothing changes for in-flight jobs until you opt into Auto.
- **Docker now shuts down gracefully.** Previously every docker stop / restart / image update was a SIGKILL after the full grace period — no WAL checkpoint, no MQTT disconnect, no clean teardown. That is fixed (uvicorn is now PID 1 and receives the signal). To also pick up the raised stop grace period, refresh your docker-compose.yml (it now sets stop_grace_period: 30s). systemd/launchd/Windows launchers get the equivalent timeout automatically via the installer.
- **Multi-printer farms dispatch far faster.** Uploads to different printers now run concurrently (new Settings → Workflow → Queue & Dispatch → Concurrent Uploads, default 4, up to 16 — set it to 1 for the old strictly-serial behaviour), and the scheduler re-ticks within seconds after a productive pass instead of waiting a fixed 30 s. A stuck printer is now failed after three attempts instead of retried forever.
- **PostgreSQL pool defaults raised and made configurable.** The pool is now 20 + 80 (100) by default with DB_POOL_SIZE / DB_MAX_OVERFLOW / DB_POOL_TIMEOUT / DB_POOL_RECYCLE overrides, plus a GET /api/v1/system/db-pool gauge. If you run a large farm on Postgres, review your server's max_connections headroom — see the PostgreSQL wiki page.
- **Bambu Cloud sign-in state is now honest.** A lapsed token is properly detected instead of showing "Connected" forever, and a single stray 401 no longer signs you out. If you linked your Bambu account before enabling authentication (or crossed an auth on/off transition on an older build), you may need to re-link once from the Profiles page.
- **Manual jog safety (Bambu firmware bug).** Bambu firmware does not enforce its soft endstops on G-code received over MQTT, so manual jog can overrun a travel limit. Bambuddy no longer disables the endstops globally around a jog (which previously also broke the touchscreen's limits until a power-cycle) and now shows a prominent warning on the jog panel. If your printer currently overruns even from its own touchscreen, power-cycle it once to restore the endstops an older Bambuddy build disabled.
- **P1S / P1P AMS drying is screen-only.** P1 firmware acks drying commands and discards them, so Bambuddy no longer offers Start/Stop for P1 drying — the flame button stays visible but disabled with an explanation. A cycle started at the printer still shows its live countdown.
- **REST smart-plug energy (Shelly users).** If your Energy JSON Path points at a cumulative lifetime counter (anything from a Shelly does), move it to the new Energy JSON Path (lifetime) field so Today/Yesterday/Total populate correctly.
- **Re-take your backups after upgrading.** A Postgres → SQLite backup export previously dropped NOT NULL / DEFAULT / FK / UNIQUE; that is fixed, but backups taken on an older build still carry the degraded schema.
- **Stats.** Reconciled-after-reconnect prints no longer inflate Total Print Time by hundreds of hours. Rows already inflated by the old bug are not auto-corrected (they're indistinguishable from real cancellations) — repair them by hand if needed.
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
Refresh docker-compose.yml if you want the new stop_grace_period: 30s (recommended, so a slow teardown on a Pi isn't clipped).
**Native install — recommended path**
sudo BRANCH=main /opt/bambuddy/install/update.sh
Snapshots the database first and rolls back on failure. Also carries any custom ReadWritePaths you added (e.g. for NAS backups) forward into the new systemd unit.
**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
cd frontend && sudo npm i
sudo systemctl start bambuddy
**Windows install**
Download bambuddy-1.2.5-windows-x64-setup.exe from this release page (or the unversioned bambuddy-windows-x64-setup.exe alias for an always-latest link). Existing Windows installs upgrade in place via the in-app Install Update flow.
---
**Highlights**
1.2.5 is a big release that lands several long-requested features on top of a large stability and farm-scale correctness pass.
**New surfaces:**
- **Slicer Pipelines** — save a printer/process/filament/bed-type bundle once and dispatch it with a click. Full production-batch semantics: multi-copy runs, printer-class targeting, three fan-out strategies (max parallel / round robin / fill one first), a runs dashboard with cancel and retry-failed, and live updates (#1425).
- **Cam Wall** — a full-page grid of live camera tiles on the Printers page, with a per-tile print-status overlay, a bookmarkable /camwall URL, and a purpose-built read-only kiosk token for a lobby TV that exposes no serial, IP, or filename (#2531).
- **HMS error actions** — the error dialog goes from read-only to actionable: Resume / Stop / Ignore / Check Assistant and the rest now send the matching command to the printer, so you no longer have to walk to the machine to clear a pause (#1743, #1830).
- **AMS Filament Backup** — read and toggle the printer's auto-switch-to-a-second-spool state right from the card, and a paused runout now names the exact physical slot the printer is waiting for (#2587).
**Farm and stability:**
- Prints on a multi-printer farm no longer start one-by-one up to an hour apart — uploads run concurrently and the scheduler re-ticks as printers free up (#2555).
- A sustained session-hygiene and pool-sizing pass stops PostgreSQL connection-pool exhaustion on large farms, where camera streams, FTP work, and 3MF parsing had been holding DB connections across slow I/O (#2572, #2573).
- Docker finally shuts down cleanly instead of being SIGKILLed on every stop, and a run of multi-plate dispatch bugs that could map the wrong filament, log the wrong plate, or split a dispatch across two printers are fixed (#2551, #2552, #2603, #2614, #2615).
**Smaller-but-useful:** batch/mass edit on the Filament tab (#1795), structured storage-location and user-tag catalogs plus recursive search and per-folder README panels in the File Manager (#1505, #1268), continue-drying-while-printing on capable hardware, a dedicated AI Failure Detection notification event (#1794), sort Printers by ETA
(#1609), admin-configurable session lifetime (#1706), a full Russian translation (#2608, 11 languages total), and much more below.
---
**New Features**
Slicer Pipelines (#1425):
- Save and reuse a preset bundle in one click from the Slice modal (PR A).
- Run a pipeline on a file (library or archive) with one click, with pre-flight eligibility and a progress toast (PR B).
- Multi-copy batches, printer-class targeting, three fan-out strategies, a runs dashboard with cancel/retry-failed, and live WebSocket updates (PR C — completes the v3 design).
Cam Wall (#2531):
- Cam Wall view on the Printers page — a responsive grid of live camera tiles with on-screen/live-budget scheduling so it stays sustainable on a Pi.
- Per-tile print/printer status overlay (Off / Compact / Full).
- Bookmarkable /camwall URL plus a purpose-built read-only kiosk token that serves only what a tile draws — no serial, IP, access code, or filename.
Printer control and status:
- HMS error actions on the dashboard — Resume / Stop / Check Assistant etc. now send the matching MQTT command (#1743, contributed by @Ichicoro).
- AMS Filament Backup status + control on the printer card, read from the print.cfg bit and toggled over MQTT.
- A paused AMS runout now names the physical slot the printer is actually waiting for, with a pulsing highlight on the AMS graphic (#2587).
- AMS drying badge shows the active cycle's filament and target temperature.
- Live print progress for Virtual Printers in Bambu Studio / OrcaSlicer while keeping the Send button enabled (#1887).
Inventory:
- Batch / mass edit on the Filament tab — bulk edit / print labels / reset usage / archive / delete across both built-in and Spoolman modes (#1795).
- Structured storage-locations catalog (shelves, drawers, dryboxes) with Spoolman parity (#1505, closing #1004, contributed by @Poltavtcev).
- By-tag spool lookup readable with a Manage-Inventory API key, for scanner-driven integrations (#1700 closing #1663, contributed by @bambuman).
- Spoolman weight tracking for no-3MF "Untitled" prints, closing a parity gap with built-in inventory (#1820).
File Manager (#1268):
- User-authored tags for cross-cutting file filtering, independent of folders.
- Recursive search inside the selected folder, and a per-folder markdown README panel (collapsible right-hand rail).
- Page-wide drag-and-drop upload (#1510), and sort the folder tree by recent activity (#1770).
Drying:
- Continue auto-drying while a print is running, on capable hardware (opt-in, temperature-capped).
Notifications:
- Dedicated "AI Failure Detection" notification event so Obico detections stop riding the multiplexed Printer Error toggle (#1794).
- Inline finish-photo embed in failure-event emails via the {finish_photo_url} template variable, plus user_print_* template disambiguation (#1792).
Accounts, API keys, and layout:
- QR code on API-key creation that encodes server URL + key together for one-scan mobile setup (#1677, contributed by @bambuman).
- Admin-configurable session lifetime (24h / 7d / 30d, default 24h) (#1706).
- Centralised sidebar layout with per-page hide toggles and an admin default order (#1673, contributed by @EdwardChamberlain).
- Per-VP G-code injection toggle for Studio Send / FTP uploads (#1516, contributed by @phieb).
Other:
- Slice as designed — keep a MakerWorld author's own embedded settings when slicing server-side, when your printer matches the design's target (#2611).
- Sort the Printers page by ETA (#1609).
- Unified print dispatch through the queue scheduler, so every print is queueable, cancellable, and attributable (#1625, by @EdwardChamberlain).
- Sticky upload-progress toast restored for scheduler-driven dispatch (#1625 follow-up).
- Sponsor surfaces now ask a print farm (5+ printers) a commercial question instead of the hobbyist donation ask.
- Russian (Русский) UI translation — 11 languages total (#2608, contributed by @pterodaktil02).
- Appliance endpoints for NTP-gate state and locale/hostname/timezone defaults.
---
**Fixed**
Dispatch, scheduler, and farm scale:
- Prints on a multi-printer farm started one-by-one, up to an hour apart — uploads now run concurrently (#2555).
- A printer that accepted a file but never started was retried forever — now failed after three attempts (#2555).
- Every job waited up to 30 s after a printer freed up — the scheduler now re-ticks fast after a productive pass (#2555).
- PostgreSQL connection-pool exhaustion on large farms, plus a sustained session-hygiene pass so camera streams, cover/snapshot/timelapse, the print-start and finish-photo handlers, notification snapshots, FTP helpers, and SMTP no longer hold a DB connection across slow I/O (#2572).
- Startup connected printers serially (~100 s to first response on a 93-printer farm) — now concurrent (#2572).
- Queue polling re-parsed every 3MF on each poll — now a single combined parse cached by file revision (#2573).
- Large prints uploaded twice at once and never landed — deadline now scales with file size and actually cancels a too-slow transfer, with a per-printer upload lock (#2529).
- queue_max_concurrent_uploads behaved as a per-batch cap instead of a refillable pool (#2602).
- Reassigning a queue item mid-dispatch split it across two printers (#2615), a start dispatched to an already-busy printer could cancel the running job (#2598), and an unresolved AMS mapping silently dispatched to the empty external spool (#2589).
Multi-plate and filament mapping:
- Skip Objects listed the wrong plate's objects (#2522), and single-plate object lists could crash dispatch.
- Queueing several plates of one file mapped them all through the first plate's filaments (#2551), Filament Override vanished for a multi-plate selection (#2552), and Force color match made every plate wait for every colour (#2551).
- A single plate of a multi-plate 3MF recorded the whole file's filament in statistics (#2614), and multi-plate queue prints lost the selected plate in Print History (#2603).
- A print mapped to a different filament than it was sliced for was logged under the sliced material, not the one used (#2563).
- Multi-nozzle prints no longer collapse all filaments onto one nozzle (#1825), and nozzle sizes other than 0.4 mm are fully supported in the AMS slot picker + a pre-dispatch guard (#1899).
AMS and filament:
- A2L "AMS Lite" slots showed empty and never deducted filament (unit id 16 normalisation).
- The HT-A (AMS-HT) spool vanished a few seconds after power-on (#2594).
- External spool kept its old inventory filament after a type change (#2575), and configuring a built-in/generic filament reverted a moment later (#2604).
- An AMS slot with a non-Bambu (no-RFID) spool showed "Empty" instead of "?" (#2527), and the drying "Rotate spool" toggle is no longer offered when a tray is threaded out.
Camera:
- P1/A1 camera stayed black on load until a ~20-minute self-heal — late-subscriber priming + teardown discipline (#2521, #2521 follow-ups).
- Cam Wall no longer kills shared streams when one viewer closes, and offline tiles show OFF rather than LIVE.
- P2S RTSP timeout could leave the fan-out stream permanently stalled (#2580, diagnosed by @ronaldheft, fix shape from PR #2581).
Cloud, MakerWorld, and connectivity:
- Bambu Cloud dropped to "sign-in expired" and forced constant re-logins (any-401 now narrowed to the documented token-expiry response) (#2530-related, and the #2562 follow-up).
- Enabling authentication silently disconnected Bambu Cloud — the token now migrates onto the admin (and back) across the auth transition (#2530).
- "Please login." during MakerWorld import while showing Connected — cloud status is now authoritative.
- MakerWorld import on Windows failed with a certificate error (S3 hop now verifies against certifi) (#2562).
- MakerWorld import/resolve/status failed under API-key auth even with a valid owner cloud login (#1777).
- H2C prints intermittently recorded no filament — the H2C now gets the TLS 1.2 FTP profile (#2582), and H2C prints now deduct from inventory (#2582).
Shutdown and launchers:
- Docker never shut down gracefully — every stop/restart/update was a SIGKILL (exec uvicorn as PID 1).
- systemctl restart could hang 90 s and end in SIGKILL when a camera stream was open — every launcher now bounds the graceful-shutdown wait.
Smart plugs and energy:
- Energy Summary stuck at zero for Yesterday and Total on REST smart plugs, plus the whole smart-plug subsystem was broken on Postgres (naive-vs-aware datetimes) (#2539).
- Switching off an accessory smart plug at print end knocked the printer into "Unknown" and stalled the queue — plugs now carry a "Powers the printer" flag (#2629).
Other fixes:
- Reconnect/restart inflated Stats → Total Print Time by hundreds of hours (#2592).
- Scheduled backups to a NAS failed with EROFS because of our own systemd ProtectSystem sandbox — installers now carry custom ReadWritePaths forward and the UI diagnoses the real cause (#2544).
- Postgres → SQLite backup dropped NOT NULL / DEFAULT / FK / UNIQUE (#2526).
- Every SpoolBuddy screen crashed when a text field was focused (CJS interop) (#2616).
- The streaming overlay (/overlay) was blank in OBS with login enabled — now a token-authenticated kiosk surface (#2613).
- The AMS slot popup covered the filament dialog it opened, on touch devices (#2631).
- Slicing a single plate failed on a filament slot the plate doesn't use, and a profile could be auto-picked for a printer it doesn't belong to (#2628 and follow-up).
- Pushover Emergency priority (2) was rejected by the API (#2586); progress notification ran off-screen in the iPhone PWA (#2612).
- "Remember Me" appeared broken — an authenticated visit to /login now redirects (#1889).
- Packaging floors that permitted un-runnable resolutions: FastAPI < 0.116 204-route crash, sqlalchemy floor raised to 2.0.38, ruff pinned exactly; printer FTPS/MQTT now declare a TLS 1.2 floor explicitly.
(This is a condensed list — see CHANGELOG.md for the full detail on every entry, including tests and scope.)
---
**Security**
- Bumped linkify-it and dompurify to their patched releases (both build/production-tree hygiene; neither reachable path was exposed).
- Raised the Docker image's pip floor to 26.1.2 (PYSEC-2026-196) — build tooling only.
- Bumped two frontend dev-tooling dependencies (brace-expansion, js-yaml) with denial-of-service advisories — lint/build-time only, not in the shipped app.
---
**Merged community PRs in this release**
Thank you to everyone who contributed:
- #1625 @EdwardChamberlain — Unify print dispatch through the scheduler
- #1743 @Ichicoro — HMS error actions
- #1673 @EdwardChamberlain — Centralised sidebar ordering + page visibility
- #1516 @phieb — Per-VP G-code injection toggle
- #1700 @bambuman — By-tag spool lookup for Manage-Inventory keys (#1663)
- #1677 @bambuman — QR code on API-key creation
- #1505 @Poltavtcev — Structured storage locations catalog (#1004)
- #2608 @pterodaktil02 — Russian localization
- #2596 @Sawtaytoes — Virtual Printer "Any [model]" dispatch diagnosis (#2595)
- #2581 @ronaldheft — P2S RTSP stream-timeout fix shape (#2580)
- #2633 @dependabot — js-yaml security bump
---
**Sponsors**
Bambuddy is sustainable thanks to people who put their money where their use is. If this release saved you time or kept your farm running, the project runs on recurring contributions — there's no paid tier, no telemetry, no upsell, just sustainable maintenance.
- GitHub Sponsors (recurring, 5 tiers from $5/mo to $300/mo) — https://github.com/sponsors/maziggy
- Ko-fi (one-time or recurring) — https://ko-fi.com/maziggy
The bed_levelling/flow_cali/nozzle_offset_cali boolean->tristate migration
builds two UPDATE statements with an f-string interpolating the column
name. Bandit flags these as B608 (SQL injection) at medium severity, which
failed the release-gate scan in test_security.sh.
The interpolated _col only ever iterates the hardcoded _tristate_cols tuple,
never user input, and SQL identifiers can't be passed as bound parameters.
Suppress with `# nosec B608` (matching the existing settings.py convention)
plus an inline rationale. No behavior change.
test_launcher_shutdown_config.py and test_systemd_backup_paths.py read
repo-root launcher/config files (Dockerfile, docker-compose.yml,
deploy/bambuddy.service, install/install.sh, installers/windows/...,
spoolbuddy/install/install.sh). The Docker test image built from
Dockerfile.test copies only backend/, pyproject.toml, gcode_viewer/ and
requirements, so all 15 tests failed in test_docker.sh with "launcher
moved or was removed" — the files simply aren't in the image.
Guard both modules with skipif on frontend/package.json, which is present
in every source checkout but never in the test image. Native runs
(test_backend.sh, every commit) still execute the tests in full and catch
a genuinely moved/deleted launcher; the release-gate Docker run skips them
instead of failing on files it deliberately doesn't ship.
Moves react-router-dom/react-router 7.16.0 -> 7.18.1, off the range
flagged by GHSA-wrjc-x8rr-h8h6 (open redirect via backslash in Link/
useNavigate), GHSA-h8fp-f39c-q6mh (RSC), and GHSA-337j-9hxr-rhxg (SSR
hydration). The latter two need RSC/SSR, neither of which this
client-only SPA uses; the open-redirect one is the only reachable path
(post-login redirect), already guarded by sanitizeRedirectTarget.
Stays within the existing ^7.16.0 caret, no new transitive deps. Rebuilt
the static bundle. npm audit now reports 0 vulnerabilities.
npm audit flagged both against the production dependency tree, and the
Frontend Security job fails on any fixable high-severity finding there
(FIXABLE HIGH: linkify-it).
linkify-it 5.0.1 -> 5.0.2 (GHSA-v245-v573-v5vm, high, CVSS 7.5) fixes a
quadratic-complexity DoS in the mailto: validator scan loop. It reaches us
only through prosemirror-markdown inside @tiptap/pm; the editor's own
autolinking uses linkifyjs, which is a different package and unaffected.
Nothing under frontend/src/ imports prosemirror-markdown or markdown-it and
neither appears in the production bundle, so the vulnerable code is tree-
shaken out and no running install was exposed.
dompurify 3.4.11 -> 3.4.12 (GHSA-c2j3-45gr-mqc4, low) fixes a
CUSTOM_ELEMENT_HANDLING bypass of afterSanitizeElements for allowed custom
elements. DOMPurify is shipped, but we never set CUSTOM_ELEMENT_HANDLING and
register no afterSanitizeElements hook, so the bypass has no precondition;
ProjectPageModal additionally passes a strict ALLOWED_TAGS/ALLOWED_ATTR
allowlist.
Both patched versions already satisfy the ranges their parents declare, so
this is a lockfile-only change - no overrides entry needed, package.json
untouched. npm audit reports zero vulnerabilities, npm run build is clean,
and all 2423 frontend tests pass.
npm audit flagged both against the production dependency tree, and the
Frontend Security job fails on any fixable high-severity finding there
(FIXABLE HIGH: linkify-it).
linkify-it 5.0.1 -> 5.0.2 (GHSA-v245-v573-v5vm, high, CVSS 7.5) fixes a
quadratic-complexity DoS in the mailto: validator scan loop. It reaches us
only through prosemirror-markdown inside @tiptap/pm; the editor's own
autolinking uses linkifyjs, which is a different package and unaffected.
Nothing under frontend/src/ imports prosemirror-markdown or markdown-it and
neither appears in the production bundle, so the vulnerable code is tree-
shaken out and no running install was exposed.
dompurify 3.4.11 -> 3.4.12 (GHSA-c2j3-45gr-mqc4, low) fixes a
CUSTOM_ELEMENT_HANDLING bypass of afterSanitizeElements for allowed custom
elements. DOMPurify is shipped, but we never set CUSTOM_ELEMENT_HANDLING and
register no afterSanitizeElements hook, so the bypass has no precondition;
ProjectPageModal additionally passes a strict ALLOWED_TAGS/ALLOWED_ATTR
allowlist.
Both patched versions already satisfy the ranges their parents declare, so
this is a lockfile-only change - no overrides entry needed, package.json
untouched. npm audit reports zero vulnerabilities, npm run build is clean,
and all 2423 frontend tests pass.
Tapping Configure on an AMS slot left the slot popup standing on top of
the filament type/colour dialog it had just opened, so both layers were
on screen at once.
The popup is portaled at z-[60] so it can escape the stacking contexts
sibling printer cards create on the dashboard (#1336), which also puts
it above ConfigureAmsSlotModal and LinkSpoolModal at z-50. Nothing
dismissed it: it is hidden only by the pointer leaving it, and a touch
device never sends that after the tap that opened it. On desktop the
next mouse movement cleared it, which is why this is a tablet report.
FilamentHoverCard and EmptySlotHoverCard now dismiss themselves before
running any action that opens a dialog or navigates away - Configure,
Assign Spool, Unassign Spool, and both Open in Inventory links. The
dismissal clears the pending timer as well, so a queued open cannot
resurrect the card over the dialog.
Actions that report progress inside the popup are unchanged: RFID
re-read, Load and Unload render their spinner there, and Copy UUID its
confirmation tick.
Pairs the top-down plate preview with the slicer's per-object pick mask
(Metadata/pick_N.png), whose pixel colours encode the same identify_id the
firmware's skip command takes, so a click resolves to a real object rather
than an inferred bounding box. Several objects can be selected before one
confirmation; selected and already-skipped items are highlighted on the
plate; the checklist stays available when no mask exists.
view=pick serves only the active plate's mask and 404s otherwise, unlike
every other view. A render returned in a mask's place would be decoded as
object IDs — dark pixels yield small integers that collide with real ones —
and a click would then skip an arbitrary object, mid-print, irreversibly.
The 404 is what tells the UI to fall back to the checklist.
Click mapping goes through the contained rect, since the canvas paints at
mask resolution under object-contain; clicks on a letterbox bar are rejected
rather than clamped onto whichever object touches the border. Confirming
names the object when one is selected and counts them when several are,
which is what plates of identically-named clones need.
No printer-control command path was added or changed; the layer, permission
and existing skip-command guards are untouched.
Slicing for a P2S failed with "filament preset Bambu PLA Basic @BBL X1C 0.2
nozzle (slot 1) is not compatible with printer Bambu Lab P2S 0.4 nozzle" —
naming a profile shown nowhere in the dialog. The picked profile was
"Overture PLA Matte @0.2", whose inheritance chain roots in that X1C profile.
The dialog classifies a profile by its compatible_printers list and falls back
to reading the printer out of its name. That name carries no model, and the
list — present on the imported copy — is not shipped by every source: Bambu
Cloud omits it deliberately (rate limits), and Orca Cloud shipped it but
Bambuddy only mined filament type and colour from the same content.
Orca Cloud entries now carry their own compatible_printers, and the existing
same-name enrichment bridge carries the list onto entries that lack one, in
both directions between the cloud tiers. A bare "@<size>" name tag is read as
a nozzle size as a last resort: it can rule a printer out but never rules one
in, and implausible values are ignored rather than guessed at.
An end-of-print auto-off on a plug that powers a filter fan marked the linked
printer offline and forced its state to "unknown". The mark was unrecoverable:
connected heals on the next MQTT message but state does not (only frames
carrying gcode_state rewrite it, and steady-state push_status frames are
partial), so the printer stayed "unknown" until a manual Force Refresh and the
queue never dispatched to it again.
The offline mark is now an explicit presumption: mark_power_off records the
state it overwrites and _on_message undoes it as soon as the printer sends
another report on its own topic, since inbound traffic proves the power was
never cut. A reconnect discards the saved state, so a genuine power cut is
unaffected. Each plug also gains a controls_printer_power flag (default true,
backfilled) that gates all five power-off paths, and the queue's power-on step
now picks the flagged plug instead of whichever linked plug came first.