install/docker-install.sh::create_install_dir ran `mkdir -p
"$INSTALL_PATH"` without sudo while DEFAULT_INSTALL_PATH was
/opt/bambuddy, root-owned on every Linux distro. set -e then
aborted the whole script before docker compose could pull the
image — anyone running the documented `curl ... | bash` flow as
a normal user hit this on first install.
Fix: try the unprivileged `mkdir -p ... 2>/dev/null` first so
--path ~/bambuddy, /srv/bambuddy and other writable targets don't
trigger a needless password prompt, then fall back to
`sudo mkdir -p` + `sudo chown -R "$USER:$USER"` only when the
first attempt failed. The chown is load-bearing: without it the
script would later try to write docker-compose.yml + .env into a
root-owned dir as the invoking user and cascade further EACCES
failures.
Not changing the default path: install/update.sh and
install/update_macos.sh both default INSTALL_DIR to /opt/bambuddy,
and install/README.md's update flow documents the same — flipping
the install default to ~/bambuddy without coordinating those
would silently break self-service updates for anyone following
the docs verbatim. The default stays /opt/bambuddy; only the
escalation gap closes.
set -e survives the redirected stderr because the `if !` form is
the documented escape hatch for an expected-failure check.
Smoke-tested writable-target, idempotent-rerun, and the
failing-mkdir-then-sudo-fallback branches.
The makerworld /status, /resolve, and /import handlers passed
current_user directly into get_stored_token / _build_service.
require_permission_if_auth_enabled returns None for API-keyed
callers by design (core/auth.py:1414), so the lookup always
missed even when the key's owner had a stored Bambu Cloud session.
Result: a "requires a Bambu Cloud login" 400 on every API-keyed
import, regardless of the owning account's actual cloud state.
Wire resolve_api_key_cloud_owner (already used by the slice path
in #1182 — slicer_presets.py:491 and library.py:3871) into the
three makerworld routes that read the cloud token. The handler
falls back to the API-key owner via cloud_token_user =
current_user or api_key_cloud_owner, then passes that through.
import_instance also propagates the resolved user to the
owner_id arg on save_3mf_bytes_to_library, so the resulting
LibraryFile.created_by_id reflects the key's owner instead of
NULL.
Fail-closed semantics preserved: resolve_api_key_cloud_owner
already fences on api_key.can_access_cloud, so keys with only
the per-route scope (can_read_status / can_manage_library) still
take the existing "requires Bambu Cloud login" path — no auth
widening.
/recent-imports is unchanged — it only uses current_user as a
permission gate (_ = current_user) and never touches the cloud
token.
Bambuddy's archive cards were blank for every print sliced through the
BS or Orca docker sidecars. The "Some recent prints couldn't be archived
with thumbnails" banner pointed at install step 4 which is unrelated —
that flag only fires on FTP-fetch failures, not on missing-thumb in the
sliced 3MF.
Root cause is upstream of Bambuddy: neither slicer CLI renders
Metadata/plate_N.png when invoked headlessly with --slice --export-3mf.
That render is a separate code path triggered by --export-png, which is
mutually exclusive with --export-3mf and additionally needs a working
display backend (BS 02.07.x's bundled GLFW is hard-locked to Wayland —
even XDG_SESSION_TYPE=x11 + GDK_BACKEND=x11 + QT_QPA_PLATFORM=xcb don't
switch it back). An Xvfb display in the sidecar wouldn't help even if we
wired the second-pass call. The Orca sidecar has been silently shipping
thumbnail-less 3MFs from STL inputs since launch; nobody noticed.
Fill the gap on the Bambuddy side: new plate_thumbnail.py renders the
missing thumbnails after the slice returns. inject_plate_thumbnails_if_missing
parses the sliced zip, finds every Metadata/plate_N.gcode entry that
doesn't have a matching plate_N.png, loads 3D/3dmodel.model via trimesh,
renders an isometric Bambu-green-on-dark view at 512x512 + 128x128 via
the same matplotlib Agg pipeline as stl_thumbnail.py, and re-packs the
zip with the PNGs injected. Visual style matches Bambuddy's existing
library thumbnails — archive cards stay consistent inside Bambuddy rather
than chasing parity with desktop Studio's plate render. Best-effort:
input bytes are returned unchanged on any failure so the slice flow itself
can never fail because of a missing thumbnail. Idempotent: re-running on
an already-injected 3MF returns the input verbatim.
Wired into both library.py slice paths via result._replace; covers the
cross-class merged-multi-plate path automatically (merged bytes flow into
the same write site). No sidecar Dockerfile change required — an earlier
attempt to install Xvfb in Dockerfile.bambu-studio was a false start and
is not part of this drop.
Dependencies: trimesh's 3MF loader uses networkx (scene-graph traversal)
and lxml (model.xml parse) lazily inside the 3MF code path — both added
to requirements.txt because they aren't strict trimesh transitives.
Follow-up to the temperature & fan-speed presets feature — the
TestUiPreferencesEndpoint.test_returns_expected_field_set test pins
the exact set of fields the endpoint exposes (so adding a sensitive
field by accident fails the assert). The 4 preset fields were added
to _UI_PREFERENCE_FIELDS without updating the pin, breaking the full
backend test run.
- The Speed and AMS load/unload tests broke after the printer-card
refactor in #1661 (icon-only Gauge button replaced the "100%" badge,
hover-card actions replaced the kebab "Slot options" button). Add
data-testid="speed-control" + data-testid="filament-slot" as stable
test hooks, rewrite both files around them. Parametrize the four-mode
speed-selection test. Update the "RUNNING hides menu" assertion to
"Load/Unload buttons exist but are disabled" — the new UX shows
actions on hover and disables them rather than hiding the trigger.
- Drop `animate-in fade-in-0 zoom-in-95 duration-150` from
FilamentHoverCard and EmptySlotHoverCard. The card briefly painted
at the offscreen (-9999, -9999) coords during the zoom-in transition,
reading as a fly-in from the upper-left corner. With the animation
gone it just appears in place at its computed position.
PR #1661 swapped the visible speed badge ("100%") for an icon-only Gauge
button and replaced the kebab "Slot options" button with hover-card
actions inside FilamentHoverCard. The two existing test files weren't
updated alongside the refactor and stayed broken on dev.
- Add data-testid="speed-control" to the Gauge button and rewrite the
Speed tests around it; assertions on the percentage text are gone
because that text no longer renders. Parametrize the four-mode API
call test.
- Add data-testid="filament-slot" to FilamentHoverCard's trigger
wrapper and rewrite the AMS load/unload tests around
fireEvent.mouseEnter → portaled actions. Replace the "hides menu
while RUNNING" assertion with "Load/Unload buttons exist but are
disabled" — matches the new UX shape.
The matcher's tray_info_idx vs color vs type_only bucket choice was
debug-only, so a bug report's bundle never showed which path won. Emit
the sorted candidate trays and the picked bucket per filament req when
prefer_lowest=True. Behaviour-neutral; existing 89 matcher tests pass.
GET /api/v1/printers/ and /api/v1/printers/{id} return access_code
only when the caller holds PRINTERS_UPDATE. Adds PrinterResponseWithSecret
as the elevated response shape; PrinterResponse no longer carries the
field. Auth-disabled single-trust mode preserved.
The Slicer -> Local Profiles page kept showing a just-deleted row for
the ~hundreds of ms it took invalidateQueries to refetch. A quick
re-click on the same row opened a second delete-confirm modal that
resolved to a 404 from the backend.
Add an optimistic queryClient.setQueryData filter in deleteMutation's
onSuccess so the row disappears the instant the DELETE returns 200.
Existing invalidateQueries calls stay in place to reconcile any drift.
Found while reproducing #1713 (verifying maziggy's setup against the
reporter's). Unrelated to that investigation but caught here.
Major version bump for the frontend build:
- vite ^7.3.2 -> ^8.0.16
- @vitejs/plugin-react ^5.1.1 -> ^5.2.0
Vite 8 swaps Rollup for Rolldown as the default bundler
(Rust-backed, same plugin contract). The bump also lifts the
transitive esbuild floor to 0.28.1, closing the last open
advisory in the audit chain.
vite.config.ts surface audited and unchanged:
- defineConfig, Connect type
- serveGcodeViewer configureServer middleware
- server.proxy with WebSocket upgrade for /api/v1/ws
- build.outDir / emptyOutDir / chunkSizeWarningLimit
- resolve.alias for @
- base: '/' regression guard from #1221
vitest@4.1.8 already accepts vite 8 in its peer range
(^6 || ^7 || ^8); no test-runner bump required.
Node floor for vite 8 is ^20.19.0 || >=22.12.0; CI Node 20.x
line satisfies this.
Not taken: plugin-react v6 — it requires
babel-plugin-react-compiler and @rolldown/plugin-babel as
peers and is a separate scope.
The 24h session cap from the M-2 audit finding was hard-coded, so the
"Remember Me" checkbox could only control storage location, never
duration. Add session_max_hours setting (default 24, max 720) honoured
at all four token-issuance sites: plain login, 2FA TOTP/email, 2FA
backup, OIDC.
- backend/app/core/auth.py: SESSION_MAX_HOURS_HARD_CEILING + resolver
that clamps to [1h, 720h] and falls back to 24h on missing/blank/
unparseable. DB errors propagate — the login transaction must abort
on a broken DB rather than silently extend or shrink the lifetime.
- backend/app/api/routes/auth.py, mfa.py: all four sites read the
resolved value instead of ACCESS_TOKEN_EXPIRE_MINUTES directly.
- backend/app/schemas/settings.py, routes/settings.py: schema field
with ge=1 le=720 + int coercion in _build_settings_response.
- frontend/src/pages/SettingsPage.tsx: half-width card at top of
Settings -> Users left column with 24h/7d/30d presets, custom input,
and a yellow warning when value > 24h.
- frontend/src/i18n/locales/*.ts: 8 new keys per locale, real
translations in all 11 (en/de/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW).
- backend/tests/integration/test_session_policy.py: 15 tests across
resolver clamping, login JWT exp end-to-end, settings API round-trip.
Already-issued tokens keep their original expiry; the new setting only
affects future logins.
The SpoolBuddy inventory page reimplemented its filter inline and only
matched material/subtype/brand/color_name/note, while Bambuddy's main
inventory uses the shared filterSpoolsByQuery helper which also matches
spool ID, slicer_filament_name, and storage_location. Delegate to the
shared helper so both pages stay in lockstep.
- frontend/src/pages/spoolbuddy/SpoolBuddyInventoryPage.tsx: replace
inline filter with filterSpoolsByQuery
- frontend/src/__tests__/pages/SpoolBuddyInventorySearch.test.ts: lock
in ID / partial ID / pre-fix fields / parity-gain fields
Restructures the queue page around three tabs (Queue / History / Timeline)
and adds first-class batch grouping plus a real time-based timeline.
Queue tab
- Layout toggle: Sort by Position (flat list) or Group by Printer (per-
printer section cards with aggregate count / time / weight headers).
- Batch grouping: pending items sharing a batch_id render as a single
collapsible row with aggregate stats; children draggable within the
batch only. Per-batch collapse state in localStorage.
- Multi-drag: dragging any selected row moves all selected items as a
contiguous block via DragOverlay (+N ghost).
- Selection bar gains a Group as batch action when 2+ ungrouped items
are selected. Ungroup lives on the batch parent row.
History tab
- Two-line rich rows: filament color swatch + weight + type, user
attribution, inline error message on failed / skipped rows.
- Responsive 1 / 2 / 3 column grid so a long history uses available
width instead of stretching one row per line.
- Batch siblings group into a collapsible parent with status-rollup
chips (3 OK / 1 failed / etc).
- Thumbnail hover preview shows the full image at 192x192 next to the
small thumb.
Timeline tab
- Replaces the hourly-list view with a Gantt swimlane: one row per
printer (plus per target_model and unassigned), horizontal hour
axis, jobs as bars positioned by start time and sized by duration.
- Live NOW marker.
- Only committed schedules are rendered: currently printing items,
pending items with scheduled_time, and pending ASAP behind an active
print. Staged (manual_start), waiting (waiting_reason), and ASAP
jobs on idle printers are filtered out.
- 24h rolling window with 12h step controls.
- Per-bar tooltip with start, end, progress, batch name.
Backend
- POST /queue/batches creates a batch, optionally assigning existing
pending item_ids (manual grouping) or returning an empty batch the
client can attach to subsequent /queue/ POSTs.
- POST /queue/batches/{id}/ungroup clears batch_id from all members
(skipping items the caller does not own) and deletes the batch row
when no members remain.
- POST /queue/ accepts an optional batch_id and validates that the
batch exists, is active, and the caller may modify it. The existing
quantity > 1 auto-batch path still fires when no batch_id is sent.
PrintModal
- When N plates from one source are queued in a single submission
(model assignment or single printer), the modal pre-creates a batch
and passes its id to each addToQueue call so multi-plate jobs land
grouped automatically. Falls back to ungrouped items if the batch
pre-create fails.
navPermissions in Layout.tsx gated three resources on the LEGACY *:read flag.
Default Operators group is seeded with *_own only (and the migration map flips
legacy → _own on existing groups), so non-admin users never held the legacy
permission and the sidebar hid Archives / Queue / Files even though the
underlying API accepted their requests. Reporter only spotted Files; same bug
shape applied to Archives and Queue.
Fix: navPermissions accepts Permission | Permission[]; the three affected
resources list all three tiers. isHidden checks .some(hasPermission) for
arrays. Permission type extended with the matching *_own / *_all variants —
backend already shipped them, the TS type just didn't declare them.
The provider toggle, schema, template, and NotificationService.on_printer_offline
all shipped, but no caller invoked the dispatcher — the offline event was an
orphan toggle. Edge detection in on_printer_status_change now schedules a
debounced (60s) background task on the connected→disconnected transition;
reconnect before the window elapses cancels it. Covers both upstream paths
(smart-plug power-off via mark_printer_offline, and MQTT staleness via
check_staleness), both of which already route through the status callback.
The "back online" channel is the existing print-failure notification on
firmware FAILED report — no symmetric on_printer_online needed.
ProtectedRoute and PermissionRoute now pass the requested location as
router state when redirecting to /login. LoginPage stashes it in
sessionStorage before the OIDC provider redirect (since window.location
kills React state) and consumes it on all three post-login navigations
(credentials, 2FA, OIDC token exchange). Targets are sanitized to
same-origin internal paths only — protocol-relative and /login itself
are rejected to prevent open-redirect.
QR labels (https://host/inventory?spool=N) now land on the scanned
spool instead of the printer page after authentik / any OIDC SSO login.
Older print_archives rows (and rows that landed via the SQLite ↔ Postgres
cross-DB restore path) can have created_at = NULL because the column was
originally created without a DEFAULT clause — server_default=func.now()
only fires at table creation, not for existing rows or raw cross-DB
inserts. The list_archives response model required a datetime, so a
single NULL row 500'd the whole endpoint via Pydantic ResponseValidationError.
- Boot-time backfill: COALESCE(completed_at, started_at, now()) for
any row where created_at IS NULL. Dialect-branched (SQLite datetime('now')
vs Postgres NOW()).
- Schema: created_at is now Optional on ArchiveDuplicate, ArchiveResponse,
and ArchiveSlim so a future NULL-leaking path doesn't break the list
endpoint again.
**Bambuddy 0.2.4.7**
**⚠ Upgrade Notes — Read Before Updating**
0.2.4.7 is a fix-led patch release on the same 0.2.4 code base — no schema breaks beyond auto-migrated column additions (dialect-branched for SQLite and Postgres), no Docker entrypoint changes. The in-app Apply Update button in Settings → System → Updates works for Docker and for any native install already on 0.2.4.x.
Four behavior-change callouts to know about before you upgrade:
- Capture-Finish-Photo on the printer side is no longer force-toggled at dispatch (#1721, reported by @agrisci). The earlier workaround force-enabled the printer-side setting on every print to drive the finish-photo capture; the side effect was that intermediary progress notifications fell silent on A1, and the slicer's own Capture Finish Photo checkbox was silently overridden. Dispatch no longer flips the printer-side setting. Finish-photo capture is now driven by Bambuddy's own stage-22 pre-capture path with a FINISH-state fallback. If you previously unchecked Capture Finish Photo in the slicer to dodge the side effect, you can re-enable it. If you relied on the force-on to get finish photos without slicer changes, the FINISH-state fallback covers it — no UI change required.
- Slicer Bundle (.bbscfg) import removed (#1712, reported by @IndividualGhost1905). The legacy .bbscfg import path on the Slicer page is gone — it silently underdelivered on multi-tier preset matching and conflicted with the Orca Cloud / Bambu Cloud precedence work. Use the cloud sync (Orca Cloud or Bambu Cloud) or local imported presets instead. The SliceModal preset picker now reaches across Imported → Orca Cloud → Bambu Cloud → Standard with proper precedence and cross-tier dedup.
- Bambu Lab A2L support added (#1684). New "A2 Series" optgroup in the Add-Printer / Edit-Printer dropdowns and across the SpoolBuddy / inventory surface. The connection diagnostic, AMS slot routing, and camera path all auto-shape themselves to A2L's hardware (Wi-Fi-only, chamber-image protocol on port 6000 instead of RTSPS:322, single-extruder + cutter/plotter head with no deputy-slot routing). Existing X1 / H2 / P1 / P2 / A1 surfaces are unchanged. If you're on an A2L: re-test Configure AMS Slot — the picker now filters profiles to A2L-compatible filaments (#1623 fix below).
- Windows installer pipeline overhauled. The installer is self-versioning (filename matches the release tag, plus an unversioned alias on stable / beta), ships its own NSSM binary instead of fetching at build time, stops the Bambuddy service before file copy on upgrade, and bootstraps setuptools + wheel into its embedded Python so the post-install dependency install doesn't fail on a clean machine. Existing Windows installs upgrade in place via the Service / Update entries on the new installer; first-time installs no longer depend on a network trip to a flaky NSSM mirror.
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 for 0.2.4.7. (If you map the VP passive FTP port range or run the slicer-API sidecar, the 0.2.4.6 upgrade notes still apply —
nothing new in 0.2.4.7.)
**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
requirements.txt bumps the aiohttp floor to >=3.14.0 this release to clear two upstream advisories. No code change inside Bambuddy — only the floor moves so fresh installs and CI pick up the fix.
**Windows install**
0.2.4.7 ships a rebuilt Windows installer with the pipeline improvements from the Upgrade Notes. Download bambuddy-0.2.4.7-windows-x64-setup.exe from this release page (or the unversioned bambuddy-windows-x64-setup.exe alias for an always-latest link). Existing 0.2.4.5 / 0.2.4.6 installs upgrade in place — the installer stops the Bambuddy service, swaps files, restarts the service, and preserves your data directory.
---
**Highlights**
0.2.4.7 is a heavy fix-cycle release with one big add — Bambu Lab A2L support (#1684) — and a long tail of contributor-credited fixes across the Virtual Printer, Slicer, Print Queue, and connection-diagnostic surfaces.
The Virtual Printer surface got another full sweep driven by #1622 telemetry. The bridge cache now accumulates push_status per-field instead of replacing fields on each push (round 4), overlays incoming dict-shaped fields onto the cache instead of wholesale replacement (round 5, @shaddowlink), and applies the tray_exist_bits empty-slot cleanup to the slicer-facing cache (#1726, @needo37 with full code-level analysis). Net effect: BambuStudio's Device tab no longer greys out between pushalls, and Sync no longer sees phantom-loaded filaments in empty AMS slots. Three env-flagged debug paths landed in the same cycle (wire-payload dump, command-flow trace, bridge-synthesised-reply trace) and stay silent in normal operation.
Slicer (SliceModal) is the second-largest theme: full preset-lookup precedence rework + cross-tier dedup + signed-out banner + AMS slot badges (#1712, @IndividualGhost1905). Follow-ups in the same train fix the Orca Cloud / Bambu Cloud preset resolver to pin type and from to CLI-accepted values for the headless slicer-api sidecar, and the Library G-code preview now correctly renders sidecar-sliced .gcode.3mf rows as G-code instead of returning raw ZIP bytes as text/plain
(#1709, root cause + fix from @yanglei1980).
Print Queue + Archive polish: multi-plate Send All now enqueues one queue item per plate instead of one item per click, archive delete cascades to remove related queue items instead of leaving "cancelled" rows behind, the force-color-match checkbox is no longer missing when scheduling against a specific printer (#1717, @SamNuttall), and the filament-override panel surfaces Bambu Studio's sub-brand colour name instead of the raw 3MF base material (#1718, @SamNuttall). Multi-color filament rows in the Print Log render one swatch per colour instead of a single barely-visible gray dot (#1731 part 1, @IndividualGhost1905). Telegram (and other image-bearing) finish notifications on a reprint-from-archive now ship the new run's finish photo instead of the original print's (#1707, @kycrna).
Windows + restore reliability. Beyond the installer pipeline overhaul: /api/local-backup/status no longer 500s on ZoneInfoNotFoundError: 'No time zone found with key UTC' from a missing zoneinfo DB on the Windows installer (a stdlib UTC fallback covers it). Network-interface enumeration on Windows now uses psutil instead of the Linux-only path. Restore pauses timer-based DB writers before the swap, fixing a Postgres deadlock cascade observed during multi-printer restores.
---
**New Features**
- Bambu Lab A2L support (#1684). New "A2 Series" optgroup in Add-Printer / Edit-Printer dropdowns, with full capability resolution from BambuStudio's machine profile cross-checked against Bambu's official A2L specs page: linear rail, single FDM extruder + integrated cutter/plotter head, Wi-Fi-only (no Ethernet), Low-Rate-Kamera on the chamber-image protocol (port 6000, not RTSPS:322), no heated chamber. The dual-tool-head capability is correctly distinguished from dual-filament extrusion — A2L is not in DUAL_NOZZLE_MODELS, so AMS does not route to a deputy slot (firmware would reject with 07FF_8012). Registry updates touch utils/printer_models.py, firmware_check.py (wiki path follows the established /en/a2l/manual/a2l-firmware-release-history pattern; existing 404 handling makes this safe to ship before Bambu publishes the page), virtual_printer/manager.py (serial prefix 26A19), virtual_printer/mqtt_server.py, PrintersPage.tsx, and SpoolBuddyAmsPage.tsx. Camera and dual-nozzle code paths need no edits — supports_rtsp() correctly falls through to chamber-image and is_dual_nozzle_model() correctly returns False.
- Re-print / Schedule modal allows cross-extruder AMS slot picks on dual-nozzle (#1722, reported by @privatsturm). The picker previously gated AMS slot choices to the same extruder as the source plate's filament group. On dual-nozzle hardware that's wrong — operators routinely re-route a left-side filament to a right-side AMS slot when the left bank is empty or busy. Fix removes the gate; the MQTT layer's existing dual-nozzle routing handles the rest.
- Support bundle now includes redacted cached push_status per connected printer. The raw push_status payload is the single highest-signal artefact for diagnosing slicer-facing VP issues. The new bundle entry walks the cached payload and scrubs net.info[*].ip plus the documented privacy-sensitive fields before serialising — the live state.raw_data is never mutated. Drops directly into the existing bundle archive alongside the existing log + config dumps.
- One-shot device identification probe for unknown printer models. When Bambuddy sees a serial prefix it doesn't recognise on the MQTT bus, it now fires a single push_all-only probe to surface the model code, instead of either silently dropping the connection or polluting the log with repeated unknown-model warnings. Aids future model rollouts (next H2 / A2 / X2 family variant).
---
**Changes**
- VP MQTT bridge cache shape rework (#1622 rounds 4 + 5). The cache now accumulates push_status per-field instead of replacing fields on each incoming push (round 4, fixes Device-tab greying), and overlays incoming dict-shaped fields onto the cache instead of replacing the whole sub-object (round 5, @shaddowlink, fixes vt_tray going "invalid" right after a slicer filament pick). Field accumulation is bounded by the field allowlist documented in the VP regression matrix.
- SliceModal preset-lookup precedence + cross-tier dedup + signed-out banner + AMS slot badges (#1712, reported by @IndividualGhost1905). The 4-tier picker (Imported → Orca Cloud → Bambu Cloud → Standard) now enforces the documented precedence end-to-end with explicit dedup on key collisions, surfaces a signed-out banner per cloud
tier instead of silently empty lists, and renders AMS slot badges next to the preset row so operators can see at a glance which slot the preset would land in. The legacy .bbscfg import path was dropped in the same train — see Upgrade Notes.
- Print-modal "off" toggles for flow_cali and nozzle_offset_cali now actually suppress the calibration stage. Live-tested on H2D 01.x. The previous behaviour wrote the toggle value into the project_file payload but the firmware still ran the stage; fix routes through the existing dual-nozzle gate and the calibration-suppress field at the MQTT layer.
- Support-bundle log noise demoted (#1721 adjacent, observed on the reporter's A1). Benign "not connected" and "may linger" warnings are now INFO-level so the bundle log dumps surface real problems instead of background heartbeat noise.
- aiohttp pinned to >=3.14.0 in requirements.txt for two upstream advisories. Bambuddy's aiohttp usage was unaffected by the underlying issues, but the floor moves so
fresh installs and CI pick up the fixed runtime.
---
**Fixed**
**UI / rendering**
- Print Log multi-color filament rows now render one swatch per colour instead of a single barely-visible gray dot (#1731 part 1, reported by @IndividualGhost1905). The renderer was averaging the multi-colour RGBA into a single greyscale fallback; the fix splits on the slicer's pipe-separated colour list and renders a small swatch row.
- Stats page Failure Analysis widget now renders translated failure reasons instead of raw camelCase keys (#1687 follow-up, reported by @IndividualGhost1905). The GET serialiser was dropping the canonical failure_reason mapping; the fix re-applies the same vocabulary the Archive Edit modal uses.
- System page boot time no longer renders with a doubled timezone offset (#1690 follow-up, reported by @IndividualGhost1905). The recorder was emitting a tz-naive datetime that the frontend then localised on top; both boot_time and generated_at now ship as tz-aware UTC.
- AMS slot card stays in sync with the new spool's preset name after RFID auto-assigns a new spool. Reporter observed H2D-1 / AMS-B3 / PLA-CF rendering as "Bambu PLA Silk+" until manual refresh. The fix invalidates the AMS slot's cached preset-name lookup on the same event that updates the spool binding.
**Virtual printer**
- Empty AMS slots no longer forwarded as phantom loaded filaments to BambuStudio Sync (#1726, reported with full code-level analysis by @needo37). The tray_exist_bits empty-slot cleanup now applies to the slicer-facing cache in addition to the live state — Sync sees the same empty-slot picture the real printer reports.
- The vt_tray external-spool object no longer goes "invalid" immediately after a slicer filament pick (#1622 round 5, reported by @shaddowlink). The cache was replacing the whole vt_tray sub-object on each push, dropping the slicer's just-picked state; the overlay rework preserves field-level updates.
- VP cache no longer drains capability / lifecycle fields between pushalls, which had greyed out Device-tab UIs (#1622 round 4, reported by @shaddowlink). The per-field accumulation fix from the Changes section.
**Print queue + dispatch + archive**
- Multi-plate Send All now enqueues one queue item per plate instead of one item per click. The route was accepting the plate count from the UI but the queue-insert was firing once per request; fix iterates the plate set on the server side.
- Archive delete now removes related queue items instead of leaving "cancelled" rows behind. The cascade was already declared at the schema level but the DELETE route was using a soft-delete path that bypassed it; fix routes the archive delete through the same cascading path the bulk-delete already used.
- Finish-photo capture: dispatch no longer force-toggles the printer-side Capture Finish Photo setting (#1721, reported by @agrisci). See Upgrade Notes for the full behaviour change.
- Telegram (and other image-bearing) finish notification on a reprint-from-archive no longer ships the original print's finish photo instead of the new run's (#1707, reported by @kycrna). The notification was reading the finish-photo path from the source archive instead of the new archive row.
- Print Queue filament-override panel now shows Bambu Studio's sub-brand colour name instead of the raw 3MF base material (#1718, reported by @SamNuttall). The panel was reading directly from the 3MF metadata instead of the resolved Bambu profile.
- Force-color-match checkbox no longer missing when scheduling against a specific printer (#1717, reported by @SamNuttall). The Charcoal-style label fix from earlier in the queue cycle was missing on the Specific-Printer panel; this also extends to a related label-fix follow-up (#1718 round 3).
**Inventory / AMS / connection diagnostic**
- Configure AMS Slot picker now filters filament profiles to the printer model (#1623, reported by @shaddowlink). The list was showing every imported profile regardless of printer compatibility; the fix routes the picker through the same compatibility filter the SliceModal uses.
- Connection diagnostic no longer flags external_storage: fail on A1 / A1 Mini (#1703, reported by @MartinNYHC). A1 and A1 Mini have no MicroSD slot; the diagnostic now skips the check for those models with a clear "n/a — printer has no SD slot" surface.
- A1 / A1 Mini internal-code map was swapped in PRINTER_MODEL_ID_MAP (surfaced while scoping A2L support, #1684). The swap had no user-visible symptoms but corrupted the printer-model resolver for any code path that round-tripped through the ID map.
**Slicer / library**
- Library G-code preview returned raw ZIP bytes as text/plain for sidecar-sliced .gcode.3mf rows (#1709, root cause + fix from @yanglei1980). The preview route was branching on filename suffix but the sidecar produces .gcode.3mf — a ZIP wrapper around a .gcode payload. Fix extracts the inner .gcode before serving and pins the Content-Type to text/x-gcode.
- Cloud + Orca Cloud preset resolver now pins type and from to CLI-accepted values (#1712 follow-up, reported by maziggy on a Mecha Mewtwo slice). The resolver was passing the user-tier-as-displayed string ("orca_cloud") to the headless slicer-api CLI, which only accepts a fixed enum; fix maps display tiers to CLI tiers explicitly.
**Windows / install / restore**
- /api/local-backup/status no longer 500s on ZoneInfoNotFoundError: 'No time zone found with key UTC' (from a user's log on the Windows installer). Some Windows hosts ship without the IANA zoneinfo database; the fix falls back to the stdlib UTC implementation when the IANA lookup raises.
- Network-interface enumeration on Windows now uses psutil instead of the Linux-only socket path. Affected the Add-Printer custom-subnet picker and any path that listed local interfaces.
- In-app updater now routes every git step through app_dir for separate-mount installs (#1715, reported by @francescocozzi). On installs where /opt/bambuddy/data is a separate mount from /opt/bambuddy, the updater's git commands ran from the data dir and failed silently on the .git lookup; fix explicitly passes -C app_dir.
- Restore now pauses timer-based DB writers before swap, fixing a Postgres deadlock cascade. Multi-printer restores were triggering concurrent timer writers against the same connection pool as the restore transaction; fix gates the timer loop on a "restore in progress" flag.
The two "0.0.0.0" comparisons in test_support_helpers verify the
support-bundle net.info[*].ip redaction sentinel (mirrors the
support.py:1193 annotation), not a socket bind. Annotate inline.
The "0.0.0.0" written into the support bundle is a JSON sentinel that
scrubs the printer's local IP plus the gateway/peers it sees — not a
socket bind address. Annotate inline so bandit stops flagging it.
The "0.0.0.0" written into the support bundle is a JSON sentinel that
scrubs the printer's local IP plus the gateway/peers it sees — not a
socket bind address. Annotate inline so bandit stops flagging it.
VP queue-mode multi-plate Send All
==========================================
BambuStudio / OrcaSlicer "Send All" of a multi-plate project uploads ONE
3MF containing every plate (one FTP STOR, single filename) — slice_info.config
inside the file lists N <plate> blocks with their own index metadata and
their own Metadata/plate_N.gcode payload. Pre-#1733 the VP queue path
called _extract_plate_id which returned only the FIRST plate index, and
_add_to_print_queue built exactly one PrintQueueItem from it. Plates 2..N
silently dropped on the floor. From the user's perspective: Send All of a
3-plate project produced 1 queue item, indistinguishable from a regular
single-plate Send, with no log line to explain the discrepancy.
The wire was confirmed against the live H2D-1 Proxy VP: the same file
ships whether the user clicked Send or Send All; the only intent signal
is the count of <plate> blocks inside slice_info.config.
Fix: replaced _extract_plate_id (-> int | None) with _extract_plate_ids
(-> list[int]). The list contains every <plate> block's index in order;
falls back to [1] when slice_info.config is missing / unparseable so the
single-plate case is preserved. _add_to_print_queue now loops over the
list and creates one PrintQueueItem per plate, with:
- plate-specific position = MAX(position) + iteration_number, so the
items inherit consecutive positions and the slicer's plate order
becomes the queue execution order.
- per-plate required_filament_types / filament_overrides via
extract_filament_requirements(file_path, plate_id) — the plate-aware
filter shipped with #1697 — so the scheduler's per-printer "Any X"
matching dispatches each plate onto a printer with the right
colours loaded for THAT plate, not for plate 1's filament set.
- shared archive_id across all plates (one upload = one archive row).
- the VP's auto_dispatch + manual_start posture inherited unchanged.
Net behaviour: single-plate Send hits the loop once → exactly today's
result (one queue item, plate_id from the slicer, one archive). Multi-
plate Send All of a 3-plate file → 3 queue items, plate_id 1/2/3,
consecutive positions, all referencing the same backing archive.
Archive delete cascades to queue rows
=============================================
Previously the soft-delete path (the default the trash-can button uses)
called _cancel_pending_queue_items which only flipped queue rows with
status='pending' to status='cancelled' while leaving every other status
alone AND leaving every row in the DB. The Send All multi-plate work
above made this much more visible: deleting an archive backed by N
queue items now had to clean up N rows, and what users saw instead was
N "cancelled" rows lingering in the queue history.
Backend:
- Replaced _cancel_pending_queue_items with _delete_related_queue_items
(db, archive_id) -> int. DELETEs every queue row where
archive_id = X regardless of status. Matches what the hard-delete
path already did via the ON DELETE CASCADE FK on
print_queue.archive_id — both paths now produce the same end state.
- Print history lives in PrintLogEntry (FK ON DELETE SET NULL) and is
untouched; Quick Stats / accuracy bands are preserved across both
delete paths.
- 409 guard on archives.py::delete_archive when any related queue
item is currently status='printing'. Both soft and hard delete are
gated; deleting the archive while a print is live would strip the
dispatcher's metadata trail (filament / plate / ams_mapping) out
from under the running print.
- New GET /archives/{id}/delete-impact endpoint returns
{related_queue_items: N, currently_printing: M}. Cheap, single
endpoint, deliberately NOT folded into the archive list response
so the much larger list endpoint isn't forced to run the same
query per row.
Frontend:
- ArchivesPage delete-confirm modal queries the new endpoint when the
modal opens (useQuery with enabled: showDeleteConfirm) and renders
an amber "N queue items linked to this archive will also be removed"
line when total > 0 AND printing = 0, OR a red "Cannot delete —
M queue items are currently printing" line when printing > 0
(confirm button disabled in that case so the user can't bonk the
409 on submit).
- ConfirmModal gained an optional confirmDisabled?: boolean prop —
isLoading was the only disable knob before; this adds the external-
precondition path.
- 2 new i18n keys (deleteQueueItemsWarning, deleteBlockedByPrinting)
translated across all 11 locales per feedback_translate_dont_fallback —
no English fallbacks.
No DB migration — the CASCADE FK was already in place; only the helper's
semantics changed.
The per-archive Print Log table cell at ArchivesPage.tsx:3882 rendered
filament_color as a single swatch with
`backgroundColor: entry.filament_color.startsWith('#') ? ... : undefined`.
For multi-color prints, the backend writes filament_color as a comma-joined
string ("#FFFFFF,#000000,#FF0000"). The whole string trivially passed the
startsWith('#') check but isn't a valid CSS color — the browser silently
drops the declaration and the swatch falls back to its black/20% border,
which on the dark theme reads as a barely-visible grey dot. Reporter's
screenshots showed "PLA" text with no visible swatch at all. DB column
was correct; render dropped the colors.
The Archive Card view at ArchivesPage.tsx:1072-1083 and :2114-2125 already
splits on comma and renders one swatch per color — only the Print Log
table cell had been missed when multi-color support landed elsewhere.
Fix mirrors the card pattern: wrap swatches in a flex container, split
on comma, trim, render one w-3 h-3 swatch per color with
backgroundColor and title={trimmed}. Single-color prints render one
swatch (no behaviour change). Empty / non-hex entries fall through to
no backgroundColor rather than poisoning the CSS for siblings.
Does NOT cover the reporter's second symptom — new multi-color prints
missing from filament usage history. That's usage_tracker._track_from_3mf
and the slot-to-tray mapping chain; needs a support bundle (PRINT START
+ PRINT COMPLETE [UsageTracker] log lines + captured ams_mapping) before
shape can be confirmed. Tracking as #1731 part 2.