Commit Graph
3357 Commits
Author SHA1 Message Date
BambuMan 5a92115546 feat(api-keys): QR code on key creation encoding server URL + key (#1677) (#1701) 2026-06-19 12:35:39 +02:00
maziggy ceb0616c82 chore(deps): backend security floor bumps + 422 constant rename
requirements.txt
    - cryptography 46.0.7 -> 48.0.1 floor (GHSA-537c-gmf6-5ccf,
      non-contiguous Python buffer handling)
    - python-multipart 0.0.27 -> 0.0.31 floor (CVE-2026-53538/53539/53540,
      multipart parser hardening)
    - starlette 1.1.0 -> 1.3.1 floor (CVE-2026-54282/54283, FormParser
      limit enforcement + StaticFiles absolute-path rejection)
    - pyopenssl 26.0.0 -> 26.3.0 floor (NOT a security fix; pyOpenSSL
      <26.3.0 caps cryptography<47 and would otherwise downgrade out
      of the GHSA-537c-gmf6-5ccf fix line)

  backend/app/api/routes/mfa.py
    - 3x HTTP_422_UNPROCESSABLE_ENTITY -> HTTP_422_UNPROCESSABLE_CONTENT
      (the former is deprecated in starlette 1.3.x, same 422 wire status;
       the 2 remaining warnings are inside FastAPI itself, upstream's)

  Release-notes review done before bump: cryptography 47/48 dropped
  binary EC, CFB/OFB/CFB8, Camellia, PUBLIC_KEY_TYPES/PRIVATE_KEY_TYPES,
  OpenSSL 1.1.x, Python 3.8 -- grep clean against every removed surface;
  starlette's newly-enforced max_part_size=1MB only applies to text form
  fields (verified in MultiPartParser.on_part_data), file streams from
  UploadFile = File(...) are unaffected; python-multipart 0.0.30 dropped
  RFC 2231/5987 filename* parsing, minor cosmetic impact on non-ASCII
  filename uploads, plain filename= fallback still works.
2026-06-19 12:02:30 +02:00
maziggy 9e24d8d297 chore(deps): dompurify 3.4.10 -> 3.4.11 (GHSA-cmwh-pvxp-8882, moderate) 2026-06-19 11:49:53 +02:00
maziggy 9b5cc1b4f1 Post work PR #1516 2026-06-19 11:45:39 +02:00
phieb b414af6b1c feat(gcode-injection): per-VP opt-in auto-print injection toggle (#1516) (#1656) 2026-06-19 11:29:07 +02:00
maziggy fcd43019e3 Updated BACKERS 2026-06-19 08:48:38 +02:00
maziggy 9ca2dd08cf Updated BACKERS 2026-06-19 08:48:13 +02:00
maziggy 1773cbd629 fix(install): docker installer tries mkdir without sudo, escalates on EACCES (#1774)
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.
2026-06-19 08:14:49 +02:00
maziggy 9f8bac63ff fix(makerworld): resolve API-key owner for cloud-token lookups (#1777)
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.
2026-06-19 08:05:20 +02:00
maziggy 3ef5119b7d fix(archives): render plate thumbnails server-side when sidecar slice skips them (#1759)
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.
2026-06-19 07:28:09 +02:00
maziggy 1b7d661552 Updated README 2026-06-18 12:13:07 +02:00
maziggy fd5c95809f Updated README 2026-06-18 12:12:43 +02:00
maziggy 52448a374e test(settings): include preset fields in /ui-preferences pin assertion
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.
2026-06-18 12:07:56 +02:00
maziggy db63e0b477 fix(printers): post-#1661 cleanup — test fixtures + remove hover-card fly-in
- 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.
2026-06-18 12:02:38 +02:00
maziggy a1cd86880c test(printers): repair speed + AMS load/unload tests after #1661 refactor
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.
2026-06-18 11:54:48 +02:00
Ed 0f99b54d7e feat: Update printer card UI for structure and readability (#1661) 2026-06-18 11:46:07 +02:00
maziggy 9d1f04b89d log(scheduler): emit prefer-lowest dispatch decision at INFO so #1766 can be triaged from a bundle
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.
2026-06-18 08:22:22 +02:00
maziggy 9a432f0050 Restrict printer secrets to update-authority callers
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.
2026-06-18 07:36:16 +02:00
maziggy e77f97060e Housekeeping 2026-06-18 07:02:29 +02:00
maziggy 2139bc5290 Post work PR #1673 2026-06-17 12:08:08 +02:00
Ed 48d3f68ae6 feat: Centralise sidebar ordering and add page visibility options (#1673) 2026-06-17 12:04:57 +02:00
maziggy 0e8b5e032b Post work PR #1505 2026-06-17 11:38:07 +02:00
Poltavtcev af5d24e289 feat(inventory): structured storage locations catalog (#1505) 2026-06-17 11:33:23 +02:00
maziggy e8f0698ae1 fix(local-presets): optimistic remove on delete
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.
2026-06-17 09:15:38 +02:00
maziggy 249dacbd53 chore(frontend): vite 7 -> 8 + plugin-react 5.2
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.
2026-06-17 08:27:48 +02:00
maziggy 861de7a0e6 chore(frontend): dependency bumps
Runtime:
  - dompurify 3.4.0 -> 3.4.10 (package.json floor raised from
    ^3.4.0 to ^3.4.10 so fresh installs cannot land on the
    deprecated 3.4.4 release; release notes 3.4.1 -> 3.4.10
    reviewed — the three call sites (MakerworldPage,
    ProjectDetailPage, ProjectPageModal) use string-output
    sanitisation and are unaffected by 3.4.4's widened default
    allow-list)

  Build / lint / test tooling (transitive, dev-only):
  - @babel/core 7.29.0 -> 7.29.7 (via @vitejs/plugin-react and
    eslint-plugin-react-hooks)
  - vite 7.3.2 -> 7.3.5
  - markdown-it 14.1.1 -> 14.2.0 (via @tiptap/extension-link
    -> @tiptap/pm -> prosemirror-markdown; Bambuddy never calls
    markdown-it.render directly)
  - js-yaml 4.1.1 -> 4.2.0 (via eslint)
  - form-data 4.0.5 -> 4.0.6 (via jsdom)
  - ws 8.20.1 -> 8.21.0 (via jsdom)
2026-06-17 08:18:33 +02:00
maziggy 1a5ae665dc Updated README 2026-06-16 12:07:04 +02:00
maziggy a15a40449d Updated README 2026-06-16 12:06:37 +02:00
maziggy 2940fbdcf7 feat(auth): admin-configurable session lifetime ceiling (#1706)
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.
2026-06-16 12:00:27 +02:00
maziggy b97f25d819 fix(spoolbuddy): inventory search matches spool ID + storage location (#1738)
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
2026-06-16 11:17:28 +02:00
maziggy e198338b29 Hosekeeping 2026-06-16 10:49:01 +02:00
maziggy eaf641ce31 . 2026-06-16 10:46:58 +02:00
maziggy eb5154f61a feat(queue): tabbed page, batch grouping, multi-drag, Gantt timeline
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.
2026-06-16 10:45:30 +02:00
maziggy 2d9f87354e Updated CHANGELOG 2026-06-16 08:42:03 +02:00
maziggy 804fe470fa fix(auth/ui): sidebar accepts granular *_read tiers for archives/queue/files (#1755)
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.
2026-06-16 07:44:22 +02:00
maziggy ead6c37147 fix(notifications): wire on_printer_offline dispatch on disconnect edge (#1752)
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.
2026-06-16 07:34:14 +02:00
maziggy 5fdea009fc fix(auth): preserve original URL across login + OIDC round-trip (#1750)
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.
2026-06-15 11:46:32 +02:00
maziggy b171980688 fix(archives): backfill NULL created_at + tolerate NULL in response (#1732)
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.
2026-06-14 12:21:57 +02:00
maziggy 7b43c545e8 Updated README 2026-06-14 11:27:40 +02:00
maziggy 3b6f6cdb57 Updated README 2026-06-14 11:27:32 +02:00
maziggy fd0c21ea90 Updated README 2026-06-14 11:26:45 +02:00
MartinNYHC e2e66e14b7 Merge pull request #1741 from maziggy/0.2.4.7
**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.
v0.2.4.7
2026-06-14 11:01:29 +02:00
maziggy ec9e5eff61 chore(tests): silence bandit B104 on redaction-sentinel asserts
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.
2026-06-14 10:58:02 +02:00
maziggy 428f3f2db1 Resolved conflicts 2026-06-14 10:52:41 +02:00
maziggy 68c06a76c4 chore(support): silence bandit B104 on net.info ip redaction
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.
2026-06-14 10:23:02 +02:00
maziggy f84756f8d4 chore(support): silence bandit B104 on net.info ip redaction
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.
2026-06-14 10:22:21 +02:00
maziggy f8d6d8fd69 Bumped version 2026-06-14 10:06:33 +02:00
maziggy 2cf6f29503 fix(vp): Send All enqueues one item per plate; archive delete cascades to queue
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.
2026-06-13 15:58:57 +02:00
maziggy 5da5bc0aaf Updated CHANGELOG 2026-06-13 14:22:36 +02:00
maziggy b8a3e7c2c9 fix(print-log): render one swatch per color for multi-color filament rows (#1731 part 1)
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.
2026-06-13 14:10:21 +02:00