mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
e01a67f58757310828cd0691da45991f4e45d25b
71
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c0c07bc509 |
fix(archives): cross-model re-slice no longer carries source printer_id
When the user re-sliced an H2D archive for X1C, the new archive card and
reprint modal both showed the source printer's name (e.g. "Workshop H2C")
even though sliced_for_model on the same row correctly read "X1C".
slice_and_persist_as_archive copied source_archive.printer_id verbatim
onto the new PrintArchive row. Both the archive card
(ArchivesPage.tsx:3571) and the reprint modal read printer_id first and
only fall back to sliced_for_model when it's None. With printer_id set
to the source's H2D printer, neither could see that the new file is for
a different model.
Same shape as the sliced_for_model fix already in the same function — a
cross-model re-slice means the source's physical printer isn't where
this output prints anymore. When the slicer-baked target model differs
from source_archive.sliced_for_model, drop printer_id so both surfaces
fall back to the sliced_for_model badge ("X1C"). Same-model re-slices
keep printer_id so the reprint modal still pre-selects the source
printer.
Edge case: when source_archive.sliced_for_model is None (older archives
that predate that column being populated), we can't tell whether this
is a cross-model re-slice. Fail open and preserve printer_id rather
than spuriously nulling it.
slice_and_persist (the library-file path) doesn't have this bug —
LibraryFile has no printer_id column.
Tests in TestSliceArchiveResliceModel cover all three branches: cross-
model nulls printer_id; same-model keeps it; unknown source model
preserves it.
Surfaced via the bambuddy demo doing H2D -> X1C re-slices after the
.bbscfg System-tier slicing fix landed.
|
||
|
|
4686d108ef |
feat(slice): cross-printer re-slicing across nozzle classes + multi-plate slice-all
Re-slicing a 3MF authored for a single-nozzle printer (X1C, P1S, A1, P2S)
onto a dual-nozzle printer (H2D / H2D Pro) — or vice versa — previously
failed with "G-code in unprintable area of multi-extruder printers" (the
source's bed-coordinate layout lands in the H2D's per-nozzle dead zone)
or, on multi-color projects, a hard SIGSEGV inside the slicer's ZFiller
polygon-clipping. Earlier shipped a fail-fast 400 guard; this drop lifts
it and actually does the conversion by forwarding the sidecar's existing
--arrange flag when the source and target nozzle classes differ. BS
itself reconciles the embedded project_settings.config against the new
printer that way, the same way the GUI's "Switch Printer" operation
does. The guard becomes a kept-for-compat no-op.
Slice-all-plates added to the SliceModal: a checkbox for multi-plate
sources sends plate=0 to the backend, which forwards --slice 0 to the
BS CLI. Same-class slice-all produces one multi-plate output 3MF in a
single sidecar call. Cross-class slice-all loops per plate (BS's
--arrange is project-wide and would otherwise consolidate every plate's
objects onto one bed) and merges the per-plate outputs into one
multi-plate 3MF locally via the new merge_plate_3mfs helper. The toast
shows "Plate 2 of 5 — Generating G-code (47%)" through the loop.
Three side fixes surfaced during testing:
- substitute_unused_plate_filaments overwrites unused-slot filaments
with the slot-1 selection before slicing so BS's loaded-filament
temperature validator doesn't reject a PLA print whose unused slot 2
defaulted to ABS in the dropdown
- re-sliced archive thumbnail now prefers the source's per-plate
render (Metadata/plate_N.png) over the project-wide MakerWorld cover
art, because BS CLI with --arrange skips writing a fresh per-plate
preview
- re-sliced archive bed_type now lifts from the sliced output's
curr_bed_type onto the PrintArchive column the card actually reads
Schema: SliceRequest.plate range relaxed from ge=1 to ge=0 to admit
the "all plates" sentinel; SlicerApiService.slice_with_profiles /
slice_with_bundle take an `arrange` parameter.
Tests: 26 in test_slicer_3mf_convert (count / merge / substitute /
extract), 3 in test_slicer_api (arrange wire format), 9 in
test_library_slice_api (guard no-op, bed_type lift, thumbnail
fallback, new cross-class slice-all loop integration test), 2 in
test_archive_service (Auxiliaries fallback), 4 in SliceModal.test
(plate=0 toggle), 2 in SliceJobTrackerContext.test (multi-plate toast
prefix). 659 backend + 42 frontend green; backend ruff clean,
frontend build clean, i18n parity green at 4984 keys × 9 locales.
|
||
|
|
4925b4c830 |
fix(slice): re-slice correctness — model label, honest errors, filament usage, nozzle guard
Five follow-up fixes to cross-printer re-slicing, all surfaced while
testing archive re-slices.
1. Re-sliced archive now records the printer it was sliced FOR.
slice_and_persist_as_archive copied sliced_for_model from the source
archive, so re-slicing X1C->H2D still showed "X1C sliced". Read it
from the freshly-sliced 3MF's parsed metadata instead, falling back
to the source only when absent.
2. Real slicer rejections are surfaced instead of silently masked.
_run_slicer_with_fallback retried with the 3MF's embedded settings on
any sidecar 5xx — including genuine content rejections (object off
the bed, incompatible filament temps), which "succeeded" only by
re-slicing for the source's original printer. A new
_slicer_rejection_message detects the slicer's own error string and
surfaces it as a 400; the embedded-settings fallback is kept only for
true CLI crashes.
3. A failed slice opens an error modal, not a 3s toast. The slicer's
reason is actionable and a toast hides it before it can be read. New
AlertModal (acknowledge-only); SliceJobTrackerContext shows it on a
failed job. New slice.failedTitle key in all 9 locales.
4. Sliced files no longer report "0 g" filament usage. The sidecar
doesn't always populate the X-Filament-Used-* headers;
ThreeMFParser._parse_gcode_header now also reads the slicer's own
"total filament weight/length" from the G-code header, and both
slice-persist paths fall back to it when the sidecar reports 0.
5. Nozzle-class re-slice guard. Re-slicing across the single-nozzle <->
dual-nozzle boundary (e.g. X1C -> H2D) fails BambuStudio's
multi-extruder validation; both slice routes now reject it up front
with a clear 400. The dual-nozzle model classification — previously
an inline tuple duplicated across start_print and the K-profile
routes — is centralized into DUAL_NOZZLE_MODELS / is_dual_nozzle_model
in printer_models.py, consumed by all three sites and the guard.
Full cross-nozzle-class re-slicing (dual-nozzle project_settings
reconciliation) remains separately tracked.
Tests: _slicer_rejection_message, _canonical_printer_model,
guard_nozzle_class_reslice, is_dual_nozzle_model, the G-code-header
filament parse, AlertModal, and end-to-end slice-API coverage including
an X1C-archive-to-H2D 400. Backend ruff + i18n parity clean; frontend
build clean.
|
||
|
|
71e58e6cf1 |
fix(library): show the filename, not the embedded 3MF Title (#1489)
File Manager cards, search and sort keyed off file_metadata.print_name, which ThreeMFParser lifts from the 3MF's <metadata name="Title">. That title is the in-app project title — generic "Exported 3D Model" for any Bambu Studio "Save As", a marketing title for a MakerWorld download — and almost never the filename the user saved as. A card for Whatever.3mf showed "Exported 3D Model"; correcting it needed a rename round-trip, since the Rename dialog disables Save while the name is unchanged. The slicer-output write path already dropped print_name for this exact reason; the four other paths that store parsed 3MF metadata onto a LibraryFile did not — external-folder scan, managed multipart upload, the multi-file ZIP-upload branch, and MakerWorld import. Add a shared _without_print_name() helper and apply it at all four import paths; switch the slicer path to it so there is one rule. A LibraryFile's display name is its filename — only PrintArchive carries a real print_name, which is untouched. Remove the now-redundant filename->print_name mirroring in the rename route. Add a one-time idempotent data migration (_migrate_drop_library_print_name, SQLite json_remove / PostgreSQL jsonb key-removal branched on is_sqlite()) so libraries imported before the fix correct themselves without the rename workaround. No frontend change: print_name || filename yields the filename once print_name is gone. Tests: 6 new in test_library_print_name.py cover _without_print_name and the migration (incl. idempotency, siblings preserved, null metadata). SQLite migration branch verified by test; PostgreSQL branch verified against a real Postgres instance. |
||
|
|
e738645b0d |
feat(slicer): filter slice profiles by printer + default from the 3MF (issue #1325)
The Slice dialog listed every process / filament preset regardless of the chosen printer, and always defaulted to the first listed preset rather than what the 3MF was prepared with (#1325). Matching uses the slicer's own compatible_printers list for imported (local) presets and falls back to the "@BBL <model>" name suffix for cloud / standard presets. Compatibility-unknown presets are never hidden. Defaults: the printer and process dropdowns default to the preset names embedded in the source 3MF's project_settings.config when those presets are available; the per-slot filament and process pre-picks prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. - UnifiedPreset gains compatible_printers, exposed for the local tier - plates endpoints return embedded_printer / embedded_process - new frontend util slicerPrinterMatch.ts; extract_embedded_presets_from_3mf - slice.otherPrinters added across all 9 locales |
||
|
|
ed27b27adb |
feat(slice): cross-printer re-slicing — drop the gate, the banner, and the dead plumbing
Step 0 empirical test on 2026-05-20 disproved the "CLI cannot re-slice a
3MF for a different printer" assumption: feeding an 18-color H2D-bound
Trent900.3mf to the X1C bundle via /slice produces valid X1C G-code in
1.8s, with bed (256x256), kinematics, nozzle count, machine_start_gcode,
and bed_exclude_area all coming from the target bundle.
- SliceModal: drop !printerMismatch from isReady; remove the banner and
the sourcePrinterModel / printerProfileName / printerMismatch state
entirely. Cross-printer slicing is now indistinguishable from a normal
slice; the picker already shows the target printer.
- Remove slice.printerMismatch from all 8 locales.
- API cleanup: drop source_printer_model from /library/files/{id}/plates
and /archives/{id}/plates responses, drop the field from
frontend/src/types/plates.ts (PlateMetadata + LibraryFilePlatesResponse),
delete extract_source_printer_model_from_3mf from threemf_tools.py and
its 6 unit tests. Zero remaining consumers.
- i18n discipline cleanup in SliceModal.tsx (same drop): strip every
inline English defaultValue / positional fallback from t() calls (22
sites). Add slice.bundle / slice.bundleNone / slice.bundleAllRequired
to all 8 locales — they had no entry in any locale file and were being
served from the inline English fallback for every non-English user.
- Tests: rewrite the mismatch-warning test to assert "no banner, Slice
enabled" when models differ (regression guard); delete 2 obsolete
tests covering gate states that no longer exist.
|
||
|
|
bff240e90a |
fix(uploads): pre-flight validation for 3MF/gcode + visible upload errors (#1401)
Reporter @iitazz uploaded slicer output to Bambuddy, clicked Print,
and the printer rejected every job with "Printing stopped because
the printer was unable to parse the 3mf file". Support bundle showed
the stored library file ended in .gcode (not .gcode.3mf), and
background_dispatch.py appends ".3mf" to filenames that don't
already end in .gcode.3mf/.3mf — so raw gcode shipped to the printer
named .gcode.3mf and the firmware's 3MF parser choked. Same shape
also surfaced as "File is not a zip file" on Bambuddy's own plate
parser.
New validate_print_file_upload() helper in library.py runs at upload
time:
- Reject filenames ending in .gcode (but not .gcode.3mf) with a
clear message — Bambu printers need .gcode.3mf zip containers,
not raw gcode.
- For .3mf / .gcode.3mf uploads, verify body starts with PK\x03\x04
(ZIP magic); reject otherwise pointing at the slicer's "Export
Plate Sliced File" action.
Applied to every relevant upload route: POST /library/files (covers
File Manager + printer-card drag-drop), POST /archives/upload,
POST /archives/upload-bulk (rejects per-row so one bad file doesn't
abort the batch), POST /archives/{id}/source, POST /archives/upload-source.
Runs after _resolve_upload_destination so folder-permission errors
(403 readonly, 400 missing-path, 409 collision) still take precedence.
STL / image / other non-print uploads bypass the validator.
FileUploadModal frontend fix: the modal auto-closed after every
batch regardless of per-file results, so a 400 rejection was captured
but invisible. Now:
- Errors render inline as red text under the file row instead of
as a hover-only title tooltip.
- Modal stays open if any file ended with status='error', so the
user can read the backend's remediation message before closing.
- Successful-only batches still auto-close as before.
UploadModal (bulk archive) was already showing inline errors and
not auto-closing — no change needed there.
|
||
|
|
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.
|
||
|
|
db308aa80b |
fix(library): defer external-scan STL thumbnails + Path coerce (#1299)
External scan hung on a 1200-subdir NAS because (1) every STL crashed
with TypeError ('str / str') inside generate_stl_thumbnail and (2)
thumbnail generation ran synchronously per file, so the FE timed out
before db.commit() and nothing was persisted.
stl_thumbnail.py now coerces inputs to Path defensively, and
scan_external_folder defers STL thumbnail generation to a background
asyncio task that opens its own session and processes each file
post-commit. Subdirs appear in the sidebar immediately; thumbnails
backfill over the next seconds/minutes.
|
||
|
|
7e1105dcb6 |
feat(slicer): bundle dispatch path for library slice route
When SliceRequest.bundle is set, the dispatch picks the per-category
JSON triplet from a sidecar-stored .bbscfg by name instead of
resolving cloud/local/standard PresetRefs. Mirrors the bundle-aware
preview slice (committed earlier) so live slices match the same
profile triplet the modal previewed against.
Schema:
- SliceBundleSpec: bundle_id + printer_name + process_name +
filament_names (min-length-1 list, plate-slot order)
- SliceRequest.bundle: optional, validator skips preset-required
check when set so bundle-only requests validate
Dispatch:
- _run_slicer_with_fallback branches on request.bundle
- Skips resolve_preset_ref, calls slice_with_bundle
- 3MF + bundle CLI 5xx still falls back to embedded-settings slice
(used_embedded_settings=True surfaces in the response)
- Sidecar 404 (unknown bundle / preset name) maps to 400
|
||
|
|
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
|
||
|
|
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. |
||
|
|
aecf8283b5 |
fix(slicer): strip "-1" inherit sentinels from project_settings.config (#1201)
MakerWorld 3MFs sliced for P2S (and likely other Bambu printers) ship project_settings.config entries with "-1" on fields BambuStudio writes to mean "inherit from the parent process preset". The headless slicer CLI's StaticPrintConfig validator runs against the embedded settings *before* --load-settings overrides apply, so the sentinel trips the field's lower-bound check and the CLI exits non-zero before the supplied profile triplet is consulted. Both slice paths (with-profiles and the embedded-settings fallback) read the same config and fail the same way, so the user surfaces the error. Surgical fix: open Metadata/project_settings.config, remove allowlisted keys when their value is exactly "-1", re-zip. Allowlist starts with the two from this report (raft_first_layer_expansion, tree_support_wall_count) plus prime_tower_brim_width (a known sentinel cited in earlier reports). Non-allowlisted "-1" values are left alone so a blanket strip can't corrupt legitimate negative values. Other zip entries pass through byte-identical — no risk of the failure mode the previous full-strip experiment hit (silent CLI exit on initialisation). Sanitiser runs before both slice_with_profiles and slice_without_profiles paths since both fail on the same sentinel. 13 new unit tests in test_project_settings_sentinel_sanitiser.py cover the allowlist removal contract (parametrised across all sentinel keys), preservation of unaffected values, byte-identical round-trip of unrelated zip entries, and defensive fallbacks for non-zip / malformed / no-config inputs. Adding new sentinel keys is one-line: append to _PROJECT_SETTINGS_SENTINEL_KEYS in library.py. |
||
|
|
d81040607e |
fix(api-keys): slice + slicer-presets routes resolve cloud token via key owner (#1182 follow-up)
turulix's headless slicing pipeline got cloud preset IDs from /api/v1/cloud/settings (the /cloud/* gate from #1182 worked), but slicing those IDs via POST /library/files/{id}/slice failed with "no Bambu Cloud session is stored" — the slice route lives on a different router, never saw the api_key_owner stash, and _resolve_cloud fell through to the empty auth-disabled global Settings token. Add a permissive route-level dep that returns the API key's owner when the key has the cloud scope and None otherwise (never raises), so non-/cloud/* routes can opt in without breaking the local-preset path. Wire it into POST /library/files/{id}/slice and GET /slicer/presets (same root cause, would hit any UI proxied through an API key). The route picks current_user or api_key_cloud_owner before deriving user_id. Auth gate's None-return for API keys is unchanged — keeping the owner-resolution scoped to the routes that actually need a cloud token prevents scope creep into routes that fence on ``current_user is None``. |
||
|
|
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.
|
||
|
|
3fa0b621dd |
fix(slicer): correct multi-color slice output + UX polish across the slice pipeline
The slicer CLI silently substitutes embedded defaults for any AMS slot
the user didn't supply a profile for. When a multi-color project (e.g.
a MakerWorld helmet with white shell + grey support filament configured
project-wide) was sliced for a "single-color" plate, the CLI took the
user's white pick for slot 1 and quietly filled slot 2 with the source
3MF's embedded grey support filament — producing a slice the user never
asked for. Same silent-fallback class as the strip-removal bug.
Backend `/filament-requirements` now returns the FULL project AMS slot
list (from project_settings.config) with a `used_in_plate: bool` flag
per entry. The flag comes from the cached preview slice for unsliced
files; sliced files (where slice_info.config already pre-filters by
used_g > 0) get used_in_plate=true on every entry. SliceModal renders
one dropdown per project slot — slots flagged used_in_plate=true are
editable, slots flagged false are auto-picked from project metadata
via the existing colour-match scoring and disabled with a
"-- not used by this plate" suffix. The wire format always carries a
profile per project slot, so the CLI never falls back to embedded
defaults.
Adjacent UX fixes in the same session, since they all hit the same
flow:
- Persistent slice-progress toast: SliceJobTrackerProvider now opens
a persistent loading toast per active job ("Slicing X -- 47s") with
a 1Hz elapsed-time tick and replaces it with the existing transient
success/error toast on terminal state. The previous start+finish
toast pair left a UX dead zone where users couldn't tell whether a
long slice was still running.
- "Analyzing plate filaments..." spinner now shows elapsed seconds and,
after 5s, a hint that the wait is a one-time preview slice (cached;
re-opens are instant). Addresses "is anything happening?" on first
open of an unsliced complex multi-color 3MF.
- Pre-slice printer-mismatch warning + disabled Slice button: the
plates response now exposes `source_printer_model` from
project_settings.config; SliceModal compares against the picked
printer profile name and surfaces an inline warning + disables Slice
on mismatch. The CLI rejects cross-printer slices (rc=-16) and used
to fall back to embedded settings, producing wrong-printer g-code
that errored at print dispatch.
- Sliced-archive card now reflects the actually-used filament list,
not the source's project-wide AMS config:
slice_and_persist_as_archive reads filament_type / filament_color
from the sliced output's slice_info.config (which already gates on
used_g > 0) instead of inheriting from the source archive. A 16+
swatch card on what was actually a 2-color print was the visible
symptom.
- MakerWorld URL-paste resolver enriches each instance with
`compatibility` + `otherCompatibility` from
design.instances[].extention.modelInfo. The /instances/hits payload
omits this so every instance row used to look identical; users
blindly picked the first one regardless of whether it matched their
printer.
Tests: 27 SliceModal tests (2 new for the disabled-row contract +
3 for printer-mismatch + 4 for multi-color rendering); 4 new
SliceJobTrackerContext tests for the persistent-toast lifecycle;
backend filament-requirements / slice-preview / threemf-tools
suites green.
i18n: new keys slice.queuedToast, slice.runningToast,
slice.analyzingPlateFilamentsHint, slice.notUsedByPlate,
slice.printerMismatch, makerworld.slicedFor, makerworld.alsoCompatible
across all 8 UI languages (English + German fully translated, the six
others seeded with English copies pending native translation, matching
the project's existing flow for newly-added user-facing features).
|
||
|
|
c1f69ee0cc |
fix(slicer): wrong-printer slicing + sliced-archive filament list + per-instance MakerWorld compat
Five stacked slice-pipeline bugs that each made the modal's profile picker
theatrical for 3MF inputs:
(1) `_strip_3mf_embedded_settings` removed `model_settings.config` /
`slice_info.config` / `cut_information.xml` along with
`project_settings.config`. The CLI silently exited after
"Initializing StaticPrintConfigs" — exit 0, no result.json — and
Bambuddy masked the failure by re-running with embedded settings
and the source's bound printer. Strip removed from the dispatch
path entirely.
(2) Standard-tier preset stubs lacked the `type` field, so the CLI
rejected `--load-settings` with rc=-5 ("input preset file is
invalid") and the same masking fallback fired. Added
`_SLOT_TO_PROFILE_TYPE` so each stub carries the right
machine/process/filament discriminator.
(3) Sliced-archive cards listed every project-wide AMS slot (16+
swatches for a 2-color print). `slice_and_persist_as_archive` now
reads `filament_type` / `filament_color` from the sliced output's
`slice_info.config` (which `ThreeMFParser` already gates on
`used_g > 0`) instead of inheriting from the source archive.
(4) SliceModal had no warning when the picked printer profile didn't
match the source 3MF — the CLI rejects cross-printer slices
(rc=-16) and fell back to embedded settings, producing wrong-printer
g-code that errored at print dispatch. Plates response now exposes
`source_printer_model`; the modal compares against the picked
profile name and disables Slice + shows an inline warning on
mismatch.
(5) MakerWorld URL-paste resolver listed plate instances without
showing which printer each was sliced for (`/instances/hits`
omits compatibility info that lives on `design.instances[]
.extention.modelInfo`). The resolve route now joins both payloads
by instance ID and forwards `compatibility` + `otherCompatibility`
onto each hit; the MakerWorld page renders "Sliced for {primary}"
+ "Also marked compatible: ..." per row.
Tests: 6 unit tests for `extract_source_printer_model_from_3mf`, 1 for
filtered filament metadata via ThreeMFParser, 2 for makerworld resolve
compat-merge (happy path + missing modelInfo), 3 frontend SliceModal
tests for the printer-mismatch warning + Slice-disabled gate. New i18n
keys `slice.printerMismatch`, `makerworld.slicedFor`,
`makerworld.alsoCompatible` across all 8 locales.
|
||
|
|
4acdcd7203 |
● 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.
- slice_and_persist_as_archive now reads filament_type / filament_color
from the SLICED OUTPUT's slice_info.config (via ThreeMFParser, which
already gates on used_g > 0) instead of inheriting from the unsliced
source archive. Without this, archive cards for sliced multi-color
prints showed every project-wide AMS slot — 18 swatches for a
2-color print — instead of just the filaments actually consumed.
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).
|
||
|
|
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
|
||
|
|
8829bc2cc6 | Merge branch 'dev' into feature/slicer-api | ||
|
|
d81e4853ec |
fix(#1112): cross-boundary file move actually relocates bytes
@Carter3DP's report on 0.2.4b1: a file moved into an external (NAS)
folder showed up in Bambuddy under that folder but was never written
to the mount. Traced to move_files only updating file.folder_id in
the DB while leaving the bytes in library_files_dir/. Direct upload
to a writable external folder was already fixed in 0.2.4b1; the move
path was not.
Cross-boundary moves now physically relocate the bytes through a new
_move_file_bytes helper. Same-boundary moves (managed -> managed)
keep the existing DB-only fast path because a managed file's on-disk
location doesn't depend on which managed folder owns it.
Four flows:
- managed -> external: copy to <mount>/<filename>, set
is_external=True, store the absolute path,
unlink the managed source
- external -> managed: copy to internal storage with a fresh UUID
name, set is_external=False, store the
relative path, unlink the external source,
recompute file_hash (scan-tracked rows
carry file_hash=None)
- external -> external: same shape as managed -> external
- managed -> managed: DB-only
Copy-then-unlink ordering means a partial copy followed by a failed
unlink leaves both copies on disk rather than losing the source if
the target write fails halfway through on a flaky NAS mount. Failed
shutil.copy2 cleans up partial dest before raising.
Defence-in-depth skips:
- source on a read-only external mount (move = delete-on-source
which a RO mount can't fulfil)
- filename collision on the target mount
- traversal-style filenames after Path.resolve()
- missing source on disk
- os.access(W_OK) on the target mount
Each skip carries a structured {file_id, code, reason} entry in a new
skipped_reasons field on the response so the UI can surface "5 of 10
skipped: 3 collisions, 2 missing on disk" instead of a blank number.
The {moved, skipped} numeric counters are preserved so existing
frontend code keeps working.
6 new integration tests in test_external_folders_api.py::
TestCrossBoundaryMove covering: managed -> external relocates bytes
(the actual fix), external -> managed relocates bytes including hash
recompute, name collision skip with the pre-existing target file
intact, source-readonly skip, managed -> managed stays DB-only, and
skipped_reasons always present.
|
||
|
|
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).
|
||
|
|
794cb6c6bd |
fix(#1112): write uploads to external folders through to the mount
POST /library/files only rejected the read-only external branch and then unconditionally wrote to get_library_files_dir() with a UUID filename. The resulting LibraryFile row pointed at the external folder via folder_id, so the file showed up in Bambuddy's UI, but the bytes physically lived in archive/library/files/ and never touched the mount -- invisible from any other machine accessing the NAS/SMB share. Writable external uploads now write through to <external_path>/<filename> with the original filename preserved, and the DB row matches what scan produces (is_external=True, file_path=<absolute mount path>). Collisions return 409 instead of silently overwriting; inaccessible or non-writable mount returns 400; path-traversal filenames are rejected via resolve + relative_to. Extract-zip is now rejected against any external folder (not just read-only) with a clear "extract on the mount and run Scan" message -- the nested-subfolder creation path would need mkdir on the mount plus matching is_external LibraryFolder rows, which is a separate design. Scan already handles that shape. |
||
|
|
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.
|
||
|
|
3adce435ee |
feat: add embedded GCode viewer (#963)
* feat: add embedded GCode viewer Adds PrettyGCode as a built-in GCode visualiser embedded directly in the Bambuddy layout, so users can preview and inspect GCode files without leaving the dashboard. |
||
|
|
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 —
|
||
|
|
f03d0c4cf7 |
fix: attribute direct library prints to the authenticated user
The library POST /files/{file_id}/print endpoint discarded the
authenticated user and passed requested_by_user_id=None to the
dispatcher, so archives created from direct prints had no
created_by_id and didn't show up in per-user statistics. Queue
and reprint paths already forwarded the user correctly.
Bind the auth dependency to current_user and pass its id/username
through to dispatch_print_library_file, matching the reprint
endpoint. The dispatcher already propagates this to
printer_manager.set_current_print_user, which the archive
creation reads.
|
||
|
|
ba1c97c808 |
feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933)
feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933) |
||
|
|
f95b2acd7d |
Feature: print files directly from project view (closes #930) (#932)
* feat: print files directly from project view (closes #930) Show printable files from linked library folders directly in the project detail page, with Print Now and Add to Queue buttons per file. Removes the detour through the File Manager for common reprint workflows. |
||
|
|
1645b51dad |
Add external folder subfolder preservation and fix file manager stale UI after delete
External folder scan now mirrors disk subfolder structure into the folder tree instead of flattening all files into root. Hidden directories are filtered, orphaned subfolders are cleaned up on rescan. Fixes #890. File manager delete endpoints (folder, file, bulk) now commit before returning the response — previously relied on post-response auto-commit, causing a race where the frontend refetch arrived before the commit. |
||
|
|
f3b2a24fa0 |
Fix external folder scan 500 on 3MF files (#846)
scan_external_folder stored raw 3MF metadata containing _thumbnail_data bytes directly in the JSON column, causing a serialization error. Applied the same clean_metadata() pattern used by upload/zip extraction and removed the call to the non-existent parser.extract_thumbnail(). |
||
|
|
3887938e8f |
Security: add token-based auth for all media endpoints
Camera streams, snapshots, thumbnails, timelapse videos, photos, QR codes, and cover images served via <img>/<video> tags were previously unauthenticated because browser media elements cannot send Authorization headers. When auth is enabled, these endpoints are now protected by a reusable stream token (?token=xxx) obtained from POST /printers/camera/stream-token (requires CAMERA_VIEW permission). |
||
|
|
95c703282d |
Fix missing image thumbnails in external folder scan (#124)
External folder scan generated thumbnails for 3mf/stl/gcode files but skipped image files. Added IMAGE_EXTENSIONS check with create_image_thumbnail() to the scan loop. |
||
|
|
82278e8d93 |
Add external folder mounting for File Manager (#124)
Host directories (NAS, USB, network shares) can now be mounted
into the File Manager without copying files. Files are indexed
into the database on scan but read directly from their original
location. Supports read-only mode, hidden file filtering, and
automatic thumbnail extraction for 3MF/STL/gcode.
- POST /library/folders/external — create with path validation
- POST /library/folders/{id}/scan — discover/sync files
- Block uploads, moves, and deletes for read-only external folders
- Never delete actual files from external paths (DB-only removal)
- Purple folder icon + info bar with rescan button in UI
- i18n for all 7 languages
- 19 backend + 11 frontend tests
|
||
|
|
bbc5ccb982 |
Library Upload Doesn't Show New File Until Page Reload ([#704](https://github.com/maziggy/bambuddy/issues/704)) — After uploading a file in the Library file manager, the file list didn't update until the user reloaded the browser. The upload endpoint used db.flush() instead of db.commit(), so the new row was only written to the database *after* the response was sent to the client. The frontend immediately refetched the file list upon receiving the response, but a new database session couldn't see the uncommitted row — resulting in stale data. Fixed by committing before the response is returned. Also fixed the same race condition in folder create, folder update, and file update endpoints. Reported by @shadowjig.
Printer File Manager Doesn't Auto-Refresh ([#704](https://github.com/maziggy/bambuddy/issues/704)) — The printer file manager (SD card browser) only fetched the file list once when opened. Files uploaded from BambuStudio/OrcaSlicer while the modal was open wouldn't appear until the user clicked the refresh button or reopened the modal. Now auto-refreshes every 30 seconds while open. Reported by @shadowjig. Database Connection Pool Exhaustion Under Load ([#704](https://github.com/maziggy/bambuddy/issues/704)) — Background tasks (print scheduler FTP uploads, camera captures, notification sends, timelapse stitching) held database sessions open during slow network I/O, consuming connection pool slots for seconds at a time. With the default pool of 15 connections (size 5 + overflow 10), concurrent operations during print start/complete events could exhaust the pool, causing `QueuePool limit reached` errors and `greenlet_spawn` failures in RFID spool auto-assignment. Doubled the pool to 30 connections (size 10 + overflow 20). Reported by @shadowjig. |
||
|
|
7584a2455c |
Fix File Manager rename not updating displayed name (#460)
The rename endpoint updated `filename` but not `file_metadata.print_name`, which the UI uses as the primary display name. Since print_name is extracted from the 3MF at upload, it always took precedence over the renamed filename. Now also updates print_name in file metadata when present. |
||
|
|
4a625c1ef2 | Add background print dispatching + tests | ||
|
|
9c81657df7 |
Fix Print Queue showing UUID hash instead of filename (#438)
Library files are stored on disk with UUID-hex filenames for uniqueness, but archive_print() used the disk path as the display name. Pass the original LibraryFile.filename through from both the print scheduler and direct-print-from-library flow so the archive's filename, print_name, and directory name all use the human-readable name. |
||
|
|
a903816b8a |
fix(slicer): token-based auth for Open in Slicer downloads (#433)
Slicer protocol handlers (bambustudio://, orcaslicer://) launch the
slicer app which fetches files via HTTP but cannot send auth headers.
The global auth middleware returned 401, causing "importing failed."
Add short-lived, single-use download tokens: the frontend fetches a
token via authenticated POST, then builds a /dl/{token}/{filename} URL
the slicer can access without auth headers. Tokens are validated
server-side (5-min expiry, single-use). Applies to archive files,
source 3MFs, and library files.
Also fix platform-specific URL formats to match slicer source code:
- macOS: bambustudioopen:// with encodeURIComponent
- Windows/Linux: bambustudio://open?file= (was wrongly using macOS scheme on Linux)
- OrcaSlicer: orcaslicer://open?file=
|
||
|
|
cf7c459099 |
fix(inventory): fix usage tracking for remapped AMS slots and slicer prints
Three fixes for inventory spool usage not updating after prints: 1. Store ams_mapping from print command (reprint, library print, queue) so the usage tracker maps 3MF slots to the actual physical trays the user selected, not the default slicer mapping. 2. Track last_loaded_tray on printer state — the last valid tray_now (0-253) seen during printing. On H2D printers, tray_now is always 255 in AMS data; the real tray resolves via snow field ~44s after print start but reverts to "unloaded" at completion. The fallback chain is: tray_now_at_start > current tray_now > last_loaded_tray. 3. Use color similarity (Euclidean RGB distance, threshold 50) instead of exact hex match for auto-unlink fingerprint checks. RFID sensors report slightly different shades across reads (e.g. distance ~43.6 for the same spool), causing false assignment unlinks after prints. |
||
|
|
f9b47282a1 |
Nozzle-aware AMS mapping for dual-nozzle printers, BL spool detection fix, AMS startup fix, SQLite WAL (#318)
Dual-nozzle H2D/H2D Pro: filament matching now respects nozzle assignments from the 3MF file. Each AMS unit feeds a specific nozzle (L/R), and the scheduler/frontend constrain matching to only trays on the correct nozzle. Falls back to unfiltered matching when no trays exist on the target nozzle. L/R badges shown in the filament mapping UI. Translated in en/de/ja/it. Fix AMS slot config overwritten on startup: on_ams_change unconditionally unlinked BL spool assignments on every MQTT pushall, then re-assigned them sending ams_filament_setting without setting_id — clearing the printer's filament preset. Now compares spool RFID identifiers before unlinking. Fix BL spool detection false positives: removed tray_info_idx from detection logic in both backend is_bambu_lab_spool() and frontend isBambuLabSpool(). Third-party spools using Bambu generic presets had GF-prefixed tray_info_idx values, causing misidentification. Now uses only tray_uuid and tag_uid. SQLite WAL mode with 5s busy timeout reduces "database is locked" errors. |
||
|
|
4b46e443dc |
Fix critical FTP upload failure and revert dangerous exception narrowing
The CodeQL cleanup in "Housekeeping" (
|
||
|
|
5dcabbdda8 |
Remove 30 redundant function-level imports
These modules were already imported at the top of each file. Removes re-imports of re, json, zipfile, and logging from inside functions in archive.py, library.py, main.py, printers.py, support.py, and test_library_api.py. Resolves all 30 CodeQL py/repeated-import findings. |
||
|
|
5b0a985da2 |
Add explanatory comments to 265 empty except blocks
CodeQL flags except blocks where `pass` has no comment explaining why the exception is silently ignored (py/empty-except rule). Added context-specific comments to all 265 instances across 31 files: - database.py (~112): ALTER TABLE migrations — "Already applied" - archive/library/3MF parsing (~64): "Skip unparseable metadata" - virtual_printer network cleanup (~32): "Best-effort socket cleanup" - discovery/SSDP (~13): "SO_REUSEPORT not available" / socket cleanup - bambu_ftp/mqtt (~13): FTP cleanup, JSON decode, signal parsing - remaining routes/services (~31): context-specific comments |
||
|
|
53bd4fadb3 |
Fix safe security findings: hashlib, log injection, broad excepts
- Add usedforsecurity=False to MD5 (AMS fingerprint) and SHA1 (git blob hash) calls to silence Bandit B303 / CodeQL weak-crypto findings - Convert ~996 f-string logging calls to parameterized %s-style across 55 files to prevent log injection (Bandit G201 / CodeQL log-injection) - Narrow ~199 broad except Exception blocks to specific types: OperationalError for DB migrations, OSError for network/file cleanup, (OSError, ftplib.error_reply) for FTP, and targeted tuples for ZIP/XML/JSON parsing — 36 intentionally left broad (mixed async, re-raise patterns) |
||
|
|
4b3b615a2c |
Fix Bandit B314: Replace xml.etree with defusedxml
Security scan (Bandit) identified vulnerable XML parsing in 3MF file processing. The standard xml.etree.ElementTree is vulnerable to XXE (XML External Entity) attacks. Changes: - Add defusedxml>=0.7.0 to requirements.txt - Replace all xml.etree.ElementTree imports with defusedxml.ElementTree in production code (6 files) Affected files: - backend/app/services/archive.py - backend/app/services/print_scheduler.py - backend/app/api/routes/print_queue.py - backend/app/api/routes/library.py - backend/app/api/routes/printers.py - backend/app/api/routes/archives.py Test files intentionally left unchanged (test XML is trusted). |
||
|
|
2833270e29 | Merge branch '0.1.8b' into feature/updated_plate_view_v2 | ||
|
|
cf286407fb | Merge branch '0.1.8b' into cadtoolbox/248 |