Two related bugs in K-profile matching, same root cause.
#1688 — spool form's PA-profile suggester (PAProfileSection via
isMatchingCalibration in spool-form/utils.ts) matched K-profiles by
parsing the profile NAME for material/brand/variant. Spools already
store slicer_filament (the slicer preset id) and K-profiles already
carry filament_id, but both were ignored — so a user's custom
K-profile whose name doesn't agree with the slicer preset got silently
dropped from suggestions even when the underlying filament_id was
identical.
#1689 — ConfigureAmsSlotModal's matchingKProfiles ran the same
name-only logic on the slot's selected preset. A spool assigned under
"Generic PLA" with a custom K-profile actively bound on the printer
landed in the modal as "K profile not assigned, default 0.020 will
be used", while the printer-card hover-card correctly showed the
active profile. Two paths, only one was filtering by name.
Shared root: spool preset ids and K-profile filament_ids look
different but are equivalent after normalising. Spools store
slicer_filament as the cloud setting_id form ("GFSG98_09" — _09 is
the variant suffix, the S infix marks setting_id form); K-profiles
store filament_id as the bare form ("GFG98"). Plain === doesn't
work; both need normalising. This conversion already existed in the
other direction at buildFilamentOptions (filament_id → "GFS" +
filament_id.slice(2)), so the inverse toFilamentId helper is just
the matching reverse, not new ground.
Fix — one shared helper, two surfaces:
- spool-form/utils.ts: new exports toFilamentId(id) (drops "_NN"
variant suffix and strips the "S" in "GFS", so GFSG98_09 → GFG98)
and isGenericFilamentId(id) (flags Bambu's generic GFx99 ids
which are shared across many filaments and must NOT id-match —
the name fallback handles those correctly).
- isMatchingCalibration: gains slicer_filament?: string in formData,
tries id-match (with generic exclusion) before the existing name
parse. PAProfileSection already passes the full formData so no
caller edit needed. Strictly additive precedence.
- ConfigureAmsSlotModal.selectedPresetInfo: resolves a filamentId
field (toFilamentId(cp.setting_id) for cloud presets,
toFilamentId(builtinFilamentId) for builtin; empty for local /
orca paths which fall through to name match).
- ConfigureAmsSlotModal.matchingKProfiles: id-match check at the top
of the per-profile predicate (preferred when both sides agree
after normalisation), then the existing name-parse logic, then
ALWAYS unshifts the slot's currently-active K-profile by
slot_id === slotInfo.caliIdx — gated on activeIdx > 0 (so caliIdx
0/null doesn't leak unrelated profiles in), extruder-matched when
slotInfo.extruderId is known. This is Spionkiller01's #1689 patch
verbatim with the activeIdx > 0 guard added.
SpoolBuddy: both kiosk K-profile surfaces reuse the shared
components. SpoolBuddyWriteTagPage renders PAProfileSection;
SpoolBuddyAmsPage renders ConfigureAmsSlotModal. Verified — fixes
propagate automatically, no kiosk-specific edits.
What this does NOT change: spools without slicer_filament, K-profiles
without filament_id, and generic GFx99 ids all fall through to the
existing name-based matching path. Strictly additive precedence; no
input shape that matched under the old logic fails to match under the
new. The #1053 cloud-preset PFUS* path is preserved because the
toFilamentId regex /^GFS/ doesn't match a "PFU" prefix.
Reporter wanted to select a transparent filament colour in the spool
editor; CMW-ISS confirmed on v0.2.5b1 that AMS-detected transparent
spools were silently labelled "Black" in the filament-mapping dropdown
because the colour name resolver dropped the alpha byte and the underlying
RGB 000000 HSL-bucketed to "Black". Spoolman already supported 8-digit
hex; the built-in inventory didn't.
Eight collapsing sites fixed together so transparent reaches the user
intact:
- frontend/src/utils/colors.ts: hexToColorName / getColorName /
resolveSpoolColorName / isLightColor short-circuit to "Clear" for
alpha=00 before HSL bucketing or catalog lookup
- frontend/src/utils/amsHelpers.ts::normalizeColor preserves the alpha
byte when alpha < FF (normalizeColorForCompare unchanged so type/colour
matching is unaffected)
- frontend/src/components/spool-form/constants.ts: new
{ name: 'Clear', hex: '00000000' } preset in QUICK_COLORS
- frontend/src/components/spool-form/ColorSection.tsx: hex draft accepts
0-8 chars, commits at 6 (+FF) or 8 verbatim; blur pads 7-char to 8;
selectColor passes 6-char as +FF / 8-char verbatim; isSelected matches
on full rgba; swatch buttons paint a checkerboard for alpha=00
- backend/app/api/routes/printers.py::get_available_filaments preserves
the full rgba on both AMS and vt_tray branches (6-char dedup key
unchanged)
- backend/app/services/spoolman.py::parse_ams_tray drops the silent
00000000 -> F5E6D3FF cream rewrite — the swatch renderer paints a
checkerboard underlay for alpha < FF already (added in #1154), so the
rewrite was hidden technical debt that made every AMS-detected
transparent spool land in inventory as cream
- backend/app/services/spool_tag_matcher.py::create_spool_from_tray
short-circuits the colour-catalog lookup for alpha=00 and stores
color_name="Clear" directly — otherwise an RFID-tagged transparent
Bambu spool would resolve against the #000000 catalog row (or "Black"
via the HSL fallback) before the frontend's resolver ever saw it
- Two shared helpers in utils/colors.ts — getSwatchStyle(rgba) (style
object: checkerboard for alpha=00) and spoolColorString(rgba)
(8-char hex string for SVG fill) — applied to every simple-swatch
site that would otherwise have rendered Clear spools as solid black:
LabelTemplatePickerModal, SpoolBuddyInventoryPage (SpoolCircle + dot),
SpoolBuddyAmsPage (both branches), SpoolBuddyWriteTagPage (4 sites),
ForecastPanel, AssignToAmsModal, AssignSpoolModal (both branches),
InventorySpoolInfoCard, TagDetectedModal, SpoolInfoCard, LinkSpoolModal,
and the FilamentSwatch tooltip title fallback
Intentionally NOT changed: native <input type="color"> keeps 6-char hex
(can't pick alpha; onChange still emits +FF, correct); Spoolman's
_find_or_create_filament strips alpha (Spoolman catalog is 6-char only);
print_scheduler colour matching strips alpha (auto-mapping treats Clear
as Black for slot compatibility); label_renderer prints "#RRGGBB" on the
physical label (printers can't print transparency, swatch fill via
_color_from_hex still honours alpha).
Reporter typed into the Add Spool modal's hex colour input and only
the first character stuck - everything after that defaulted to "0"
with no way to override except by pasting the full hex.
Pre-fix, the #1055 fix aggressively normalized the input to a valid
8-char rgba on every keystroke. After typing the first char the
controlled input value snapped to e.g. "A00000", the browser placed
the cursor at the end, and the user's next keystroke landed at
position 7. The #1055 fix's 7-char branch then truncated that byte
away, leaving the form state unchanged - so the user appeared to
type nothing.
Fix splits the typing-state from the backend-state:
- The hex input gets its own `hexDraft` useState holding 0-6 chars
freely. Typing one char at a time works naturally because the
controlled value matches what the user typed.
- `updateField('rgba', ...)` fires only when the draft reaches a
complete 6-char RGB (commits as `<6chars>FF`). Below that, the
form state stays untouched - no mid-keystroke snap.
- On blur, a partial 1-5 char draft is right-padded with `0` and
committed. Keeps the #1055 invariant: anything reaching the
backend is exactly 8 hex chars matching /^[0-9A-F]{8}$/.
- A `useEffect` resyncs the draft when an external action (color
picker, swatch click, edit-mode load) changes the canonical hex.
- Paste of 7-/8-char strings truncates to the leading RGB. Bambu
filaments are opaque; the UI never exposed an alpha affordance,
so dropping the (undocumented) paste-with-alpha case is fine.
Adding a third-party PETG-CF spool via the Material=PETG + Subtype=CF
flow (same shape as the existing PETG HF) hit a missing option: KNOWN_VARIANTS
in spool-form/constants.ts didn't list CF or GF. Users had to type it
freehand into the "create new" tail of the dropdown.
Added both: CF (matches PETG-CF / PLA-CF / ASA-CF / PA-CF) and GF (the
natural pair for ABS-GF / PA6-GF). parsePresetName is unaffected — its
materials list is iterated longest-first, so cloud presets like
"Bambu PETG-CF Black" still resolve to material=PETG-CF with empty
afterMaterial.
The assign flow was sending slicer-invalid values for tray_info_idx and an
empty setting_id, which the slicer rejected — slot detail modal showed
empty fields. With a stored k-profile the realignment path masked the
issue; without one, garbage hit MQTT.
Backend (apply_spool_to_slot_via_mqtt):
- Discard tray_info_idx values that aren't real preset IDs: literal
material names ("PLA", "PETG-CF") AND PFUS-prefix cloud setting_ids
(valid as setting_id but rejected as tray_info_idx). Same check applied
to current_tray_info_idx so stale slot values don't get reused as
garbage.
- Local-preset path now reads the printer-recognized filament_id from
the preset's setting JSON (e.g. P4d64437) instead of falling through
to a generic material ID.
- Derive setting_id from filament_id_to_setting_id when empty so
ams_filament_setting always carries a matched pair.
- No stored k-profile: always send cali_idx=-1 (Default K), regardless
of the live cali_idx on the slot. The live value belongs to whatever
filament was there before, so reusing it would apply the wrong K to
the new spool.
Frontend (spool-form/utils.ts):
- Local preset options use String(preset.id) as the unique code instead
of preset.filament_type — every PLA local preset was collapsing onto
the same "PLA" code, so picking any of them saved slicer_filament=
"PLA" and lost the specific preset identity.
Spoolman counterpart in spoolman_inventory.py mirrors the cali_idx=-1
reset.
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 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.
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.
@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.
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 optional fields on Spool: free-text `category` (max 50) and
`low_stock_threshold_pct` (1-99). Powers the "differentiate critical
spools from prototype spools and alert at different thresholds" use
case from #729 without taking on the full multi-tag taxonomy + auto-
apply rules + per-tag alert system the ticket originally proposed.
Form gains:
- Category input with datalist autocomplete sourced from categories
already in use, so casing/spelling stays consistent.
- Per-spool low-stock threshold input. Empty = global default; the
global value renders as the placeholder.
Inventory page:
- New category filter chip (hidden until at least one spool carries
a category — keeps the chip row uncluttered).
- Stat-card "Low Stock" count and the "Low Stock" filter both honour
the per-spool override.
Plus: rename "Delete Tag" button to "Clear RFID Tag" (the original
ticket reporter mistook it for a taxonomy-tag delete; the button
actually clears the RFID UID/UUID off the spool record). Toast key
renamed from `tagDeleted` to `rfidCleared`.
i18n: full translations across all 8 locales.
Tests: 9 new backend schema tests (defaults, partial-update, range
rejection, max-length); 2 new frontend tests (per-spool threshold
pulls extra spools into low-stock count, filter chip hidden when no
categories exist).
feat(inventory): replace Spoolman iframe with internal inventory UI
When Spoolman is enabled, the Inventory page now uses the same internal
UI (spool list, create/edit modal, archive, delete, weight sync) backed
by a new proxy layer instead of opening an iframe.
A single legacy spool with a 7-char rgba ('FFFFFFF', missing one F)
caused GET /api/v1/inventory/spools to 500 with a pydantic
ResponseValidationError, leaving the reporter with a blank Filaments
page and "Add Spool" silently failing. Root cause spans three layers:
1. Write path: SpoolUpdate.rgba had no pattern constraint (only
SpoolCreate did), so PATCH could plant malformed values in the DB.
2. Frontend: ColorSection hex input's `val.length <= 6 ? 'FF' : ''`
emitted 7-char rgba for 5-char input (XXXXX + FF = 7) and for
7-char typed input (no alpha appended).
3. Read path: SpoolResponse inherited the write-side pattern, so a
single bad row 500'd the entire list endpoint instead of being
tolerated through serialize.
SpoolUpdate.rgba now carries the same ^[0-9A-Fa-f]{8}$ pattern as
SpoolCreate. The hex input emits a fully-formed 8-char RRGGBBAA on
every keystroke — 8-char paste passes through, 7-char drops the
stray, shorter input pads RGB with '0' and appends FF alpha.
SpoolResponse.rgba is now Optional[str] with no pattern — write-side
validation is the right place for format rules; responses must
tolerate historical rows.
Tests: 16 schema tests (SpoolCreate/Update reject, SpoolResponse
tolerate), 7 frontend tests covering every input length 0–8 plus
non-hex strip. A user who already has a bad row in their DB now sees
it render with a default color instead of having to hand-edit SQLite.
Long filament profile names were cut off because inline filament ID
codes consumed horizontal space in the dropdown. Remove the codes from
dropdown items (selected code still shown below the input) and widen
the modal from max-w-lg to max-w-xl.
Group toggle in inventory toolbar collapses identical unused/unassigned
spools into expandable rows/cards with count badges. Grouping key uses
material, subtype, brand, color, and label weight. Brand and subtype
fields now appear in quick-add mode as optional (no asterisk, no
validation). Quantity field restricted to quick-add mode only.
Add Quick Add mode to the spool form for simplified entry (material +
color + weight only), and a quantity field (1-100) for creating multiple
identical spools at once. Stock spools (no slicer profile) are computed
rather than stored — any spool without slicer_filament shows an amber
"Stock" badge. New filter chips (All/Stock/Configured) on the inventory
page. Backend bulk endpoint creates N spools in one transaction.
Backend:
- SpoolBulkCreate schema with quantity validation (1-100)
- POST /inventory/spools/bulk endpoint
Frontend:
- Quick Add toggle in SpoolFormModal (create mode only)
- Quantity field in FilamentSection (both modes)
- bulkCreateSpools API method and bulkCreateMutation
- Stock column (hidden by default) and stock filter chips
- Inline error rendering moved into FilamentSection
- CellCtx extended with t() for translatable cell content
Tests:
- 13 backend tests (schema validation + endpoint logic)
- 10 frontend tests (validateForm quickAdd + UI behavior)
i18n: 6 locales (en, de, fr, it, ja, pt-BR)
Docs: CHANGELOG, README, website features, wiki inventory
* feat(spool-form): enhance brand and material selection in color section
* feat(spool-form): change input field behavoiur when deleting all chars, fix scroll menu theming
* feat(spool-form): add available materials to SpoolFormModal and FilamentSection
* refactor(ColorSection): remove unnecessary hasCatalogMatch variable
* feat(SpoolFormModal): add bilateral brand and material filtering logic
* feat(ColorCatalogSettings): improve input field sizes and layout for color and material selection
* Refactor colorCatalog settings and dependencies, allow custom materials, fix ml-12 marging, show catalog colors on brand/material selection
* Refactor ColorCatalogSettings and ColorSection components; remove onBlur logic in FilamentSection
* Prioritize exact matches in search results
* Added inventory.useCustomMaterial to locales
* Remove unused filteredCatalogColors logic and update button rendering; update types file to import Printer and SpoolKProfile
* Fix indentation for useCustomMaterial
* Refactor showCatalogSection logic to only depend on matched catalog colors
* Remove extra newline before brand dropdown
* Clean up ColorSection.tsx by removing blank lines
Removed unnecessary blank lines in ColorSection.tsx.
* Ensure material property is a string in ColorSection component
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
The spool add/edit modal allowed saving without brand or subtype,
causing incomplete tray_sub_brands when assigned to AMS slots.
BambuStudio then failed to recognize the filament profile.
Brand and Subtype are now mandatory with validation errors on submit.