**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.
The image upgraded pip to >=26.1, but PYSEC-2026-196's fix is specifically
26.1.2 (PYSEC-2026-2875/2876 are fixed in 26.1). The old floor could resolve
26.1.0/26.1.1, which are still vulnerable to PYSEC-2026-196. --upgrade already
grabbed the latest in practice; this makes the pin match the advisory exactly.
Both are transitive dev-only dependencies under eslint (via minimatch and
@eslint/eslintrc) with denial-of-service advisories (GHSA-3jxr-9vmj-r5cp,
GHSA-52cp-r559-cp3m). They are lint/build tooling and not part of the
shipped app, so no running install was exposed. npm audit fix wouldn't move
eslint to the patched releases on its own, so they are pinned through the
existing overrides block in package.json (brace-expansion ^5.0.7,
js-yaml ^4.3.0). npm audit now reports zero vulnerabilities; eslint runs clean.
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.
Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
Assigning a spool to an AMS tray pushed ams_filament_setting +
extrusion_cali_sel and reported success immediately, whether or not the
tray accepted it. A silently-dropped assignment never surfaced, and since
a print only deducts from the spool on the exact tray it pulls from, it
also recorded no filament usage - which made the whole thing feel random.
Read the AMS telemetry back after every assign (inventory assign_spool and
the Configure Slot modal) and toast the outcome: loaded when the tray
echoes the pushed tray_info_idx, a warning when the filament loaded but the
K-profile (cali_idx) did not, or not-confirmed after ~30s. Verification
uses the periodic per-tray push (the command ack hardcodes sequence_id 0
and can't be correlated); an on-demand pushall is nudged so it lands
quickly. Covers regular AMS, AMS-HT and external slots; stays silent rather
than inventing a failure if the printer goes quiet. The read-back check
runs on every AMS push because the change-hash excludes tray_info_idx.
Since #2562, a Bambu Cloud sign-in flipped to "expired" and forced constant
re-logins even while cloud features worked. #2562 made a 401 durably record
the stored token as dead, but treated *any* 401 from any cloud/MakerWorld call
as expiry. Bambu 401s for benign reasons (endpoint/region/scope refusals,
Cloudflare edge, transient blips), so one stray 401 -- including from a
background poll -- signed the whole cloud integration out until manual re-login.
The flag lives in the DB, so a setup with more than one instance against the
same database signed the user out across all of them.
Invalidate only on Bambu's documented expiry body {"code":4,"error":"Please
login."}. A plain/unparseable 401 is treated as transient: the request fails
but the session stays signed in. validate_token maps a signature-less 401 to
None (unknown), never expired. A shared is_expiry_401() gates both the Bambu
Cloud and MakerWorld services (same token). Genuine expiry is still detected
and surfaced exactly as before.
Bed levelling, flow calibration, and nozzle-offset calibration were on/off
only, so the sole way to run bed levelling was to force a full level before
every print. Bambu Studio has always offered a third "Auto" state that lets
the printer skip the calibration when it was done recently -- the state most
users actually want. Make these three options tri-state (off/on/auto),
defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off
(Bambu Studio exposes no auto for those).
Wire encoding follows Bambu Studio's source exactly: each option sends a JSON
bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The
bool fields stay booleans (the #1478 H2S regression); only the companion int
widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays
queued when sending 2 is the auto contract (queued, skipped at runtime if
recent), not a broken "off".
- schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing
legacy bool / 0-1 / true-false so old clients and un-migrated rows validate
- model + migration: boolean columns -> String; SQLite via column affinity +
data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema
(verified on both dialects); settings rows normalised true/false -> on/off
- MQTT: start_print takes the tri-state strings and emits the paired bool+int
- Virtual Printer: reconstructs the slicer's auto/on/off from the int companion
(auto_bed_leveling / extrude_cali_flag) in both capture paths
- frontend: CalibrationMode type; off/auto/on segmented controls in the print
dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings
in all 11 locales
Focusing any text field on a SpoolBuddy screen (inventory Search, or the
Search / Color Name / Brand fields on write-tag New Spool) blanked the UI
with React error #130 ("Element type is invalid ... but got: object"). It hit
both internal and Spoolman inventories, so it was not data-specific.
The SpoolBuddy shell mounts VirtualKeyboard, an on-screen keyboard that pops up
on focusin for any input -- so every field on every SpoolBuddy page tripped it,
while the main app (no on-screen keyboard) was fine. VirtualKeyboard imports the
default export of react-simple-keyboard, a CommonJS package; under the current
bundler's CJS->ESM interop that default resolves to the module namespace object
({ KeyboardReact, default }) rather than the component, so <Keyboard> renders an
object as an element type and React throws. vitest's interop returns the real
component, so it only manifested in the browser build -- a runtime, not a type,
problem.
Add a small resolveInteropDefault helper that unwraps such an interop-wrapped
default: it returns the value as-is when already a usable element type
(function/class, tag string, or a $$typeof-marked forwardRef/memo/lazy) and
otherwise falls through to .default and named exports. VirtualKeyboard resolves
the real component through it.
The /overlay/{id} route renders without a login, but everything it draws is
auth-gated: printer status and name (PRINTERS_READ), one setting (SETTINGS_READ),
and the camera stream (a camera-stream token). A signed-in browser rides its JWT
from local storage; OBS is a fresh browser with no session, so the overlay stayed
blank whenever authentication was enabled. Cloudflare/remote access was never the
cause -- an incognito window fails identically.
Give the overlay a self-contained kiosk-token mode, mirroring the Cam Wall:
- New `overlay` long-lived-token scope, kept separate from `camwall`: the overlay
names the printed file on screen, which a Cam Wall token is trusted never to
expose, so folding it in would silently widen every existing wall token.
- New token-authed GET /printers/{id}/overlay-status returning exactly the fields
the overlay draws and nothing else; added to the auth-middleware allowlist so it
reaches its own RequireOverlayTokenIfAuthEnabled gate.
- StreamOverlayPage reads ?token= and, in that mode, authenticates its status and
camera calls with the token and skips the WebSocket (the 2s poll is the feed).
The logged-in path is unchanged.
- Token-mint UI (Settings > API Keys) offers the scope with a ready-made
/overlay/{id}?token= URL copied once on creation.
A queue row stays status='pending' for the whole FTP upload; status only flips
to 'printing' at the end. The edit routes only blocked non-pending rows, so a
PATCH during the upload window was accepted while the in-flight dispatch kept
using its snapshotted printer -- splitting the queue row from the archive /
expected-print / physical command across two printers, and enabling a duplicate
dispatch on restart. The #1853 CAS guards cancellation, not reassignment.
Add a dispatching_at claim, stamped atomically (WHERE status='pending' AND
dispatching_at IS NULL) before any slow I/O and cleared on every exit. While
held, the single-item PATCH returns 409 (re-checked just before the write),
bulk edits skip the row, and the scheduler won't re-select it. Startup
reconciliation clears claims orphaned by a crash mid-dispatch. The row stays
pending throughout, so no status/UI/completion/reconciliation path changes.
New column print_queue.dispatching_at (nullable, dialect-safe DDL). Covered by
scheduler tests (claim exclusivity, non-pending rejection, release-on-exit,
skip-already-claimed, startup stale-clear) and API tests (reassign 409,
printer_id unchanged, bulk skip, unclaimed row still edits).
A single plate dispatched from a multi-plate 3MF could log the entire file's
filament against that one plate. When the AMS tracker measured nothing, a
completed run's PrintLogEntry.filament_used_grams fell back to
PrintArchive.filament_used_grams -- the sum over every plate (correct for the
archive card / project rollup, #1593) -- ignoring the archive's plate_id. So
each printed plate of a 22-plate file logged the full ~12 kg; cost inherited
the same whole-file value.
Forward: when the archive has a plate_id and its 3MF is on disk, the completed-
run fallback uses that plate's own slicer estimate (extract_plate_metadata_from_3mf)
and scales cost by the plate's share of the whole. Tracker-measured runs and
single-plate archives are unchanged.
Backfill: a startup migration repairs rows already written -- completed entries
whose stored grams exactly equal the archive's whole-file value, with a plate_id
and an on-disk 3MF, get recomputed to plate-scoped grams + cost. The exact-match
guard never touches tracker-measured or partial rows; idempotent, data-only,
identical on SQLite and Postgres, and logs the correction.
The dispatch progress toast is a fixed 420px wide and the toast viewport is
anchored 80px from the right (to clear the bug-report bubble). On a 390px-wide
phone that overflows the left edge by ~110px, so in the Home-Screen PWA the
toast was clipped off the left, with text bleeding past the edge.
Cap every toast to a viewport-relative max-width (calc(100vw - 6rem - safe-area
insets)) so it can't exceed the screen; desktop keeps the 420px. Make the
viewport position safe-area-aware (env(safe-area-inset-*) on bottom/right) so an
installed PWA clears the home indicator and a landscape notch, and add
min-w-0/shrink-0 to the per-job filename row so long names truncate instead of
widening the toast at the narrower phone width.
Frontend-only; no backend, schema, or i18n change. Covered by a test pinning the
width cap; the suppression test's viewport lookup moved to a stable data-testid.
Server-side slicing always applied the picked printer/process/filament
triplet via --load-settings, which overrides the designer's embedded
project_settings.config — so a MakerWorld model set up for 5 walls came
out at the picked profile's default 2. That override is correct for
re-slicing a design onto your own printer/AMS, but there was no way to
slice a file the way its author configured it.
SliceModal now offers a "Use the file's built-in settings" checkbox when
the source 3MF carries embedded settings AND the picked printer matches
the design's target model. It routes to the existing embedded-settings
slice path (previously only a crash fallback), so walls/infill/filament
come from the file. Ticking it locks all four preset dropdowns — printer
included, since it's unused on this path and changing it would drop the
match and hide the toggle. The printer-match gate stops embedded settings
being honoured across models (wrong bed); there is no cross-printer
re-targeting on this path.
- schema: use_embedded_settings on SliceRequest
- route: embedded_mode branch; crash-fallback guarded against re-running
- frontend: gated checkbox locking all four dropdowns, resets on mismatch
- 2 i18n keys across all 11 locales
- tests: backend (flag skips triplet / ignored for STL) + frontend
(toggle offered on match, locks dropdowns + sends flag / hidden on mismatch)
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1
in Print History after cancellation: the archive derives its plate from the
filename, but a whole multi-plate 3MF uploads under one name with no plate
suffix, so the parser defaulted to plate 1 and nothing copied the queue
item's plate_id onto the archive (which had no plate field).
Add a nullable print_archives.plate_id, copy it from the queue item at
dispatch (archive- and library-file paths), expose it in the archive API,
and render it in Print History. A startup backfill copies the plate onto
existing archives from their linked queue rows. Column add + backfill are
identical on SQLite and Postgres.
Also fix a related lifecycle bug: stopping a printing item while the printer
was offline left the linked archive stuck at "printing" (queue row
cancelled, but no MQTT completion ever arrives to reconcile the archive).
The offline-stop path now closes the archive out directly; the online path
still defers to the MQTT completion event.
check_queue awaited asyncio.gather() over the whole selected batch before
returning, so the scheduler run loop was blocked until the slowest FTP
upload in the batch finished. On a large farm a 513s upload left 15 of 16
configured upload slots idle for 8.5 minutes while other printers came
free — the setting behaved as a per-batch cap, not a worker pool.
Launch uploads as independent background tasks tracked in a _inflight pool.
Each tick excludes in-flight item rows and their printers from selection,
launches at most limit - len(_inflight) new uploads, and returns
immediately, so a freed slot refills on the next fast tick. The no-double-
dispatch invariant the batch-await provided (rows stay pending until upload
completes) is now carried by the in-flight exclusion; the pending->printing
CAS, busy-printer guard (#2598), per-printer hold, auto-drying exclusion,
and per-item failure isolation are all preserved per task.
Rewrites the concurrent-dispatch tests around pool/reservation/refill
semantics and adds coverage for slot refill, in-flight exclusion, and the
non-blocking return.
The Configure AMS Slot modal sends built-in / local / Orca-generic presets
with a GF* tray_info_idx but an empty setting_id, and configure_ams_slot
forwarded that empty value to ams_filament_setting. The firmware treats a
filament-id-without-setting-id slot as half configured: it shows the new
material briefly, then reverts to its previously stored profile.
Back-fill setting_id from the resolved tray_info_idx via
filament_id_to_setting_id when the client sent none (e.g. GFB99 -> GFSB99),
mirroring the derivation the inventory/assignment path already does. Doing
it server-side also protects API callers and future frontends. P* user
presets and already-GFS* values are left unchanged, and an explicit
setting_id still passes through untouched.
The AMS merge clears a tray on a partial {id, state} update when state != 11
(the 4-slot AMS "emptied slot" signal, #784). An AMS-HT (single-tray high-temp
dry box, id >= 128) reports its loaded tray as state=9, so the partial the
printer sends on power-on was misread as "emptied" and wiped the HT-A spool's
tray_type/RFID/assignment seconds after power-on.
Skip the state-heuristic for HT units (id >= 128). Genuine HT removal still
clears via the explicit tray_type="" update and tray_exist_bits cleanup;
regular AMS (id < 128) is unchanged.
start_print() published project_file guarding only on connection state, so a
re-dispatch onto a printer that had already started — e.g. a watchdog revert
(#2555) after the printer sat in FINISH past accepting the job — collided with
the live print. The firmware answers 0500_4004 ("Device is busy and cannot
start a new task"), which on an A1 mini cancels the running job.
Defense-in-depth at the paths that can reach a busy printer:
- bambu_mqtt: refuse to publish project_file when gcode_state is
PREPARE/SLICING/RUNNING/PAUSE and return without sending. This is the one
publish choke point every dispatch path funnels through (queue scheduler,
manual start, webhook, Virtual-Printer forward). IDLE/FINISH/FAILED still
start.
- print_scheduler: re-check the live printer state right before the FTP upload
and defer a busy printer (leave the item pending for a later tick) instead of
uploading and dispatching. If the printer goes busy in the upload window and
the start is refused, revert the item to pending rather than marking it
failed — a busy printer is a deferral, not a failure.
A transport-level MQTT QoS-1 replay on reconnect would bypass the client guard,
but the dispatch/watchdog reconnect path already hard-resets the client with a
fresh session, so it has no inflight project_file to replay.
Three more idle-in-transaction / thundering-herd paths from farm testing:
- print_scheduler: _start_print commits before the FTP delete/upload and
_preheat_and_soak commits before the heat-soak wait, so the per-item
session no longer sits idle-in-transaction across preheat + upload.
- cloud/filament-info: rollback the request transaction after the token
read and before the sequential Bambu Cloud calls; single-flight
concurrent misses for the same setting_id through one shared call.
- printers/cover: coalesce identical in-flight cover requests so followers
serve from the cache the leader fills instead of duplicating the
multi-path FTP + 3MF extraction.
Also adds pool_use_lifo (PostgreSQL default on, DB_POOL_USE_LIFO override,
shown in /system/db-pool) so a bursty farm keeps a small hot connection set.