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.
Picking a color preset from the catalog only copied color_name and
rgba onto the spool — extra_colors (gradient stops) and effect_type
(sparkle / wood / etc.) were silently dropped at three layers above
the API: the SpoolFormModal state shape, the CatalogDisplayColor
mapping in ColorSection, and the selectColor handler itself. All
three widened to carry both fields through.
Picking a catalog swatch now writes both fields from the entry, so
solid presets cleanly replace previous gradients. Recent-colors and
the hardcoded-fallback palette stay as plain hex pickers — they
don't touch extras/effect since they aren't full presets.
Also fixed the en-US `colour` → `color` drift in 8 locale files
that the reporter flagged.
Two regressions reported in #1336:
1. spoolMatchesQuery did not include spool.id in the predicate, so
typing a numeric Spoolman ID into the Assign Spool dialog or the
Inventory page search returned no matches. Predicate now also
tests String(spool.id).includes(q).
2. The Unassign button in the spool edit modal was permanently
disabled for Spoolman-mode spools. The modal only ever queried
the legacy spool_assignments table (keyed by spool_id), but in
Spoolman mode the assignment lives in spoolman_slot_assignments
(keyed by spoolman_spool_id). Both the lookup query and the
unassign mutation now branch on the spoolmanMode prop and call
the Spoolman-flavored endpoints.
GitGuardian flagged "Bambuddy1!" and "LongerP@ssw0rd!" in password.test.ts
as potential leaked credentials. Replace with patterned placeholders
("Aa1!aaaa", "Aa1!Aa1!Aa1!") that still satisfy every complexity rule
(upper/lower/digit/special, >=8 chars) but won't trip secret scanners.
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
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.
- backend/app/api/routes/spoolbuddy.py: re-formatted via ruff (lambda
conditional wrapped in parens — the formatter check fires when the
expression spans multiple lines without grouping)
- frontend/src/__tests__/pages/SettingsPage.test.tsx: per-test timeout
raised to 15s for the external_camera_snapshot_url PATCH test; the
default 5s was tight enough on GitHub Actions runners that user.type()
of the 49-char URL + 800ms debounce occasionally blew past it
external-spool extruder routing (#1257)
X2D with 0 AMS units and two external spools (Ext-L feeding left
extruder, Ext-R feeding right) showed "Required filament type not
found in printer" even when the matching filament was physically
loaded. Cause: useFilamentMapping derived dual-nozzle status from
ams_extruder_map being non-empty -- that map is populated from AMS
info bits, so dual-nozzle printers without AMS got an empty map
and hasDualNozzle=false. External spools then fell through to
extruderId=undefined, and the nozzle-aware filter rejected every
candidate because undefined !== 0/1.
Prefer the hardware-reported printerStatus.nozzles array length as
the dual-nozzle signal -- populated regardless of AMS configuration
-- and keep the ams_extruder_map branch as fallback for older
firmware that might not surface nozzles. Affects all dual-nozzle
printers running without AMS: X2D, H2D, X2 Pro.
Regression test pins both layers the bug straddled --
buildLoadedFilaments extruderId assignment per external spool, and
computeAmsMapping picking the correct external for a per-nozzle
requirement -- so a future change that re-breaks either fails CI.
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.
Two defects in buildFilamentOptions, surfaced together:
1. The function was precedence-based — cloud presets short-circuited
the local-presets branch, silently hiding any imported Local Profile
while the user was logged into Bambu Cloud. The wiki documents the
dropdown as "merged and deduplicated" across cloud + local + built-in.
2. Cloud default presets and local presets were being collapsed by base
name (everything after "@" stripped), so all P1S/X1C/A1 variants of
"Bambu PLA Basic" rendered as a single row. The spool form is
printer-agnostic by design, so the right semantic is to show every
variant individually — the union across all printers — not collapse
them. AMS Slot is per-printer (it filters), the spool form is
union-of-all (it doesn't).
Rewrote the merge to push each cloud setting_id and each LocalPreset row
as its own FilamentOption with the full @printer suffix preserved in
displayName. Built-in dedup against cloud setting_id is kept (mirrors
ConfigureAmsSlotModal.tsx). Wired api.getBuiltinFilaments() into both
callers. slicer_filament persistence is unchanged so existing spools
keep slicing correctly.
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.
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.
The earlier `min-h-0` fix on the spool list (61314cf2) made the
shrinkable child shrinkable, but on @elit3ge's 838px viewport the
four stacked templates (~310px) plus footer still blew past
max-h-[90vh] once Brave's browser chrome ate into vh, and
overflow-hidden on the modal clipped Avery 5160 mid-row with the
Cancel button entirely below the clipped bottom edge — no scroll
path. The screenshot showed the spool list at ~5 visible rows with
its own scrollbar still active, confirming the templates section's
natural height was the dominant problem, not the spool list.
Templates now render as a responsive grid (grid-cols-1
sm:grid-cols-2 gap-2) so the four buttons pack into a 2x2 grid
above the sm breakpoint, trimming ~150px of vertical. Per-cell
padding tightens to p-2.5, labels/hints get text-sm + truncate,
and the full strings are reachable via title="<label> — <hint>"
on each button. Footer drops py-3 to py-2 for a few extra pixels.
The min-h-0 on the spool list is kept as a belt-and-braces shrink
for any viewport tighter still. Mobile (<sm) keeps the stacked
layout — no regression there.
Long preset names like "SUNLU PETG GLOW IN THE DARK GEN2 @Bambu Lab
H2C 0.4 nozzle" were visually clipped in the Configure AMS Slot
modal's preset picker. With several near-identical entries differing
only in nozzle size, users had to open browser dev tools to tell
them apart.
A `title={preset.name}` alone was too slow visually — browsers wait
500-1000ms before rendering native tooltips. The row now un-truncates
inline on hover via group-hover:whitespace-normal + break-all, so the
full name appears the moment the cursor enters the row. `truncate`
stays as the default to keep the list compact when scanning.
The native `title={preset.name}` is also kept as a belt-and-braces
fallback for assistive tech and touch devices where :hover doesn't
fire. Both desktop and mobile layouts updated.
Test: new ConfigureAmsSlotModal.test.tsx regression that pins the
truncate / group-hover:whitespace-normal / group-hover:break-all
classes on the span, the title attribute, and the `group` class on
the parent button — so a future refactor that drops any of those
fails CI.
The Print Labels modal used a flex column with overflow-hidden on the
outer container, the spool list as the flex-1 shrinkable child, and the
templates + footer as fixed siblings below it. The spool list had
min-h-[160px], which combined with the implicit min-height: auto on
flex items meant it could not yield space when the modal was tight —
templates and the Cancel button overflowed the modal's max-h-[90vh] and
got clipped. Reproducible on Windows 11 + Brave at 1080p with browser
chrome / DPI scaling reducing the effective viewport.
Switching to min-h-0 both removes the explicit floor and overrides
min-height: auto so flex shrinking actually works; the spool list now
yields height to keep all four templates and the Cancel button visible
on constrained viewports. Larger viewports behave identically since
flex-1 still grows to fill.
Adds a regression test that asserts all four template names + the
Cancel button render in the DOM and pins the structural fix by
checking the spool list scroller has min-h-0 with no min-h-[…] literal.
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.
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.
Until the preview slice / embedded-metadata read returns the per-plate
filament list, the modal renders a synthetic single-slot fallback so
the auto-pick has something to bind against. That made the Slice button
enabled the moment the modal opened, even before the slicer had told us
which AMS slots the plate actually consumes — clicking would dispatch
against opaque defaults.
Add filamentReqsQuery.isSuccess to the isReady chain so the button
stays disabled while the preview slice is in flight (or before the
backend's /filament-requirements call settles for sliced files) and
flips to enabled the moment the real slot list lands and auto-pick
fills it.
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
formatTimeOnly calls date.toLocaleTimeString([], …) which respects the
user's locale by design — the en_DK.UTF-8 locale uses "." as the time
separator, so the function correctly returns "02.30 pm" / "14.30" for
a Danish-English user. The two assertions hard-coded ":" as the
separator, which made the tests fail under any locale that doesn't
use ":". The implementation was right, the tests were wrong.
Switch the regex to use \D+ (any non-digit, one or more) for the
separator. This tests the actual contract — "hours and minutes,
separated somehow" — without coupling to a separator that varies by
locale (en_DK uses ".", some en_* locales use a narrow no-break space
at U+202F, most others use ":").
Verified passing under en_DK.UTF-8, en_US.UTF-8, and de_DE.UTF-8.
Audited every other toLocaleTimeString / toLocaleString call site in
the test suite — no other places hard-code separator characters.
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.
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.
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.
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].
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.
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.
@smandon flagged the 40×40 cover thumbnail as too small to recognise
the print and asked for a click-to-enlarge full preview. Enlarging
the thumbnail itself would shift the card grid layout, so keep the
small thumbnail and show a 384×384 hover popover with the full image
in ``object-contain`` rendering (so tall MakerWorld photos aren't
cropped to a square).
Why a portal: ProjectCard carries ``overflow-hidden`` (rounded
corners + color accent bar), so any in-tree popover gets clipped the
moment it extends past the card. Rendering via
``createPortal(..., document.body)`` escapes every ancestor clipping
context, and ``position: fixed`` with measurements from
``getBoundingClientRect()`` keeps the popover pinned next to the
thumbnail regardless of grid position. ``pointer-events-none`` on the
popover so it can't intercept hover and create a flicker loop;
``z-[100]`` so it stacks above sibling cards.
Edge handling: if the thumbnail is near the viewport's right edge the
popover flips to the LEFT side of the thumbnail; vertical position is
clamped so the popover never overflows the window top or bottom. The
thumbnail's own ``onClick`` is ``stopPropagation``'d so hovering the
popover area never accidentally triggers the parent card's
"open project" navigation.
Tests: 2 new ``ProjectsPage.test.tsx`` cases — mouseenter mounts the
popover at document.body level (not nested in the card subtree, which
would re-introduce the clipping bug, and the assertion catches that);
mouseleave unmounts it; the popover img points at the same
cover-image URL as the small thumbnail with ``object-contain``; cards
without a cover_image_filename never mount the portal-rendering
component.
@maugsburger surfaced four bugs against the original #1154 multi-colour
swatch work:
1. Editing an existing spool always opened with the Extra Colours field
blank, even when the COLOR preview banner above it was rendering
correctly from the saved data. ColorSection seeded its local
``extraColorsDraft`` via ``useState(formData.extra_colors)`` at
mount time, but SpoolFormModal opens *before* its own useEffect
populates ``formData`` from the spool record — so by the time the
saved value landed, the input had already locked onto ''. The user
then had to retype the value before saving anything else.
2. Dual Color and Gradient produced the same diagonal blend
(``linear-gradient(135deg, A, B)``), so the two variants were
visually indistinguishable. The whole point of the Dual Color variant
is that the spool has two distinct bars on the reel — a smooth blend
defeats it.
3. Sparkle was almost invisible on card-sized swatches. The original
4-dot pattern (each ~1px) read fine on the inline 20×20 swatch but
disappeared on the 60-pixel inventory card banners — exactly where
the user actually identifies a spool.
4. Checkerboard cell density scaled with the swatch — the same 4-cell
pattern was either tiny squares on a small swatch or four huge
squares on a card-sized banner. The user couldn't tell a translucent
filament from a multi-colour one because the indicator changed shape.
Fix:
- ``ColorSection.tsx``: ref-guarded ``useEffect`` resyncs the draft
whenever the parent's ``formData.extra_colors`` changes via an
external update. ``commitExtraColors`` updates the ref before
calling ``updateField`` so live user typing is round-tripped without
the resync useEffect clobbering it.
- ``filamentSwatchHelpers.ts: buildColorLayer``: branch on
``effect_type``. ``dual-color`` and ``tri-color`` produce
``linear-gradient(to right, c1 0 X%, c2 X% Y%, ...)`` with CSS
double-position stops (hard line, not blend) and equal-width
segments. ``gradient`` keeps the original 135° smooth blend. The
``multicolor`` conic-gradient path is untouched.
- ``filamentSwatchHelpers.ts: EFFECT_OVERLAYS.sparkle``: bumped from 4
dots to 13 flecks in mixed sizes (1 / 1.5 / 2 px) and varying
opacity (0.65 → 1.0) for a depth-of-field "metal flake" feel.
- ``filamentSwatchHelpers.ts: buildFilamentBackground``: now returns
``{ backgroundImage, backgroundSize }`` so per-layer sizes can be
applied — painted layers stay ``cover``, the checkerboard gets a
fixed 12px tile so cell density is constant regardless of element
size. Updated the three existing call sites (``InventoryPage`` group
banner + spool card, ``ColorSection`` preview) to spread the style
object directly. ``FilamentSwatch.tsx`` composes the same per-layer
sizing inline so its output stays in lockstep.
Tests: 8 new frontend cases pinning the four fixes — Dual/Tri Color
hard-split (3 tests + 1 regression guard that Dual ≠ Gradient for the
same stops), Sparkle prominence (≥ 10 distinct radial-gradient layers
in the rendered background), checkerboard density (last backgroundSize
layer is a fixed pixel value, not ``cover``), 4 hydration cases (fills
when formData arrives via parent update, resyncs when the spool
changes mid-form, doesn't clobber live user typing, clears when the
new spool has no extra_colors). Existing buildFilamentBackground tests
updated for the new return-object shape. Full frontend suite: 1610
passed; full backend suite: 3598 passed; no regressions.
@maugsburger surfaced two bugs against the original #1154 multi-colour
swatch work:
1. Editing an existing spool always opened with the Extra Colours field
blank, even when the COLOR preview banner above it was rendering
correctly from the saved data. ColorSection seeded its local
``extraColorsDraft`` via ``useState(formData.extra_colors)`` at
mount time, but SpoolFormModal opens *before* its own useEffect
populates ``formData`` from the spool record — so by the time the
saved value landed, the input had already locked onto ''. The user
then had to retype the value before saving anything else.
2. Dual Color and Gradient produced the same diagonal blend
(``linear-gradient(135deg, A, B)``), so the two variants were
visually indistinguishable. The whole point of the Dual Color variant
is that the spool has two distinct bars on the reel — a smooth blend
defeats it.
Fix:
- ``ColorSection.tsx``: ref-guarded ``useEffect`` resyncs the draft
whenever the parent's ``formData.extra_colors`` changes via an
external update (modal opening with a spool, or switching to a
different spool mid-form). ``commitExtraColors`` updates the ref
before calling ``updateField`` so the user's own typing is round-
tripped without the resync useEffect clobbering it.
- ``filamentSwatchHelpers.ts``: ``buildColorLayer`` now branches on
``effect_type``. ``dual-color`` and ``tri-color`` produce
``linear-gradient(to right, c1 0 X%, c2 X% Y%, ...)`` with CSS
double-position stops — the colour change is a hard vertical line
rather than a blend region — and equal-width segments across N stops.
``gradient`` keeps the original 135° smooth blend. The
``multicolor`` conic-gradient path is untouched.
Tests: 4 new ``FilamentSwatch.test.tsx`` cases pinning the hard-split
contract (Dual Color uses ``to right`` not ``135deg``; Tri Color
renders 3 equal hard-split bars; ``gradient`` keeps the smooth
diagonal; explicit regression guard that Dual Color and Gradient never
produce the same CSS string for the same stops). 4 new
``ColorSectionExtraColorsHydration.test.tsx`` cases pinning the input
hydration (fills when formData arrives via parent update, resyncs when
the spool changes mid-form, doesn't clobber live user typing, clears
when the new spool has no extra_colors). Full frontend suite: 1608
passed; full backend suite: 3598 passed; no regressions.
The minor "Sparkle could be more prominent / checkerboard denser"
feedback in the same comment is deferred to a separate cosmetic pass —
the reporter flagged it as finetuning.
@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.
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.
Backend routes were already gated on makerworld:view, the permission
was granted to admin + standard-user role defaults, and the frontend
Permission type union already included 'makerworld:view' — but the
sidebar's hand-maintained navPermissions map in Layout.tsx had no
entry for `makerworld`. So `isHidden('makerworld')` always returned
false, the entry rendered for every authenticated user regardless
of group permissions, and the only way the user found out they
couldn't use it was by clicking and getting 403'd by every API call.
Fix is two lines:
- Layout.tsx: add `makerworld: 'makerworld:view'` to navPermissions,
matching every other sidebar entry's gating shape.
- App.tsx: wrap the /makerworld route in PermissionRoute for defence
in depth, so a user who knows the URL can no longer reach the page
directly. Same pattern already used by settings, groups/new, and
groups/:id/edit two lines below.
Two new Layout tests pin the contract: with auth enabled and a user
lacking makerworld:view, the sidebar <a href="/makerworld"> link is
absent while other links still render; with the permission granted,
the link renders.
PrinterInfoModal's CopyButton only tried navigator.clipboard.writeText(),
which is gated by the secure-context requirement (HTTPS or localhost).
On the typical Bambuddy deployment shape — bare-IP HTTP on the LAN —
navigator.clipboard is undefined; the existing try/catch swallowed the
TypeError, the icon never flipped to the tick, and nothing landed on
the user's clipboard.
Fixed by adding the same off-screen-textarea + document.execCommand('copy')
fallback that CameraTokensPage's plaintext-token modal already uses for
plain-HTTP LAN deployments. Gate on `navigator.clipboard && window.isSecureContext`,
fall back to the legacy path otherwise, and surface the success-tick only
when the copy actually landed (return early without flipping `copied` if
execCommand returns false). The try/finally around the textarea guarantees
DOM cleanup even when the browser throws on a restricted context.
3 new component tests in PrinterInfoModal.test.tsx cover the secure-context
happy path (navigator.clipboard.writeText is called with the correct value),
the plain-HTTP fallback path (execCommand is invoked, no leaked textarea
left in the DOM), and the finally cleanup when execCommand throws
synthetically.
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.
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.
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.
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.
VP-uploaded archives land with status='archived' (uploaded but never
sent to a printer), but the Archives page sidebar only offered
All/Recent/This Week/This Month/Favorites/Failed/Duplicates -- no way
to filter "what's still queued in my library" vs "what's been printed."
Two new collections fill the gap: Not Printed filters to
status==='archived'; Printed filters to any final-status archive
(completed/failed/aborted/cancelled/stopped) so users see every
archive that had a print attempt regardless of outcome (the existing
Failed collection covers just the failure subset).
Frontend-only -- the status field was already populated correctly by
the VP archive paths, this was purely a UI gap. 2 new tests pin the
filter behaviour against a 4-status fixture.
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.
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).