mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 12:46:49 +02:00
ccf985abfc030da7cbe2bc1a1adbfdec01870950
349
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ccf985abfc |
feat(slicing): build-plate override in the SliceModal (#1337)
Slicing an STL via the integrated slicer always defaulted to whatever
curr_bed_type lived in the chosen process preset (typically "Cool
Plate"), which the slicer CLI rejected for high-temp filaments with
"Plate 1: Cool Plate does not support filament 1". The user had no
way to switch plates without cloning the preset in BambuStudio.
The Slice modal now exposes a Build plate dropdown with the six
canonical BambuStudio / OrcaSlicer plates (Cool Plate, Cool Plate
SuperTack, Engineering Plate, High Temp Plate, Textured PEI Plate,
Smooth PEI Plate) plus an "Auto (use process preset)" option that
preserves the previous behavior. Positioned between Process profile
and Filament rows so a long filament list never pushes it off the
modal's scrolled viewport, and always enabled regardless of whether
the user picked a Printer Preset Bundle.
A new bed_type field on SliceRequest flows through both dispatch
paths:
- Resolved-preset path: _patch_process_bed_type overwrites
curr_bed_type on the process JSON before forwarding to the sidecar.
Works end-to-end today, no sidecar change needed.
- Bundle dispatch path: slice_with_bundle adds a bedType form field
to the sidecar multipart. The sidecar (maziggy/orca-slicer-api
fork) needs a matching change to honor it as --curr_bed_type on
the CLI invocation; until then the field is silently ignored and
the slice runs with the bundle's default plate.
|
||
|
|
b8e350c3bf |
fix(spoolman): persist color_name edits and stop form round-tripping the subtype synth fallback (#1319)
Editing a spool's color name on Spoolman-backed inventory appeared to
accept the new value but the inventory list column and the next edit
showed it back to the subtype. Three layers stacked to produce this:
1. find_or_create_filament matches by material/name/color_hex/vendor —
color_name is intentionally not part of the match key, but on a
match it returned the existing filament's id unchanged, silently
dropping the new value.
2. The read helper falls back to subtype when filament.color_name is
empty (kept on purpose: without it Spoolman installs that don't
fill the field render every spool as "Unknown color").
3. The edit form prefilled color_name from spool.color_name — which
on those installs was the synth value. Changing subtype but not
color_name silently round-tripped the OLD subtype back to Spoolman
as if it were a real user-set color_name.
Fixes:
- find_or_create_filament now patches the matched filament's
color_name via the existing patch_filament wrapper when the request
differs. Parameter convention: None = don't touch, "" = explicit
clear, any other string = set/update. A patch failure is logged but
does not block the match.
- The PATCH route uses model_fields_set to distinguish "field omitted"
from "field explicitly set to null" (mirrors the existing
storage_location pattern at the same site).
- The map helper returns color_name_is_synthesized: bool. The edit
form leaves the input blank when true, so the user sees the real
stored state and can't accidentally round-trip the synth value back.
|
||
|
|
d08183278f |
fix(users): show password rules in form + match FE check to BE (#1303)
Create/Edit User modal previously had no hint about backend password
complexity, and the FE pre-check was only length>=6 — so any weak
password got bounced as a bare "HTTP 422" toast after the round-trip.
- New checkPasswordComplexity util mirroring backend's validator order
- Helper text under password inputs in both modals
- Submit disabled until every rule passes
- client.ts 422 parser falls back to JSON.stringify(detail) when the
mapped array is empty, so a bare status code never masks the real
Pydantic detail again
- i18n keys added in all 8 locales (EN/DE fully translated, others
per project's English-seed convention)
- 7 unit tests pinning the validator contract, including the
reporter's "12345678" input
|
||
|
|
8a6fcf5cbd |
fix(settings): expose UI rendering fields without requiring SETTINGS_READ (#1293)
The Clear Plate button (and 4 other features on the Printers page) read their state from /settings, which requires SETTINGS_READ. Granting that permission also adds the Settings nav item and leaks SMTP/LDAP/MQTT credentials — exactly what users were trying to avoid by giving an operator only printers:clear_plate. New /settings/ui-preferences endpoint returns a curated, opt-in subset of non-sensitive fields. Matches the existing /default-sidebar-order precedent. PrintersPage switched to the new endpoint; admin pages still use /settings for full access. |
||
|
|
79d54a8d53 |
feat(archives): build-plate icon on cards + uniform printer/model line (#1253)
Show an OrcaSlicer-style bed icon in the archive card's printer-name row indicating which build plate the print was sliced for (Cool / Cool SuperTack / Engineering / High Temp / Textured PEI / Smooth PEI), with the full plate name in the hover tooltip. Closes the gap where users had to remember which plate matched a re-print or open the source 3MF in a slicer just to read the bed setting. Card row also unified: archives with a real Bambuddy-printer association used to render "H2D-1 GCODE ..." while slicer-only uploads rendered "Sliced for X1C GCODE ..." -- same line, two different shapes. Drop the "Sliced for " prefix so both render as a uniform "<name-or-model> [bed-icon] GCODE <hash>" row, scanning identically regardless of provenance. Backend: new bed_type column on print_archives (idempotent ALTER TABLE migration; SQLite + Postgres safe). Populated from curr_bed_type in Metadata/slice_info.config (per-plate, authoritative -- that's what got sent to the printer for the exported plate) with a fallback to project_settings.config for older 3MF shapes. Wired through both archive_to_response() (the hand-rolled dict converter that bypasses from_attributes -- easy to miss) and the /rescan endpoint, so old archives can be re-parsed via the existing per-archive Rescan button. Backfill script (scripts/backfill_archive_bed_type.py, --dry-run supported) re-opens every NULL archive's 3MF on disk to populate the column. Auto-loads .env from project root before importing backend modules (config.py reads DATABASE_URL from os.environ at import time, not from pydantic-settings at Settings() time) and prints the resolved DB URL with credentials redacted, so operators can confirm they're hitting the intended database -- Postgres or SQLite. Frontend: 6 OrcaSlicer-style PNGs ship in frontend/public/img/bed/ -- under /img/ because that path is already statically mounted; a toplevel /bed-icons/ tried first hit the SPA catch-all and returned index.html as text/html. New utils/bedType.ts maps slicer strings case-insensitively, covering both Bambu Studio and OrcaSlicer naming variants for the same physical plate. Unmapped or NULL bed_type simply omits the icon, so cards stay clean for pre-feature archives. |
||
|
|
83a83ed724 |
feat(labels): add 40x30 mm template, hex colour code, bolder brand (issue #809 follow-up)
Three enhancements requested by @oliboehm after the V1 label-printing ship in #809: - New box_40x30 single-label template (common DK/Brother roll size, good for filament-bag and storage-bin labels). Routes through the existing roomy layout since height >= 20 mm. - Colour hex code (#RRGGBB, alpha-stripped, uppercase) rendered on every label - useful when several near-identical material/colour spools sit next to each other and the swatch alone isn't enough to tell them apart. Skipped silently when rgba is None or malformed. - Brand line bumped to Helvetica-Bold (was regular) and a couple of points larger on both layouts so it reads cleanly at arm's length. Wired through the SpoolLabelTemplate union, the modal's TEMPLATE_OPTIONS, and the inventory.labels.templates.box40x30 i18n key in all 8 locales (native translations for de/fr/it/ja/pt-BR/ zh-CN/zh-TW). Modal regression test widened from 4 to 5 template buttons. Three new renderer tests pin the hex-code render, the hex-code skip on invalid rgba, and the bold-brand font reference. |
||
|
|
b30a283184 |
Feature/spoolman inventory UI (#1241)
feat(spoolman-inventory): squashed feature work for rebase onto dev Squashed all commits from feature/spoolman-inventory-ui onto a single commit to enable a clean rebase onto dev. Original per-commit history preserved at backup tag backup/spoolman-inventory-ui-prerebase-20260507-105721. |
||
|
|
90743cfa39 |
feat(encryption): MFA at-rest encryption auto-bootstrap with status UI (#1219) (#1231)
chore(i18n): extend parity gate to all locales with strict/info tiers Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest report informationally until their drift is caught up. ja notably has 27 real placeholder bugs worth fixing before promotion to strict. |
||
|
|
dac2a31192 |
Revert "feat(inventory): unified Spoolman inventory UI + AMS slot assignments…" (#1232)
This reverts commit
|
||
|
|
55d71498e9 |
feat(inventory): unified Spoolman inventory UI + AMS slot assignments + Storage Location + NFC write support + Spoolman Filament Catalog Picker (#1114)
feat(spoolman-inventory): squashed feature work for rebase onto dev Squashed all commits from feature/spoolman-inventory-ui onto a single commit to enable a clean rebase onto dev. Original per-commit history preserved at backup tag backup/spoolman-inventory-ui-prerebase-20260507-105721. |
||
|
|
a50958e426 |
feat(slice-modal): Bundle tier for picking presets from imported .bbscfg
Closes the loop on the bundle work: users who imported a Printer
Preset Bundle via Settings → Slicer Bundles can now pick it in the
SliceModal and slice through the bundle dispatch path the backend
already supports.
UX:
- New "Slicer bundle" picker at the top of the modal, rendered only
when at least one bundle is imported (GET /slicer/bundles non-empty)
- Selecting a bundle replaces cloud/local/standard preset dropdowns
with bundle-scoped pickers (process + per-slot filament names from
the bundle). Printer is implicit (each .bbscfg has exactly one).
- Submit routes through SliceRequest.bundle so the backend skips
PresetRef resolution and asks the sidecar to materialise the JSON
triplet from the stored bundle by name.
- "None" leaves the modal on the original preset triplet path.
Frontend types: SliceBundleSpec + bundle?: SliceBundleSpec on SliceRequest.
|
||
|
|
8a31397171 |
feat(slicer): bundle-aware preview slice for accurate gram estimates
The SliceModal's preview slice runs against unsliced project files to
discover per-plate AMS slot consumption. Until now it always used
slice_without_profiles — accurate slot mapping (a model property) but
gram numbers were derived from the file's embedded process settings,
which can drift from the triplet the real print will use.
When the caller provides a bundle id + printer/process/filament preset
names, get_preview_filaments now routes through slice_with_bundle so
the preview's gram numbers match what the real print will produce.
Cache key picks up a bundle-context fingerprint so different bundle
picks on the same file occupy distinct entries.
Backend:
- slice_preview.get_preview_filaments: optional bundle_* params
- library.py + archives.py: forward params via /filament-requirements
Frontend (forward-compat for the upcoming SliceModal Bundle tier):
- api.getLibraryFileFilamentRequirements / getArchiveFilamentRequirements
accept an optional 4th-arg bundle context object
|
||
|
|
37c9d5f26d | [Feature] Add Stock forecasting and Logistics view (#1184) | ||
|
|
864e5c990e |
feat(inventory): printable PDF spool labels in 4 sizes (#809)
Closes the longest-standing inventory gap — finding a specific spool
in a closet of 50 partials. Per-spool icon button on every inventory
card and table row, plus a "Print labels..." header action that opens
a multi-select picker pre-loaded with the currently filtered spools.
Four pre-built templates: AMS holder (30 x 15 mm) for the popular
Makerworld AMS Filament Label Holder, single box label (62 x 29 mm)
for Brother PT/QL or Dymo small labels, Avery L7160 (A4, 21 per
sheet), and Avery 5160 (US Letter, 30 per sheet). Each label carries
the colour swatch (with multi-colour gradient stripes for spools
with extra_colors set), brand, material, name, the *spool ID*
(bsaunder's articulated user-need: telling 8 spools of "PLA White"
apart, especially partials), and a QR code that deep-links to
/inventory?spool=<id> for phone-scan round-trips. Box-label adds
storage location; AMS-holder drops the QR — at 30 x 15 mm there is
no room for swatch + text + QR without truncating away the spool ID,
and AMS-bay identification is at arm's length where the swatch and
ID are enough.
Server-side rendering via ReportLab + qrcode (already a dep). Pure
Python, no headless browser, no system libs. Output is byte-identical
across browsers, Avery sheets align to <0.1 mm, and bulk export is
one click for one PDF. Two endpoints — POST /inventory/labels (local
DB) and POST /spoolman/labels (Spoolman-backed) — gated on
INVENTORY_READ, capped at 500 spools per request, returning
application/pdf via StreamingResponse. The renderer is decoupled
from the SQLAlchemy model via a LabelData dataclass so the same code
path serves both modes.
Modal picker scales to large libraries: search (substring match
across name / brand / #ID), material filter chips derived from the
visible spools, additive Select-all-visible / Deselect-visible /
Clear-all actions so selections survive filter changes. Restyled
twice in development — first cut used generic Tailwind which clashed
with the inventory's bambu-dark palette; second cut switched to
bambu-dark-secondary / bambu-green / bambu-gray to match.
Two render bugs found during visual inspection of generated PDFs and
fixed before commit:
1. AMS-30x15 template originally produced labels with only swatch
+ QR and no text at all — the side-by-side layout left <5 mm
for the text column, so the renderer bailed without drawing
anything. Layout split into tight (h<20mm) and roomy (h>=20mm)
regimes; tight regime drops the QR and gives the right column
to brand + material + a 13pt-bold spool ID.
2. Box-62x29 template aggressively truncated text — swatch + QR
each at ~14 mm on a 26mm-tall label squeezed the text column
to ~16 mm, turning "Polymaker Ivory" into "Polymak..." and
"Polymaker . PLA . Matte" into "Polymaker ...". Swatch capped
at 16 mm, QR capped at 18 mm and constrained to ~20% of width,
leaving the text column ~30 mm — full names render without
truncation.
Both bugs pinned by regression tests in test_label_renderer.py that
render with pageCompression=0 so the resulting PDF bytes contain the
text as ASCII and `assert b"Polymaker" in pdf` works.
|
||
|
|
b42aaca521 |
fix(spool-assign): defer MQTT for empty AMS slot, replay on physical insert
The SpoolBuddy "weigh-then-assign" workflow tried to configure an empty AMS slot at assign time, but Bambu firmware silently drops ams_filament_setting and extrusion_cali_sel for unloaded slots — the MQTT calls completed and the modal closed, yet BambuStudio kept showing the slot as default-PLA forever. assign_spool now detects an empty target slot (fingerprint_type empty) and persists the SpoolAssignment without publishing MQTT, returning a new pending_config flag so the frontend can swap "Assigned!" for "Slot will configure when you insert the spool." on_ams_change watches for the slot to load (state == 11, which fires for 3rd-party tags too even when tray_type stays empty) and replays the deferred ams_filament_setting + extrusion_cali_sel — including the printer-kp realignment that converts PFUS-prefix cloud user presets to the P-prefix local-preset filament_id the slicer actually accepts. The full assign-time MQTT block was extracted into apply_spool_to_slot_via_mqtt so both the assign endpoint and the on_ams_change replay path use the same resolution logic; the helper takes ~270 lines of duplication out of assign_spool. |
||
|
|
64899a8ca4 |
refactor(virtual-printer): drop Tailscale LE cert path, keep toggle informational
The Tailscale toggle was supposed to obtain a publicly-trusted Let's Encrypt
cert via `tailscale cert` so users wouldn't need to import Bambuddy's CA into
the slicer. End-to-end testing showed this was always going to fail:
- Bambu Studio and OrcaSlicer refuse hostname input in the Add Printer
dialog (IP-only).
- Their printer-MQTT trust path validates only against the bundled BBL CA
store (`printer.cer`), NOT the system trust store. Confirmed against
ClusterM/open-bambu-networking's clean-room reimplementation:
`mosquitto_tls_set(BBL_CA)` + `verify_peer=1` + `tls_insecure=true` —
chain validation against BBL CA only, hostname check intentionally
skipped (because Bambu's printer cert CN is the device serial).
- LE certs don't chain to BBL CA, so the slicer rejects with the
well-known "-1" before any hostname/IP logic runs.
The cert-import step is unavoidable; LE provisioning was dead code for slicer
connections. Pivot:
- Toggle stays as an informational marker — when ON, the VP card surfaces
the host's Tailscale IP + MagicDNS hostname so users know what to paste
into the slicer.
- Cert is always self-signed (signed by `bbl_ca`).
- Tailscale exposure is via the existing bind_ip dropdown, which already
includes `tailscale0` IPs.
- Tailscale's role is strictly network reach — same trust burden as LAN.
Backend cuts:
- `tailscale.py`: `provision_cert`, `ensure_cert`, `cert_needs_renewal`,
`_FQDN_RE`, `_HTTPS_DISABLED_RE`, `TS_CERT_EXPIRY_THRESHOLD_DAYS`,
`cryptography` import. Keep `get_status` and `TailscaleStatus`.
- `certificate.py`: `ts_cert_path`, `ts_key_path`, `use_tailscale_cert`.
- `manager.py`: `tailscale_fqdn` field, `_cert_renewal_task`,
`_cert_restart_task`, `_cert_renewal_loop`, `_restart_for_cert_renewal`,
`_cancel_renewal_task`, `_cancel_restart_task`. Simplify
`_resolve_cert_and_advertise` to a sync method that just generates the
self-signed cert. Drop `tailscale_disabled` from the change-detection
diff (toggle is informational — no service restart needed).
- `routes/virtual_printers.py` + `routes/settings.py`: drop the
`tailscale_not_available` 409 guard on toggle-enable.
Frontend cuts:
- `VirtualPrinterCard.tsx`: FQDN/IP display sourced from
`multiVirtualPrinterApi.getTailscaleStatus()` (host-level) when toggle
is ON, instead of `printer.status.tailscale_fqdn` (cert side-effect,
no longer populated). Drop the `tailscale_not_available` toast handler.
- `api/client.ts`: drop `tailscale_fqdn` from the VP status type.
- i18n: rewrite `tailscaleDisabled.description` in all 8 locales to drop
the "no cert import" promise. Remove `toast.tailscaleNotAvailable` key.
Docs:
- Wiki `features/virtual-printer.md`: rewrite the entire Tailscale section
— remove the LE-cert + HTTPS-Certs-toggle + tailscale-cert-operator
steps, document the toggle as informational, keep the Docker socket
mount + LXC TUN troubleshooting (those still apply for daemon
reachability).
- README: drop "the Tailscale benefit here is the tunnel, not cert-import
elimination" framing in favour of "surfaces the IP for paste into
slicer; CA import unchanged because BBL CA store, not system trust
store, is what gets validated".
Tests:
- `test_tailscale.py`: reduced to surviving `get_status` cases (binary
missing, command fails, success, empty DNSName, malformed JSON).
- `test_virtual_printer.py::test_sync_from_db_restarts_on_tailscale_disabled_change`
→ `test_sync_from_db_does_not_restart_on_tailscale_toggle` (toggle is
informational; `remove_instance` must NOT be called).
- `test_virtual_printer_api.py::TestVirtualPrinterTailscaleGuardAPI` →
`TestVirtualPrinterTailscaleToggleAPI` (single test asserts both
directions succeed and daemon is never consulted).
- `VirtualPrinterCard.test.tsx`: mock now stubs `getTailscaleStatus`;
FQDN-copy block drives data through that query.
DB column `tailscale_disabled` is kept (persists toggle state) — Postgres-
safe column drop is harder; future cleanup can remove if the toggle goes
away entirely. LE cert files on disk (`virtual_printer_ts.{crt,key}`) are
left in place per VP — harmless residue, manual cleanup if desired.
Verified: ruff clean, 2484 backend unit tests pass, 17 frontend VP-card
tests pass, frontend build succeeds, live service restart confirms VPs
serve `issuer=CN=Virtual Printer CA` on the Tailscale interface — slicer
trusts the user-imported bambuddy CA and skips hostname checks, so MQTT
connection succeeds end-to-end.
|
||
|
|
e45f967616 | feat(backup): Extend Backup to other Git providers (#1160) | ||
|
|
abc8e97050 |
feat(camera): optional snapshot URL override for external cameras (#1177)
go2rtc and several IP cameras still emit a warm-up / black frame on every fresh MJPEG connection — even with the v0.2.4b2 warm-up-skip fix it slipped through intermittently for @nkm8's setup. His own bisect named the clean solution: go2rtc exposes /api/frame.jpeg as a dedicated single-frame endpoint that never returns the encoder's stale keyframe. Adds an optional external_camera_snapshot_url column on printers. When set, every single-frame capture path (snapshot endpoint, [SNAPSHOT] notification thumbnails, [PHOTO-BG] finish photo, layer timelapse, Obico ML, plate-detect / calibrate-plate) routes through _capture_snapshot on the override URL via plain HTTP GET, bypassing the warm-up dance. Live view stays on the configured stream URL — only single-frame captures use the override. Override is camera-type-agnostic. SSRF guard applies (existing _sanitize_camera_url allowlist). Empty string treated as unset. Settings UI: new "Snapshot URL (optional)" input + Test button under External Cameras, hidden for camera_type=snapshot since the live URL is already a single-frame source. en + de fully translated; 6 other locales seeded with English copy. 5 backend tests pin the routing contract; 3 frontend tests pin the input + debounced PATCH. Documented in bambuddy-wiki/docs/features/camera.md with the go2rtc example. |
||
|
|
d0d0be89ea |
fix(oidc): use preferred_username/name claim for auto-created username (#1173) (#1176)
fix(oidc): use preferred_username/name claim for auto-created username When auto-creating an OIDC user without a valid email claim, derive the username from preferred_username or name IdP claims instead of falling back to the opaque provider_sub[:30]. |
||
|
|
3320c7fd45 |
feat(printers): AMS slot Load / Unload from the printer card (#891)
The ams_load_filament / ams_unload_filament MQTT primitives existed
in bambu_mqtt.py but were unused — no HTTP route and no UI. Surface
both as POST /printers/{id}/ams/load?tray_id={int} and
POST /printers/{id}/ams/unload, gated on PRINTERS_CONTROL.
Wire them into the existing AMS slot popover (next to "Re-read RFID")
and add a popover wrapper on the external spool slot which had none.
Hidden while the printer is RUNNING, mirroring the RFID re-read
gating. Both buttons enabled when permission is granted; the printer
no-ops gracefully if there's nothing to do (matches BambuStudio).
Dual-extruder H2D Ext-R support is the trickier piece. The existing
ams_load_filament(254) capture came from a single-extruder printer
and used slot_id=254, curr/tar=-1. Captured the Ext-R command from
BambuStudio fresh: it sends ams_id=255, slot_id=0 (the right
extruder index, NOT a slot index), target=255, and curr/tar = the
actual right-nozzle temp (read from state.temperatures["nozzle_2"],
falling back to 215 °C if cold so the printer doesn't reject the
command on a nonsensical temp). Added that as a new branch in
ams_load_filament; the existing tray_id=254 branch is preserved
verbatim — no risk of regression on single-external setups.
|
||
|
|
459cfdc51f |
fix(virtual-printer): queue mode pins per-slot type+color so scheduler can match colour (#1188)
Edward's diagnosis was exact: the manual /print-queue/ POST extracts filament requirements from the 3MF and writes required_filament_types + filament_overrides + ams_mapping onto the queue item, but the VP queue-mode write path skipped all of that. Net effect: scheduler reached its model-only-matching fallback and auto-dispatched onto whatever printer was free regardless of loaded colour. Extract the scheduler's existing _get_filament_requirements 3MF parser into a shared helper so the VP path can reuse it. VP's _add_to_print_queue now populates required_filament_types unconditionally (cheap; helps the scheduler reject obvious type mismatches) and writes filament_overrides with force_color_match: true per consumed slot when a new per-VP queue_force_color_match toggle is on. Default off to preserve current behaviour for upgraders. UI: new toggle on VirtualPrinterCard, mode-gated to print_queue, mirroring the existing auto-dispatch toggle. i18n: en + de translated, other 6 locales seeded with English copy. Schema: one nullable column on virtual_printers (queue_force_color_match BOOLEAN, default 0/FALSE). 11 new backend tests (8 for the extracted parser, 3 for the VP write path) + 6 new frontend tests (toggle render gating, default state, click posts queue_force_color_match in update body). Existing scheduler tests pass against the refactored helper. README, CHANGELOG, website features page, and wiki virtual-printer page all updated. |
||
|
|
01a7e6ee93 |
fix(archive,vp): strip .gcode.3mf properly + sync review/archive name (#1152)
@smandon retested the original #1152 fix on the latest daily and surfaced two distinct holes: 1. ``Path(name).stem`` only strips the *last* suffix, so Bambu Studio's default ``Plate_1.gcode.3mf`` exports landed in the archive UI as ``Plate_1.gcode`` — never the bare ``Plate_1`` the user expected. 2. The pending-uploads review card always showed the raw FTP filename, while the eventual ``PrintArchive.print_name`` resolved from the 3MF's embedded title (or, with the toggle on ``filename``, the stripped stem). Net effect: same upload showed two different names depending on which view you were looking at, with no way for the toggle to flip both views in lockstep. Three changes: - ``resolve_display_stem`` helper in ``services/archive.py`` strips ``.gcode.3mf`` / ``.3mf`` / ``.gcode`` (case-insensitive). Applied at the archive-creation site so ``Plate_1.gcode.3mf`` → ``Plate_1`` for every flow that produces a ``PrintArchive`` row. - ``PendingUpload.metadata_print_name`` (new nullable column) is populated at FTP-receive time by peeking at the 3MF's embedded title via the existing ``ThreeMFParser``. Read happens once per upload — the list endpoint then doesn't have to reopen each 3MF on every render. Parser failures are swallowed and the column stays NULL; the response model gracefully falls back to the stripped filename. - ``PendingUploadResponse.display_name`` is a computed field that mirrors ``archive_print``'s exact precedence — ``filename`` toggle → stripped stem; ``metadata`` toggle (default) → cached title or stripped stem. The frontend's review card reads it (with ``upload.filename`` as a defensive fallback) and surfaces the raw FTP filename via tooltip so users can still inspect what arrived. Migration is one idempotent ``ALTER TABLE pending_uploads ADD COLUMN metadata_print_name VARCHAR(255)`` (Postgres/SQLite-safe). Pre-migration rows have NULL and degrade to filename-stem behaviour without any operator action. Tests: 14 unit tests in ``test_archive_display_stem.py`` covering the canonical normalisation rules (Bambu Studio default name, mixed case, dots-in-the-middle, edge cases like ``.gcode.3mf``-only, full-path inputs); 6 integration tests in ``test_pending_upload_display_name.py`` pinning the response contract (default toggle uses metadata title when present, falls back to stripped stem when absent, ``filename`` toggle overrides metadata, ``filename`` toggle still strips the double suffix, ``GET /{id}`` exposes the same field, whitespace-only metadata behaves like absent); 3 frontend tests in ``PendingUploadsPanel.test.tsx`` pinning the review card's render path (resolved name shown, fallback to filename when display_name is empty, raw filename available via tooltip). Full backend suite: 3598 passed; frontend build clean; no regressions in any flow that previously processed ``.3mf`` / ``.gcode`` / non-3D filenames. |
||
|
|
133ec72527 |
feat(api-keys): per-user ownership + opt-in cloud access scope (#1182)
Tim (@turulix) is building a fully automated headless slicing pipeline against Bambuddy's API and hit the wall flagged in #665: /cloud/* routes resolve cloud_token per-user from User.cloud_token, but the auth gate returned None for API-keyed requests, so the route fell back to the global Settings-table token, which only carries a value in auth-disabled deployments. Net effect on auth-enabled deployments: API keys reached the gate just fine, then /cloud/filaments always saw user=None and returned 401 / empty results — no path to read slicer presets or the filament catalogue that a CLI workflow needs. Make API keys carry an owner and route /cloud/* lookups through that owner; gate the new capability behind an explicit opt-in scope so existing automation doesn't gain cloud-read access on upgrade. - APIKey gains user_id (FK to users.id, ON DELETE CASCADE) and can_access_cloud (BOOLEAN DEFAULT 0). User-delete route also runs an explicit DELETE FROM api_keys WHERE user_id = ? since SQLite ships FK enforcement off — same pattern as the existing created_by_id cleanup blocks. - New cloud_caller dep on /cloud/* routes resolves to the JWT user OR the API-key owner stashed by a router-level gate. The auth gate itself continues to return None for API keys so #1182's surface stays bounded to /cloud/* — without that bound, any route that fences API keys via `if current_user is None: raise 403` (e.g. long-lived-token management) would silently start accepting them. - The /cloud/* router-level dep enforces three independent fences for API-keyed callers: user_id IS NOT NULL (legacy keys → 401 with recreate copy), can_access_cloud=True (otherwise 403), and owner has cloud_token (existing fence, unchanged). Two extra one-shot fence errors at create/update time refuse can_access_cloud=True when auth is disabled or the key is ownerless. - Frontend: APIKey list shows "Cloud" badge on cloud-enabled keys and "Legacy" badge on ownerless rows; create form gains an "Allow cloud access" toggle, default off. New i18n keys in all 8 locales (en + de fully translated, others seeded with English fallbacks pending native translation — matches the project's flow for newly-added features). Migration: two idempotent ALTER TABLE statements + an index on user_id for the auth gate's owner→keys lookup. Postgres-safe. Tests: 9 backend integration tests in test_api_key_cloud_access.py covering creation flags, the three /cloud/* fences, JWT no-op, and deletion CASCADE; 2 frontend SettingsPage tests pinning the badge matrix and the create-form contract; 5 daemon unit tests for the related SpoolBuddy ssh-key sync work that landed in the same branch. Full backend suite: 3578 passed; full frontend suite: 1597 passed; no regressions. Permission semantics for existing keys: keys created before this release become "legacy" and are rejected at /cloud/* with the recreate message. Every other endpoint they were used against — queue, status, control — is untouched. |
||
|
|
4aea4be2bd |
feat(updates): detect HA Supervisor addon and defer update UI to it (#1167)
Bambuddy already supports running as a Home Assistant addon (HA_URL/HA_TOKEN env-var integration since #283, community addon at hobbypunk90/homeassistant-addon-bambuddy), but the update UI was oblivious to it: HA addon users saw the in-app "Update available" banner and, on Settings, the docker-compose snippet — neither of which they can act on, since the HA Supervisor owns the addon lifecycle. Detection uses the SUPERVISOR_TOKEN env var that HA Supervisor injects into every addon container; no other environment sets it, so the check has zero false-positive surface. Backend: - new _is_ha_addon() helper in routes/updates.py - /updates/check now returns is_ha_addon: bool and extends update_method to 'git' | 'docker' | 'ha_addon' - /updates/apply checks HA before Docker (HA addons ARE Docker containers, so checking docker first would mis-classify) and returns an HA-specific message that points to Settings → Add-ons → Bambuddy in HA - response keeps is_docker: true alongside is_ha_addon: true so older frontend bundles still hit a managed-deployment branch instead of rendering an Install button that can't work Frontend: - SettingsPage update card branches on is_ha_addon BEFORE is_docker; HA users get a Supervisor-targeted message instead of the docker-compose snippet - Layout update banner is suppressed for HA addons — HA Supervisor surfaces its own update notification natively, so Bambuddy's banner would be duplicate noise linking to a page that just says "update via HA" - Plain Docker deployments are unaffected i18n: settings.updateViaHomeAssistant added to all 8 locales with full native translations. Tests: 3 backend unit tests for _is_ha_addon (present, absent, empty-string treated as unset), 3 backend integration tests (HA-precedes-Docker rejection on apply; HA branch on check; plain Docker branch on check), 2 SettingsPage tests pinning the mutually-exclusive UI rendering, 2 Layout tests pinning banner suppression for HA and retention for plain Docker. |
||
|
|
b45ca2a662 |
feat(printer): support Filament Track Switch (FTS) accessory in print modal (#1162)
The FTS routes any AMS slot to either extruder, so AMS info reports
bits 8-11 = 0xE (uninitialized) and ams_extruder_map ends up empty.
The print modal's per-nozzle dropdown filter then hides every loaded
slot, leaving the user with an empty filament dropdown.
Detection: parse print.device.fila_switch from MQTT push_status into a
new FilaSwitchState dataclass on PrinterState; surface it through the
GET /printers/{id}/status response as a nullable FilaSwitchResponse.
Frontend: useFilamentMapping and FilamentMapping skip the per-extruder
filter when fila_switch.installed is true. Slots currently fed into a
track display an [L]/[R] routing badge in the dropdown so the user
can see where the FTS is currently routing them.
Tests: 4 backend unit (TestFilamentTrackSwitchDetection), 2 backend
integration (status route), 2 hook regression, 2 component regression.
|
||
|
|
a34beaa599 |
feat(inventory): multi-colour gradients, transparency, visual effects (#1154)
Spool and color_catalog rows carry extra_colors (comma-separated hex stops) and effect_type (14 visual variants: surface effects, sheen, structural). The shared FilamentSwatch component renders gradient, conic, effect overlay, and alpha-checkerboard consistently across the inventory grid, table, group banner, card, ColorSection preview, and catalog editor. Catalog hex_color accepts #RRGGBBAA so catalog entries can carry transparency too. The paste field accepts the exact format 3dfilamentprofiles.com puts on its filament details pages, so users can copy a multi-colour combo directly. The effect dropdown spans the full filament-variant vocabulary -- surface effects (sparkle/wood/marble/glow/matte), sheen variants (silk/galaxy/rainbow/metal/translucent), and structural variants (gradient/dual-color/tri-color/multicolor). None of these fields touch MQTT/firmware -- pure visual hint. Spool group-key extended to include extra_colors + effect_type so "Group similar" no longer collapses visually distinct spools. Migrations: 4 idempotent ALTER TABLE ADD COLUMN (Postgres-safe), plus ALTER COLUMN hex_color TYPE VARCHAR(9) on Postgres only (SQLite ignores VARCHAR length). Tests: 42 new backend (35 unit + 7 integration), 20 new frontend (14 FilamentSwatch + 3 ColorCatalogSettings + 3 InventoryPageGrouping regression). 3522 backend + 1582 frontend tests pass; ruff clean. Localised across all 8 UI locales. |
||
|
|
57af8a1c19 |
feat(projects): URL field + cover photo on project cards (#1155)
Two new project fields: a free-text URL rendered as a one-click
external-link button beside the project name on every card (opens in a
new tab, click is e.stopPropagation()-guarded so it doesn't enter the
project), and a cover photo that replaces the status-icon box with a
square thumbnail.
URL is plumbed through ProjectCreate/Update/Response/ListResponse,
including from-template + create-template flows so it inherits between
a project and its template. Cover photo is not inherited because the
file would be shared on disk between source and copy.
Schema validator rejects anything other than http:// or https://
prefixes -- <a href> rendering would otherwise execute javascript:
/ data: / file: URLs even with React's default escaping. PATCH uses
model_fields_set for the URL field so users can clear it by sending
{"url": null}.
Cover image storage: Project.cover_image_filename references a file
Cover image storage: Project.cover_image_filename references a file
inside the existing archives/projects/{id}/attachments/ dir, but it's
tracked separately from the attachments JSON list so swap/delete on
the cover doesn't perturb the user's other attachments. Three routes
(POST/GET/DELETE /projects/{id}/cover-image) accept only .jpg/.jpeg/
.png/.gif/.webp (no SVG -- SVG can carry script payloads), replace in
place (prior file deleted before the new one lands so repeat uploads
can't accumulate orphans), and self-heal when a DB reference points at
a vanished disk file by clearing the column and 404'ing.
GET cover-image is gated by RequireCameraStreamTokenIfAuthEnabled
(accepts ?token=... query string) -- not the bearer-token gate -- so
<img src> requests work in both auth-on and auth-off configurations.
The frontend wraps getProjectCoverImageUrl with withStreamToken(),
matching the existing pattern from getArchiveThumbnail.
Permissions: PROJECTS_UPDATE for upload/delete/PATCH, PROJECTS_READ
gate is implicit via the stream-token credential. Migration: 2
idempotent ALTER TABLE projects ADD COLUMN. Localised across all 8
UI languages.
|
||
|
|
c2e7f8eb4b |
feat(vp): add archive name source toggle (metadata/filename) (#1152)
Slicer-uploaded archives picked up their display name from the 3MF's
embedded print_name (the creator-baked title); users who renamed a job
in BambuStudio's "Send to printer" dialog never saw that name surface
because the FTP filename was only used as a fallback when metadata was
empty.
Settings -> Virtual Printer now exposes an Archive name source toggle
(Metadata / Filename, default Metadata) that flips precedence in
ArchiveService.archive_print via a new prefer_filename_for_name param.
All four VP-sourced archive paths read the new
virtual_printer_archive_name_source setting and forward the flag:
_archive_file, _add_to_print_queue, POST /pending-uploads/archive-all,
POST /pending-uploads/{id}/archive.
|
||
|
|
d5153f1de3 |
feat(slicer): live progress + filament discovery polish + OrcaSlicer warning
End-to-end live progress, two correctness fixes, and a UX warning around
the upstream OrcaSlicer bugs we discovered while testing.
LIVE PROGRESS
=============
Wire OrcaSlicer / BambuStudio's --pipe progress channel through the
sidecar -> Bambuddy -> persistent toast so a user-initiated slice shows
"{name} -- Generating G-code (75%) -- 47s" instead of just elapsed time.
The same wiring covers the SliceModal's filament-analysis preview slice
(the real slice that fires before profile picking, used to discover
which AMS slots an unsliced plate consumes) and the embedded-settings
fallback path triggered by Orca's --load-settings segfault on complex
H2D models.
- Sidecar (orca-slicer-api/bambuddy/profile-resolver, separate commit):
switch /slice from execFile to spawn, mkfifo per request, parse the
CLI's structured JSON progress events into a per-process
ProgressStore, expose GET /slice/progress/:requestId.
- Bambuddy backend: slicer_api.slice_with_profiles + slice_without_profiles
accept request_id + on_progress, spawn a 1Hz parallel poller that
forwards each snapshot via SliceDispatchService.set_progress(job_id,
...) onto the matching SliceJob; GET /slice-jobs/:id includes the
latest snapshot on every poll. The 404 from the early-race window
(POST fired before sidecar's progressStore.start) is treated as a
retry rather than terminal -- otherwise the poller bailed before any
progress could ever arrive.
- /api/v1/slicer/preview-progress/:requestId proxies the sidecar's
progress endpoint for the modal's filament-discovery flow (the
/filament-requirements call is server-originated; the browser can't
reach the sidecar directly).
- Frontend: SliceJobTrackerContext re-renders the persistent toast with
the new format when a useful progress frame is present, falls back
to elapsed-time-only when the sidecar hasn't emitted yet or doesn't
support progress. SliceModal.FilamentAnalysisSpinner generates a
per-(source, plate) UUID, polls the proxy at 1Hz, and mirrors the
inline spinner contents into a separate persistent toast so the
preview slice doesn't feel silent either.
CORRECTNESS FIXES
=================
- MakerWorld imports were persisting URL-encoded filenames verbatim
("stormtrooper-helmet%20h2d.3mf"). Backend now urllib.parse.unquote
s the manifest-supplied name and the URL path-tail fallback before
passing to save_3mf_bytes_to_library; frontend defensively
decodeURIComponent s in the slice toast / analysis spinner so
already-imported rows display cleanly without a backfill migration.
- The fallback path's slice_without_profiles call now forwards the
same request_id + on_progress as the primary slice_with_profiles
call so the toast keeps updating across the segfault -> embedded-
settings retry boundary instead of going blank.
ORCASLICER WARNING
==================
Verified two upstream OrcaSlicer CLI bugs reproduce on the latest
nightly (2.4.0-dev, 2026-04-28) with the help of an isolated AppImage
extract and a minimal sentinel-value-injected cube fixture:
- OrcaSlicer/OrcaSlicer#12426 -- SIGSEGV in
update_values_to_printer_extruders_for_multiple_filaments on
painted multi-extruder 3MFs (commented on the existing thread,
not a new issue)
- OrcaSlicer/OrcaSlicer#13386 -- CLI strict-validates parameter
values BambuStudio writes by default (solid_infill_filament: 0,
tree_support_wall_count: -1, prime_tower_brim_width: -1) and
rejects with exit 238, even though Orca's own GUI tolerates
them (filed by us alongside this change)
Settings -> Workflow -> Slicer card renders an amber inline warning
under the preferred-slicer dropdown when orcaslicer is selected,
linking both upstream issues and recommending BambuStudio until the
fixes land. Option stays pickable -- users who only slice STLs aren't
affected by either bug.
|
||
|
|
988c00554e |
feat(slicer): multi-color slicing + per-plate filament discovery
The slice modal previously rendered exactly one filament dropdown and
silently truncated multi-color 3MFs to a single profile, producing wrong
colours on every multi-filament print. End-to-end fix across sidecar,
backend, and frontend.
Sidecar (orca-slicer-api / bambuddy/profile-resolver, separate commit):
- /slice accepts up to 16 repeated filamentProfile parts; slicing
service materializes each and joins paths with `;` for
--load-filaments.
- /profiles/bundled emits filament_type and filament_colour per leaf
so the bundled tier carries metadata into the modal.
Bambuddy backend:
- SliceRequest gains filament_presets: list[PresetRef]. Validator
accepts three shapes (multi-color array, source-aware singular,
legacy bare-int id) and lands them all on a populated array before
the route handler runs — fully backwards-compatible.
- SlicerApiService.slice_with_profiles takes filament_profile_jsons:
list[str] and sends one filamentProfile multipart part per profile
(in submission order) so the sidecar receives N profiles cleanly.
- New service slice_preview runs the sidecar's slice_without_profiles
against an unsliced project file's embedded settings, parses the
result's slice_info.config, and returns the canonical per-plate
filament list. Cached by (kind, source_id, plate_id, content_hash)
with LRU eviction at 256 entries, per-key asyncio.Lock prevents
thundering-herd; transient sidecar failures are NOT cached so they
retry naturally; parse failures ARE cached (deterministic property
of the input, no point re-running).
- /filament-requirements endpoint chain: slice_info.config (existing,
sliced files) → preview-slice (new, unsliced project files) →
project_settings.config + painted-face heuristic with 5% noise
threshold (sidecar-down fallback).
- threemf_tools gains extract_project_filaments_from_3mf and
extract_plate_extruder_set_from_3mf — the latter unions object
top-level extruder, per-part overrides, and painted-face quadtree
leaves (1-E nibbles in paint_color attrs of <triangle> elements
inside per-object .model files).
- Cloud preset listing no longer fetches per-preset detail (Bambu's
rate limit at ~10/sec returns 429 on every request for users with
50+ presets). Unified-listing dedup pass instead backfills metadata
cross-tier so a cloud entry that wins dedup over a same-named local
entry inherits the local's filament_type / filament_colour.
Frontend:
- SliceModal multi-step: plate-picker first when the source is a
multi-plate 3MF, then preset dropdowns. One filament dropdown per
AMS slot the plate actually uses, each pre-picked by metadata
match against user's local + standard presets via existing
colorsAreSimilar / normalizeColorForCompare utils.
- SliceModal-only tier priority is now local → cloud → standard
(was cloud → local → standard). Other consumers of /slicer/presets
keep the existing cloud-first order.
- Submits filament_presets array; backfills the legacy singular
filament_preset from the array's first entry for stale-tab
compatibility.
- i18n keys added across all 8 locales: slice.filamentSlot,
slice.tier.{local,cloud,standard}, slice.cloud.{notAuthenticated,
expired,unreachable}, slice.noPresetsForSlot,
slice.allPresetsRequired (en + de fully translated; six others
seeded with English copies pending native translation, matching
the project's existing flow).
Permissions: no new endpoint paths added. Preview-slice runs inside
/filament-requirements (LIBRARY_READ / ARCHIVES_READ) and multi-filament
dispatch runs inside POST /slice (LIBRARY_UPLOAD). No auth surface
widened.
Tests: 6 SliceRequest schema tests for multi-filament + legacy-new
precedence; 9 unit tests for slice_preview cache behaviour (LRU
eviction with lock cleanup, content-hash invalidation, concurrent
thundering-herd guard, no-cache-poison on transient sidecar failure);
15 unit tests for the two new threemf_tools helpers (5 + 10 cases
including the 60/40 painted-threshold regression pin); a multi-filament
wire-format test pinning the multipart part count + order; 22 frontend
SliceModal tests covering plate picker, multi-color render,
metadata-aware pre-pick, manual override, and the new tier order.
|
||
|
|
61c15aac03 |
feat(slicer): unified Cloud/local/standard presets + harden 3MF profile path
UNIFIED PRESET LISTING (the main feature)
The initial slicer integration only saw DB-backed local imports — users
without imported profiles got an empty Slice modal even when their
Bambu Cloud account or the slicer sidecar carried perfectly usable
presets. The Slice modal now pulls from three tiers in priority order:
- cloud: user's own Bambu Cloud presets, fetched live.
- local: DB-backed imports.
- standard: slicer-bundled stock profiles via the sidecar's new
GET /profiles/bundled endpoint.
Listing endpoint: GET /api/v1/slicer/presets
- Name-based dedup, cloud > local > standard, within-tier order
preserved exactly. A preset that exists in multiple tiers only
renders in the highest-priority one.
- cloud_status (ok / not_authenticated / expired / unreachable)
drives a precise modal banner instead of an unexplained empty
list.
- Cloud branch: per-user cache, 5 min TTL, key
(user_id, sha256(token)[:16]) so logout/login or token rotation
auto-invalidates without callback wiring from the cloud-auth
routes.
- Bundled branch: global cache, 1 h TTL.
- Bundled URL respects preferred_slicer (bambu_studio vs orcaslicer)
so BambuStudio installs see the bambu sidecar's bundled list, not
OrcaSlicer's.
Slicing endpoint: POST /library/files/{id}/slice + /archives/{id}/slice
- Body now accepts source-aware {source, id} triplets per slot:
printer_preset: PresetRef
process_preset: PresetRef
filament_preset: PresetRef
- Legacy *_preset_id integer fields kept for backwards-compat. The
schema validator normalises bare ints into
PresetRef(source='local', id=str(int)) so the route handler only
deals with one shape.
New preset_resolver service fetches the JSON content per source:
- cloud: BambuCloudService.get_setting_detail(id), unwraps the
`setting` envelope (falls back to top-level for minor
shape variants).
- local: DB read with preset_type slot validation (existing path,
factored into the new helper).
- standard: minimal {name, inherits, from: "system"} stub — the
sidecar's profile-resolver flattens it against
BUNDLED_PROFILES_PATH/<category>/<name>.json with no
preset-content round-trip from Bambuddy.
PERMISSIONS
- Listing route gate: LIBRARY_UPLOAD (matches the slice action — any
user who can slice can populate the dropdowns).
- Cloud branch in BOTH the listing helper and the resolver checks
CLOUD_AUTH independently — a user with LIBRARY_UPLOAD but not
CLOUD_AUTH doesn't see the cloud tier (returns 403 if they try
to slice with a cloud preset) even if a leftover User.cloud_token
survived a permission revocation. Cloud listing path
short-circuits the token lookup entirely on the gate-fail branch.
FRONTEND — SliceModal
- Calls api.getSlicerPresets() instead of api.getLocalPresets().
- Dropdowns render <optgroup> per tier with localised section
labels (Cloud / Imported / Standard).
- Default selection follows cloud > local > standard priority on
first load (auto-pick fires once when the data arrives, manual
choices stick after that).
- Cloud-status banner renders three variants
(sign-in / expired / unreachable) only when status != 'ok'.
- Slice button submits source-aware refs; legacy integer payload
is preserved server-side for older clients.
3MF PROFILE-PATH HARDENING (shipped together because they touch the
same code paths)
(1) Strip widened. _strip_3mf_embedded_settings only removed
Metadata/project_settings.config. Real-world Bambu Studio /
OrcaSlicer 3MFs also carry model_settings.config, slice_info.config,
and cut_information.xml — any single leftover trips the CLI's
input validation and the slice falls back to embedded settings,
making the SliceModal's profile picker theatrical for 3MF inputs.
Now removes all four configs via a centralised
_STRIPPABLE_3MF_CONFIGS frozenset with per-file rationale;
geometry (3D/3dmodel.model), thumbnails, multi-part data
preserved.
(2) Sidecar 5xx error capture. slicer_api.py was reading only
`message` from sidecar 5xx responses and dropping `details`, so
every CLI failure surfaced as the unhelpful generic
"Failed to slice the model". New _format_sidecar_error helper
combines both fields, falls back to plain-text body for
non-JSON 5xx (nginx 502s, gateway timeouts), replaces the four
duplicated extraction blocks. Pairs with the orca-slicer-api
fork's bambuddy/profile-resolver branch which now emits
`details` on AppError responses (d9c6121) and captures CLI
stderr in the failure path (fb928c8).
CARE TAKEN — additive on existing surfaces
- main.py: +1 import, +1 router register
- slicer_api.py: +list_bundled_profiles, +_format_sidecar_error
(dedupes the 4 message-extraction blocks);
no existing method behaviour changed
- library.py: resolver swap inside _run_slicer_with_fallback,
user_id threaded through two callers,
strip widened
- schemas/slicer.py: PresetRef added, *_preset fields added,
legacy *_preset_id kept; validator normalises
- 4 new files: schema, route, resolver, tests
- No existing route URL changed, no existing field removed, no
behaviour change for clients still sending bare integer ids.
TESTS
- 17 unit tests for the listing endpoint helpers
- 11 unit tests for the source-aware resolver
- 6 schema tests for SliceRequest legacy + new shapes
- 3 unit tests for the new sidecar error-detail capture
- Strip integration test extended to assert all 4 configs go and
geometry stays
- 12 frontend tests for SliceModal covering tier-priority
auto-selection, <optgroup> grouping, fallback paths, source-aware
payload on submit, manual override across tiers, archive vs
library routing, error display, all three banner variants
Verified: 3394 backend + 1531 frontend tests pass, ruff clean,
frontend production build clean.
Pairs with three already-pushed commits on the orca-slicer-api fork's
bambuddy/profile-resolver branch:
- 5fd6bc6 feat(profiles): add GET /profiles/bundled
- d9c6121 fix(error): include causeMessage in JSON response as `details`
- fb928c8 fix(slicing): include CLI stdout/stderr in failure causeMessage
|
||
|
|
6deaa513af |
● feat(slicer): server-side slicing via OrcaSlicer / Bambu Studio sidecar
Adds an optional slicer-api/ Compose stack and wires Bambuddy's File
Manager, Archives, and MakerWorld pages to a new server-side Slice flow.
Slicing runs as an in-memory background job (POST returns 202 + job_id,
polled via GET /api/v1/slice-jobs/{id}) so a multi-minute slice no
longer pins the modal; result lands as a new .gcode.3mf in the same
folder (or new archive for archive sources) with the embedded
thumbnail extracted.
Backend
- New services: slice_dispatch (in-memory dispatcher, 30min retention
sweep) and slicer_api (HTTP bridge with 4xx/5xx/connection error
split that drives the 3MF embedded-settings fallback retry path).
- New schemas: SliceRequest, SliceResponse, SliceArchiveResponse,
SliceJobEnqueueResponse.
- New routes: POST /library/files/{id}/slice,
POST /archives/{id}/slice, GET /api/v1/slice-jobs/{id} (gated on
LIBRARY_READ since job IDs are sequential and the body leaks source
filenames and result IDs).
- AppSettings + env defaults: use_slicer_api, orcaslicer_api_url,
bambu_studio_api_url. DB-stored values override env defaults.
Frontend
- New SliceModal handles preset gating; enqueues then closes
immediately.
- New SliceJobTrackerProvider polls active jobs at app level, surfaces
a single toast per job (queued -> running -> completed / failed)
and invalidates library/archives queries on terminal status.
- Settings -> Workflow -> Slicer card: preferred slicer dropdown,
Use Slicer API toggle, contextual sidecar URL field.
- File Manager / Archives / MakerWorld get a Slice button gated on
the Use Slicer API setting.
- gcode-viewer adapter learns ?library_file=<id> so sliced library
files preview inline.
i18n
- New slice.* and settings.{useSlicerApi,slicerCard,orcaslicerApiUrl,
bambuStudioApiUrl,slicerApiUrlDescription,useSlicerApiDescription}
+ fileManager.noPermissionSlice keys across all 8 locales (en, de,
fr, it, ja, pt-BR, zh-CN, zh-TW). English fully translated, German
fully translated, the other six seeded with English fallbacks
pending native translation.
Tests
- 10 backend integration tests in test_library_slice_api.py covering
validation (404/400), happy-path enqueue, sidecar-down, 3MF
embedded-settings fallback, STL no-fallback, and preset-error ->
failed job paths.
- New unit tests in test_slicer_api.py for the HTTP bridge.
- 5 new SliceModal frontend tests covering preset gating, library +
archive enqueue paths, error surface, and preset-load failure.
- Existing SettingsPage tests adjusted: slicer dropdown asserts now
switch to the Workflow tab first; added a beforeEach URL reset so
one test's tab click doesn't bleed into sibling tests.
Sidecar
- New slicer-api/ folder is self-contained and optional. Two services
(orca-slicer-api on 3003, bambu-studio-api on 3001 behind --profile
bambu) build via Docker git-build-context from
maziggy/orca-slicer-api@bambuddy/profile-resolver. The fork patches
the OrcaSlicer CLI's profile compatibility quirks (inherits-chain
resolver, from:User -> system rewrite, '# ' clone-prefix strip,
sentinel-value strip) empirically required to slice real GUI
exports without segfaulting the CLI.
Docs
- CHANGELOG entry under [0.2.4b1] - Unreleased Added.
- README File Manager bullet for the new server-side Slice button.
- bambuddy-website features.html: new card under "Configurable Slicer".
- bambuddy-wiki: new page features/slicer-api.md + nav entry +
features index card.
Notes
- Opt-in: with Use Slicer API off, the existing "open in desktop
slicer via URI" flow is the default and unchanged.
- 3MF inputs that segfault the CLI on --load-settings transparently
retry with embedded settings; the resulting job carries
used_embedded_settings: true.
- Sliced files always export as .gcode.3mf so File Manager picks up
the embedded thumbnail; file_type is set to "gcode" (blue badge).
|
||
|
|
fdaec47378 |
feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login (#1126)
feat(oidc): add Azure Entra ID support with configurable email claim resolution Adds two new OIDC provider fields: email_claim and require_email_verified. |
||
|
|
4304a42542 |
feat(#729): per-spool category + low-stock threshold override
Two new optional fields on Spool: free-text `category` (max 50) and `low_stock_threshold_pct` (1-99). Powers the "differentiate critical spools from prototype spools and alert at different thresholds" use case from #729 without taking on the full multi-tag taxonomy + auto- apply rules + per-tag alert system the ticket originally proposed. Form gains: - Category input with datalist autocomplete sourced from categories already in use, so casing/spelling stays consistent. - Per-spool low-stock threshold input. Empty = global default; the global value renders as the placeholder. Inventory page: - New category filter chip (hidden until at least one spool carries a category — keeps the chip row uncluttered). - Stat-card "Low Stock" count and the "Low Stock" filter both honour the per-spool override. Plus: rename "Delete Tag" button to "Clear RFID Tag" (the original ticket reporter mistook it for a taxonomy-tag delete; the button actually clears the RFID UID/UUID off the spool record). Toast key renamed from `tagDeleted` to `rfidCleared`. i18n: full translations across all 8 locales. Tests: 9 new backend schema tests (defaults, partial-update, range rejection, max-length); 2 new frontend tests (per-spool threshold pulls extra spools into low-stock count, filter chip hidden when no categories exist). |
||
|
|
12c01f029d |
Revert "feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login (#1118)"
This reverts commit
|
||
|
|
50382006b3 |
feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login (#1118)
feat(oidc): add Azure Entra ID support with configurable email claim resolution |
||
|
|
fcda728af4 |
feat(#1108): long-lived camera-stream tokens + fix(#1089) audit-pass tweaks
#1108 — Long-lived camera-stream tokens for HA / Frigate / kiosks. Camera-only V1, hard 365-day cap (no infinite tokens), pbkdf2 hashed at rest, plaintext shown to user exactly once on creation. New "Camera API Tokens" panel under Settings → API Keys with self-service create/revoke, styled confirm modal, admin "All users" view for leak triage. Auth path: /camera/stream tries the existing 60-min ephemeral table first, falls through to the long-lived path. Indexed lookup_prefix keeps verify O(1) per token. Permission audit: gated the existing API-keys-CRUD + Webhook docs + API Browser content behind api_keys:read so non-admins with camera:view land on the API Keys tab and see only the Camera Tokens panel they actually have permission to use. Grid layout collapses to single column for non-admins. Tests: 29 new backend (15 service + 14 integration covering create/list/ revoke ownership rules, the auth fall-through, scope enforcement, prefix collisions) + 6 new frontend tests for the section UI including the new modal flow. All 77 backend tests + 21 frontend camera tests pass. Ruff clean (lint + format). Docs: README updated with fan-out + long-lived-token bullets. Wiki gets a new "Long-Lived Camera Tokens" section under features/camera.md (HA YAML example, security model, permission requirements, revoke flow). Website features.html gets the bullet under Camera Streaming. Also includes #1089 follow-up tweaks already merged in this branch: _stream_start_times.setdefault for accurate stream_uptime, subscribe() RuntimeError retry to close the grace-vs-subscribe race, atomic unsubscribe count via the iter_subscriber on_unsubscribe callback. |
||
|
|
7f11618e1e |
Revert "feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login (#1103)"
This reverts commit
|
||
|
|
365c38483b |
feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login (#1103)
feat(oidc): add Azure Entra ID support with configurable email claim resolution fix(oidc): harden email claim resolution, guards, and test coverage |
||
|
|
9e938cbc8c |
Revert "feat(inventory): unified Spoolman inventory UI + Storage Location + AMS deep-link + SpoolBuddy NFC write support (#1063)"
This reverts commit
|
||
|
|
89f14c57ad |
feat(inventory): unified Spoolman inventory UI + Storage Location + AMS deep-link + SpoolBuddy NFC write support (#1063)
feat(inventory): replace Spoolman iframe with internal inventory UI When Spoolman is enabled, the Inventory page now uses the same internal UI (spool list, create/edit modal, archive, delete, weight sync) backed by a new proxy layer instead of opening an iframe. |
||
|
|
91a3d391ff |
feat(virtual-printer): add Tailscale opt-out toggle (closes #701 point 3) (#1070)
* feat(virtual-printer): add Tailscale certificate provisioning |
||
|
|
bf511c54cd |
feat(#1008): archive auto-purge + dedicated archives:purge permission
Adds an archive counterpart to the library trash sweeper shipped in the
previous commit. Unlike the library flow, archives are hard-deleted —
print history is a decaying timeline, so there is no trash intermediate;
download or favourite anything you want to keep first.
Backend
- New ArchivePurgeService (backend/app/services/archive_purge.py) with
its own 15-minute scheduler loop and a 24h throttle on actual purge
runs. Delegates every delete to the existing safety-checked
ArchiveService.delete_archive so the 3MF, thumbnail, timelapse, source
3MF, F3D, and photo folder all get cleaned up together with the DB
row. Per-row session via async_session() avoids commit-per-row churn
on any caller-passed session.
- New /archives/purge/{preview,settings} + POST /archives/purge routes
gated on a dedicated archives:purge permission (not archives:delete_all)
so admins can delegate bulk-delete to a role without granting
per-archive delete on other users' rows.
- seed_default_groups() now backfills both library:purge and
archives:purge on the Administrators group for upgraded installs —
the original library:purge was added after Administrators was first
seeded so the "create if not exists" path skipped existing DBs and
left admins without the permission.
- 8 new integration tests (defaults, settings roundtrip, bound
validation, preview, manual purge, auto-purge enabled path, 24h
throttle, disabled skip).
Frontend
- Settings → Archives card gains an auto-purge toggle + age input (7d
floor, 10y ceiling, 365d default), with a save-toast on every change.
The bulk "Purge old" button lives on the Archives page header
(rightmost, after Upload 3MF) to match the File Manager pattern —
configuration in Settings, one-shot action on the page.
- New PurgeArchivesModal mirrors PurgeOldFilesModal: live preview (count
+ total size freed + sample filenames) debounced at 300ms, amber
"hard-delete, no undo" warning.
- Admin-only UI gates on archives:purge via the standard hasPermission
hook; Permission TS union updated.
- i18n blocks across all 8 locales (en/de full, other 6 English
fallback per project convention).
Docs
- CHANGELOG entry under 0.2.4b1 following the existing library-trash
entry.
- bambuddy-wiki archiving.md gains a new "Auto-Purge" section.
- bambuddy-website features.html gets a matching bullet.
Verification: python -m ruff check backend/app/ clean; 25 integration
tests pass (8 archive_purge + 17 library_trash regression); npm run
build clean.
|
||
|
|
e0e597271e |
● feat(#1008): library trash bin, admin bulk purge, auto-purge setting
Library files now move to a configurable-retention trash bin on delete
instead of being hard-deleted from disk (default 30 days). Admins get a
"Purge old" bulk action on the File Manager with a live preview, plus an
optional auto-purge setting in Settings → File Manager that runs the same
operation once per 24h when enabled (default off). Regular users see and
manage their own trashed files; admins see everyone's. External (linked)
files bypass trash since their bytes aren't under Bambuddy's control.
- New `library:purge` permission (admin-only by default)
- Nullable indexed `deleted_at` column on library_files; dialect-aware
ALTER TABLE so the column actually gets added on PostgreSQL (raw
DATETIME is SQLite-only syntax)
- New `LibraryFile.active()` classmethod; every query site routed through
it so trashed rows don't leak into listings, print dispatch, MakerWorld
dedupe, or stats
- Trash page: select-all + bulk restore/delete, per-row checkboxes, wider
layout so datetime columns don't clip
- Auto-purge: 24h throttle via `library_auto_purge_last_run` setting so
the 15-minute sweeper cadence still runs the purge at most once per day
- Save toast wired into every trash/auto-purge setting change
- 17 new backend integration tests (service + routes + auto-purge throttle),
8 new frontend tests, localised across all 8 UI languages
- Wiki + website feature entries updated
|
||
|
|
5da403ba0c |
Feature/makerworld (#1099)
* feat(makerworld): URL-paste import and print for MakerWorld models
Add a dedicated /makerworld sidebar page where users paste a MakerWorld
model URL and get the full plate list + one-click "Import to Library" or
"Print Now". Closes the workflow gap that kept LAN-only users on the
Bambu Handy app solely for MakerWorld download-and-send.
The authenticated tier reuses the existing Bambu Cloud token that
Bambuddy already stores for firmware checks and slicer settings --
MakerWorld shares the same auth backend, so the same JWT works there.
No separate OAuth flow, no companion browser extension, no credential
hijack. Anonymous users can still paste a URL and see model metadata;
the 3MF download itself requires the Cloud login.
Print Now hands off to the existing PrintModal (plate picker + AMS
mapping + dispatch) so multi-filament models work via the same code
path as library-file prints. Imported 3MFs are stored through a new
shared save_3mf_bytes_to_library() helper so the multipart upload
route and the MakerWorld import route don't duplicate 3MF parsing +
thumbnail extraction logic.
LibraryFile gains indexed source_type + source_url columns. Re-pasting
a URL for a model already in the library returns the existing row
instead of re-downloading -- dedupe is by canonicalised URL, not SHA256,
because MakerWorld's download URLs are signed and change per request.
Thumbnail proxy (/makerworld/thumbnail) hot-links through the backend
instead of directly to makerworld.bblmw.com -- the SPA's img-src CSP
stays strict and users' IPs don't hit MakerWorld's CDN logs. The
endpoint is intentionally unauthenticated since <img> tags can't carry
a Bearer token; SSRF-guarded by a CDN host allowlist so it can't be
used as a generic proxy.
Search and browse-catalogue are explicitly out of scope. The public
design/search endpoint returns empty results from server-originated
requests (likely needs csrf/session state reproducible only from a
real browser), and the __NEXT_DATA__ HTML fallback is blocked by
Cloudflare. URL-paste covers the realistic discovery pattern (Reddit /
YouTube / shared links).
Headers match kloshi-io/makerworld-api-reverse's production-tested set
(User-Agent: 3d-printing-service/1.0, x-bbl-* client identifiers,
Referer). The /instance/{id}/f3mf call includes ?type=download which
community userscripts use to signal legitimate download intent. 418
responses (MakerWorld's CAPTCHA gate) retry once with backoff and then
surface a clear actionable error with an "Open on MakerWorld" fallback
link; we never try to evade bot detection.
Permissions: new makerworld:view (browse metadata, view thumbnails) and
makerworld:import (save 3MFs to library). Administrators and Operators
get both; Viewers get view-only. Migration grants these to existing
groups based on whether they already have library:upload / library:read.
Disclaimer in the UI and wiki page mirrors kloshi's framing: not
affiliated with or endorsed by MakerWorld or Bambu Lab, interoperability
only, not intended to circumvent access controls.
Tests: 30 backend (service + routes) + 4 frontend. Full backend suite
(1931 tests) clean. Frontend build clean.
* feat(makerworld): ship working URL-paste import via api.bambulab.com iot-service
The MakerWorld integration shipped in 0.2.4b1 dev was broken for most
public models: the makerworld.com/design-service path returns "Please
log in to download models" even with a valid Bambu Cloud bearer,
because it's cookie-gated behind Cloudflare. Published reverse-
engineering projects work around this by pasting browser cookies; we
route around it entirely by using the api.bambulab.com/iot-service
endpoint (documented by Pr0zak/YASTL#51), which accepts the same
bearer Bambuddy already has and returns a presigned S3 URL.
Working flow:
GET api.bambulab.com/v1/design-service/design/{id} → metadata
GET api.bambulab.com/v1/iot-service/api/user/profile/{pid}?model_id=<str>
Authorization: Bearer {cloud_token} → signed S3 URL
urllib.request (no redirects, no query re-encoding) → bytes
Notes on each step:
- The model_id query param is the alphanumeric string from the
design response (e.g. US2bb73b106683e5), NOT the integer designId
from the /models/{N} URL. The import route fetches design metadata
first to get it.
- S3 presigned URLs MUST be fetched with urllib (not httpx/curl_cffi)
because the signature is computed over exact query-string bytes;
any normalising encoder breaks it with SignatureDoesNotMatch 400s
(YASTL#52 hit the same issue). Wrapped in a no-redirect opener so
the .amazonaws.com host allowlist guarantee isn't bypassed by a
302 elsewhere.
- The canonical source_url now includes profile_id so different
plates of the same model get distinct library entries. Older rows
from dev builds keep the model-level URL; the resolve endpoint's
"already imported" check LIKEs both shapes.
UI rebuild:
- Per-plate Save + Save & Slice in Bambu Studio / OrcaSlicer (the
plate is unsliced source, so "Print Now" was misleading and is
replaced by an explicit slicer hand-off).
- Import all plates with sequential progress.
- Folder picker (default: auto-created top-level "MakerWorld"
folder, created on first import, folder tree invalidated so
File Manager shows it immediately).
- Image gallery per plate with keyboard-navigable lightbox.
- Recent imports sidebar (sticky on lg+, vertical list with
jump-to-library / slicer / open-on-makerworld icons).
- Inline follow-up actions on imported plate rows so the user
doesn't scroll back to a top-of-page card.
- Per-plate delete via the standard ConfirmModal (no window.confirm).
- Elapsed-time + phase label during import so the 10-30s synchronous
POST doesn't feel frozen.
- URL-change detection drops the preview when the pasted URL
diverges from the resolved one.
Security hardening (found in review):
- DOMPurify.sanitize on the MakerWorld HTML summary before
dangerouslySetInnerHTML (user-authored content).
- <img> tags in that HTML routed through the thumbnail proxy so
the SPA's img-src 'self' data: blob: CSP isn't widened.
- /makerworld/thumbnail uses follow_redirects=False (the host
allowlist only covers the initial URL).
- 3MF CDN fetch strips the bearer (signed URL is the credential).
- S3 fetch uses a no-op HTTPRedirectHandler for the same reason.
- Upstream filename is os.path.basename'd before persisting.
Tests: 46 backend service unit tests, 19 route tests, 12 frontend
tests — all passing. All user-facing strings localised across the
8 UI languages.
* - frontend/src/App.tsx — removed the 3 stale <AdminRoute> lines (kept the 3 <PermissionRoute> equivalents). TSC + Vite both clean.
- backend/tests/integration/test_auth_api.py — added # pragma: allowlist secret + # noqa: S106 on the test fixture line that GitGuardian flagged.
|
||
|
|
1682b6956f |
fix(dispatch): clean up transient library upload from Direct-Print flow (#730)
The "Print" button on a printer card (and drag-drop-onto-card) used
FileUploadModal to persist the file as a LibraryFile, then dispatched
through POST /library/files/{id}/print. The LibraryFile row + disk file
were left behind after every one-off print, polluting File Manager with
entries the user never asked to save.
FilePrintRequest.cleanup_library_after_dispatch (default False) opts
into post-dispatch cleanup. When set, _run_print_library_file stages
db.delete(lib_file) in the same transaction as archive_print so a
mid-flight FTP / start_print failure rolls both back cleanly, commits
together, then unlinks the library disk file + thumbnail after commit
succeeds. External library files (is_external=True) are never touched.
Only the Printers-page Direct-Print PrintModal sets the flag. Every
other api.printLibraryFile caller (File Manager Print, Project Detail
Print) leaves it unset — their entries are there by user intent.
Also moves formatPrintName out of PrintersPage.tsx into a new
utils/printName.ts module —
|
||
|
|
fa1c46d9a5 |
feat(printers): show plate name on card for multi-plate active prints (#881)
When two printers were running different plates of the same multi-plate 3MF, the Printers page cards displayed the same file name on both and there was no way to tell them apart. The Queue view already had this information by cross-referencing the archive's plate list; the card didn't have the linkage. Expose `current_archive_id` (resolved by matching the MQTT `subtask_id` against `PrintArchive.subtask_id` — the bridge introduced in #972 for restart-resume) and `current_plate_id` (parsed from `gcode_file` by a new shared `parse_plate_id` helper) on the status endpoint. The helper is also called from the WebSocket push path so plate transitions reflect within 100 ms instead of waiting 30 s for the next REST poll; the archive id itself stays REST-only since it's stable for the life of a print and shouldn't make the push path touch the DB. The card fetches plate metadata via the same `api.getArchivePlates()` call QueuePage uses — shared React Query cache keeps it cheap across polls — and renders the actual plate name (or a "Plate N" fallback) only when `is_multi_plate` is true. Single-plate prints stay clean. Falls back to the previous `plate_N.gcode` regex path when there's no archive linkage (e.g. prints started directly from the printer LCD). Tests cover the plate-id extraction across Bambu Studio path shapes (backend parse_plate_id, printer_state_to_dict wiring) and the label override precedence in formatPrintName (frontend). |
||
|
|
2366a1a0b3 | Fixed backup fie name | ||
|
|
baf0716a9a |
feat(cloud): support China region for token-based login (#1013)
feat(cloud): support China region for token-based login The /cloud/token endpoint always used the global Bambu API endpoint, so users with China-region access tokens could not validate their token. The password login flow already exposes a region selector; this brings the token flow to parity. |
||
|
|
a2c7fd4542 |
fix(obico): revert POST-bytes approach — Obico /p/ is GET-only
The 0.2.3b4 #1003 "fix" POSTed JPEG bytes as multipart form data,
but Obico's /p/ endpoint is declared methods=['GET'] upstream and
reads ?img=URL from the query string. Every POST was 405'd by
Flask's router before any handler ran, which is why the Obico
container logs were silent while Bambuddy kept reporting
"ML API call failed for printer N:" with a blank suffix —
raise_for_status() on the 405 produced an exception whose str()
rendered empty.
Restored the pre-#1003 nonce-URL approach (commit
|