A printer files each calibration under a nozzle id of the form HH00-0.4
(high flow) or HS00-0.4 (standard) and can hold both for one diameter -- a
maintainer's H2D carries 102 high-flow entries against 6 standard --
because the same filament reads a different K through each. Nothing read
that, so a standard-flow profile could be selected for a high-flow nozzle
and vice versa.
The flow is now stored with the profile, shown against each option in the
picker, and checked before a stored profile is applied. Two spellings have
to agree for that: a calibration entry says HH00-0.4 while the fitted
nozzle reports HH01, so the comparison is two characters rather than four
-- the trailing digits are a hardware variant the calibration table
normalises to 00.
Unknown flow on either side matches anything, which is what it has to do.
Every profile stored before this has none. And an X1C declares none on any
profile at all -- probed live, all eight come back with an empty nozzle id,
against a four-digit cali_idx and a populated setting_id -- even though the
machine really does take either nozzle. supports_nozzle_flow_type is
therefore the wrong thing to gate on: it returns True for an X1C, and
treating that silence as Standard would have dropped every X1C profile the
moment a high-flow nozzle was fitted. What the printer's own table declares
per profile is the test.
NozzleInfo.nozzle_type carries two vocabularies by printer generation --
the nozzle material on legacy printers, the flow code on H2 -- and the
comment claiming only the former is corrected. Anything that is not HH or
HS reads as unknown, which is what makes the material spelling harmless.
Storing both flows for one hotend and diameter is deliberately not done:
spoolman_k_profile is UNIQUE on (spool, printer, extruder, diameter) with
no flow column, and allowing a second row in internal mode alone would
break inventory-mode parity. The picker marks a profile whose flow does not
match what is fitted instead of letting it look configured while doing
nothing.
A slicer preset is bound to a printer model: "Bambu PLA Basic @BBL X1C" is
not the same preset as "@BBL H2C", and Bambu names a nozzle size in it as
well. A spool carried exactly one, which was right until the same spool was
used on a second machine -- the AMS slot on the other one was then
configured with a preset that machine has no profile for. K profiles had
the matching gap from the other side: the tables have always been keyed per
hotend, but the picker could not express it.
spool_filament_preset and its Spoolman twin store the exceptions, keyed
(spool, printer_model, nozzle_diameter). Model rather than printer because
the preset is a property of the model -- "@BBL X1C" is the same preset on
every X1C, and asking per machine would mean picking the identical value
twice. K profiles stay on printer_id, because a K value is measured on one
physical hotend and two machines of the same model legitimately differ.
Resolution is exact (model, diameter) -> (model, "") -> the spool's own
preset, so a spool nobody has configured behaves exactly as it did before.
The form writes one row per nozzle size and never the "" row; that level is
kept for API clients wanting one value to cover a model.
Both halves cover every standard nozzle size rather than the size currently
fitted, because a spool is configured once and nozzles get swapped. The PA
Profile tab becomes a Printers tab: a model list beside a detail pane
holding a preset row per size and a K-profile grid of size by hotend. Each
model is offered only the presets that name it, through the same matcher
the Configure AMS Slot modal filters with, which moves out of that
component into utils/slicerPrinterMatch. Presets whose name identifies no
model -- most user-authored and OrcaSlicer ones -- stay offered everywhere,
as does whatever is already selected, so a saved override cannot vanish
from the control that shows it. Every preset carries an origin badge in the
wording and colours that modal already uses.
Every path that configures a slot now respects both: manual assign in
either inventory mode, RFID auto-assign, the Spoolman tag link, the re-fire
when a slot goes empty to loaded, the re-apply after a calibration-table
refresh, and the re-selection when a Filament Track Switch moves an AMS to
the other nozzle. Which nozzle a slot feeds, and how wide it is, was worked
out independently in seven of those places, each reading nozzles[0] for
every slot on the machine -- correct on a single-nozzle printer and on a
dual-nozzle printer with matching nozzles, wrong the moment two sizes are
fitted. That resolution is now services/slot_nozzle.
Which array entry belongs to which hotend is no longer inferred. Measured
on an H2D fitted with a 0.4 high flow on the left and a 0.6 on the right,
nozzles[0] reads the right hotend, so the array is indexed by extruder id
and the H2/X2 parser's convention is the one that holds. The legacy
parser's opposite convention never governs a real dual-nozzle machine:
every model in DUAL_NOZZLE_MODELS reports device.nozzle.info, and
left_nozzle_diameter appears in no log or wire capture. Two comments that
said otherwise were wrong and are fixed; amsHelpers' code was right all
along and only its comment lied.
Four defects surfaced while wiring it, all pre-existing except the last.
The picker identified a chosen calibration by cali_idx alone, and the
printer numbers its calibration table per nozzle -- on a dual-nozzle
machine the same index exists on both hotends meaning different things, so
saving could persist the other hotend's K value and diameter; SpoolBuddy's
write-tag page carried a verbatim copy and gets the same fix. RFID
auto-assign chose a K profile with no extruder test at all, so a spool
calibrated on both hotends had a coin toss decide which pressure-advance
value the slot got, on the path that runs unattended every time a Bambu
spool is loaded. The Spoolman tag-link path resolved no preset whatsoever,
configuring every linked slot with a generic material id and discarding a
preset set in inventory -- the same defect #1713 fixed on the assign path,
one function over. And an FTS inlet move re-selected K for nozzle 0 rather
than for the nozzle the AMS had just been moved to.
The last one is new here: a per-model override can be a cloud USER preset,
whose PFUS-prefixed id the slicer rejects, and passing it straight into
extrusion_cali_sel would silently lose the K-profile link. Reached the
printer only where such an override exists, which is why nothing in the
suite caught it. printer_safe_filament_id falls through to the spool's own
preset and then the tray's RFID value instead.
Reading a printer's calibration table asks for one nozzle size at a time.
H2-series firmware answers only the first one or two of a concurrent burst
of extrusion_cali_get and silently drops the rest, each dropped request
costing a five-second timeout before its retry: measured at 11 and 23
seconds on an H2C and an H2D for four parallel requests, against roughly
one second in series. An X1C answers all four at once, which is why this
only ever surfaced on dual-diameter printers. Printers themselves are read
in parallel -- separate machines are separate connections.
The Configure AMS Slot dialog opens on the spool's own configured values,
falling back to the slot's last manual configuration and then the tray's
RFID data. The spool form is wider for the two-pane layout, colour, weight,
cost and location move to their own tab in two columns, and a printer card
in expanded view lists every fitted nozzle size rather than the first entry
alone.
The reporter's A1 mini has nine Flow Dynamics calibrations, all of them saved
under Generic PLA and named after the spool's colour — "Dark Brown", "Glow",
"Marble". Bambu Studio lists all nine for that slot. Configure AMS Slot offered
one: the profile already bound to the slot. After a slot reset it offered none,
leaving the slicer as the only way to assign a K value.
Two independent faults, both tripped by picking a built-in generic preset.
The filament-id match discarded Bambu's generic GFx99 ids as too broad. But the
comparison already requires both sides to carry the same id, so that exclusion
could only ever fire when the selected preset was itself the generic one —
precisely the case where the match is right. The printer keeps one calibration
table per filament id, so a slot on Generic PLA should offer everything
calibrated under Generic PLA. Equal ids now match, generic or not.
The name fallback was dead for the same presets: parsePresetName reads the
leading "Generic" in "Generic PLA" as a manufacturer, which put the matcher into
brand-gated mode and demanded the word GENERIC appear in the profile name. No
real profile has it. "Generic" is no longer treated as a brand, so profiles still
match on material when a printer reports no filament_id with its calibrations.
The one profile that did appear came from the #1689 safety net that always
surfaces the slot's active cali_idx — which is also why a reset slot, having no
active profile, showed an empty list.
Neither fix can be complete on its own, because profile names are free text and
nothing ties "Marble" to a material. The picker now also lists every remaining
profile on the printer under "Other K profiles on this printer", so a profile
that exists can always be selected. Applying one from that group needs no new
backend work: configure_ams_slot already realigns the slot's filament context to
the chosen profile's, which is what makes the cali_idx stick.
Options are keyed by name+k_value rather than the bare name, so two profiles
sharing a name are no longer indistinguishable in the select. Both render blocks
carry the change — the modal duplicates the picker for its full-screen variant.
isMatchingCalibration gets the same generic-id rule for the spool form's PA
suggester, with two guards. A new generic-id-to-material table means a PETG spool
can never claim GFL99 profiles just because both sides stored a generic id
(Nylon and PA compare as one material). And a spool that names its own brand
keeps the stricter name path, so its suggestions stay brand-specific rather than
becoming the printer's whole generic table.
A spool created by Quick Add, a CSV import or an RFID scan has no slicer
preset, brand or subtype. Reopening it in Edit Spool demanded all three
before anything could be saved, so changing its storage location, cost
or notes was impossible - and Copy Spool had the same gate with no Quick
Add toggle to waive it. The preset you were then forced to pick auto-
filled material, brand and subtype from the preset name, silently
rewriting a hand-entered manufacturer (Elegoo -> Generic) so the spool
no longer appeared where it had been filed.
Editing and copying now require only what the backend requires: the
material. Preset, brand and subtype stay fully visible and editable -
nothing is hidden the way Quick Add hides it - and the required-field
markers no longer advertise a rule that isn't enforced. Selecting a
preset fills only fields that are still empty or that a previously
selected preset had filled, so values the user (or the saved spool)
provided survive; switching between presets still replaces what the
earlier one contributed.
The brand and material dropdowns also no longer filter themselves down
to the brand/material pairs known to the color catalog and slicer
presets. Elegoo is catalogued only for PLA, which made a real product
like Elegoo ASA look impossible to enter. Both lists now always offer
everything known, with paired entries ranked first under Suggested and
the rest under All, and a spool's own custom brand or material is always
present in its own dropdown. The SpoolBuddy write-tag form shares these
fields and gets the same treatment.
Lastly the Quick Add layout no longer leaks out of create mode: quick-
adding a spool and then opening Edit left the edit form in the reduced
layout with no toggle to leave it, because the toggle is create-only.
Frontend only. Translated in all locales; wiki updated. Covered by
validation and form-interaction tests.
The Edit Spool "PA-Profil" tab and the SpoolBuddy write-tag page fetched a
printer's calibrations with getKProfiles(printer.id), which defaults the nozzle
filter to 0.4. The printer/MQTT layer filters strictly by that diameter, so on
a multi-nozzle printer a same-filament 0.6mm K-profile was never retrieved and
the picker showed only the 0.4mm entry ("1 match"). (The AMS-Slot config dialog
was already fixed in #1899; these two pickers were not.)
Add installedNozzleDiameters(status) and a shared fetchPrinterCalibrations()
that queries every reported nozzle diameter and merges the results, falling
back to 0.4 when the printer hasn't reported nozzle hardware. Each profile row
now shows a nozzle-diameter badge so identically-named profiles are distinct.
The app was built dark-first, so hundreds of hardcoded Tailwind semantic
text/icon utilities at light shades (text-amber-400, text-blue-300, ...) had
no dark: variant. With darkMode:'class' they applied in light theme too,
producing washed-out text on pale tints and white cards — including the three
reported spots (AMS Drying banner, Archives no-3MF warning, debug-logging
banner). Give each a theme-aware pair: a darker readable shade in light theme
with the original pinned to dark:, so dark theme is unchanged. ~100 files.
The bambu-* CSS-variable palette (self-correcting) and the dark-only SpoolBuddy
kiosk are left untouched. Plain text-white is already theme-aware via the
existing index.css .text-white override, so it needed no changes.
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.