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.
The original #793 fix added the spool note as an HTML title= tooltip
on each picker button in AssignSpoolModal.tsx. title= only surfaces
on hover, which doesn't exist on touch devices — phone users tapping
a card just selected it, the note never appeared. Users who store
their tracking ID in the note field were blind on mobile (raised by
@EmcetPL on the closed issue).
Render the note as a small muted truncated line directly under the
weight on both the internal-inventory branch (line 436-ish) and the
Spoolman branch (line 510-ish): text-[10px] text-bambu-gray/70 mt-1
truncate, kept inside the truthy `&&` guard so empty notes don't add
a blank row. The existing title={spool.note} is preserved on the new
<p> so desktop hover and mobile long-press still surface the full
untruncated text for notes that overflow the truncate.
Mirrored across both inventory branches per the parity rule
(internal and Spoolman pickers stay shape-equal). No backend change,
no new state, no popover, no new touch target.
Backend correctly defers MQTT configuration when the AMS slot is empty
at assign time (state ∈ {9, 10}) — firmware drops the push silently;
on_ams_change replays the config once a spool is detected. The
response carries pending_config=true to communicate that. The
printer-card AssignSpoolModal was ignoring the flag and always
showing "Spool assigned and AMS slot configured" — the SpoolBuddy
modal has handled this since it shipped. Now mirrors the same
branch and shows "Assigned. Slot will configure when you insert
the spool." when pending_config is true.
Reporter assigned a spool to a slot whose stored slicer profile differed
from the new spool's, got the Cancel / Assign Anyway popup, and was under
the impression that confirming the popup just linked the spool in
Bambuddy's DB without pushing the new profile to the AMS — i.e. that he
then had to manually open Configure AMS Slot to fix it. The auto-push has
actually been in place since the assign route existed:
backend/app/api/routes/inventory.py::assign_spool calls
apply_spool_to_slot_via_mqtt after upserting the SpoolAssignment row,
which publishes ams_filament_setting + extrusion_cali_sel over MQTT, and
backend/app/api/routes/spoolman_inventory.py::assign_spoolman_slot does
the same for the Spoolman backend. The only short-circuit is when
firmware reports the slot explicitly empty (tray_state in {9, 10}), in
which case main.py::on_ams_change deferred-replays the configure once a
spool appears. So the popup was friction without revealing what it did.
Two changes:
- frontend/src/components/AssignSpoolModal.tsx and
spoolbuddy/AssignToAmsModal.tsx: profile-only mismatch no longer
fires the popup. The condition becomes
`if (materialMatchResult !== 'exact')` instead of
`materialMatchResult !== 'exact' || !profileMatches`. The 'profile'
member is dropped from the mismatchType union and its standalone
branch in each popup render body is removed as dead code. Material
mismatch still warns — Bambu firmware can refuse the print when the
type is wrong.
- Every firing warning (material, partial, material+profile,
partial+profile) now appends one line via a new
inventory.assignReconfigureNote i18n key:
"The AMS slot will be reconfigured to use the spool's profile."
Real translations across all 9 locales per
feedback_translate_dont_fallback; parity script clean at 4999
leaves per locale.
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 (@snozzlebert on A1 mini external slot) saw the
Filament page Location column update correctly after Assign
Spool, but the Printer card kept showing "Empty slot" until
they manually pressed Force-refresh. MQTT command was going
through fine; gap was client-side.
AssignSpoolModal's two onSuccess callbacks invalidated the
inventory / slot-assignment queries but never invalidated
['printerStatus', printerId] and never issued a pushall. For
Bambu RFID-tagged spools the printer echoes the new tray_type
on its own; for non-RFID spools and A1 mini external slots
the firmware doesn't volunteer that state change.
Added nudgePrinterRepublish() helper called from both onSuccess
paths: api.refreshPrinterStatus(printerId) to issue the pushall
(same call the Force-refresh button uses) plus invalidate
printerStatus so the refetch lands. Refresh failures are
swallowed — the assignment itself succeeded; a stale-cache
nudge that didn't go through shouldn't surface as "assign
failed". Same pattern as ConfigureAmsSlotModal since #1235,
with the extra pushall because assign-spool affects firmware-
side state.
Two latent issues surfaced after the original AssignSpoolModal z-50 →
z-[100] bump landed:
1. Material-mismatch ConfirmModal hidden behind AssignSpoolModal.
ConfirmModal's overlay was hardcoded to z-50 in its wrapper, so once
the parent moved to z-[100] the nested confirmation dialog sat
behind it. Added an optional overlayZIndex prop to ConfirmModal
(defaults to z-50 — none of the 82 other call sites change), and
the mismatch site in AssignSpoolModal passes z-[110] so the warning
stacks above its parent.
2. FilamentHoverCard / EmptySlotHoverCard covered by sibling printer
cards on the dashboard. The popovers used position:absolute with
z-[60] inside the trigger, but every printer card creates its own
stacking context (drop-shadow filter on the slot tiles is enough),
and z-index doesn't cross stacking-context boundaries — the next
sibling card always wins by DOM order. Visible as the "Jade White
· Bambu PETG HF" tooltip getting half-eaten by the neighbour card's
AMS column.
Fixed by portaling both hover cards to document.body with
position:fixed and screen-space coordinates from
triggerRef.getBoundingClientRect(). Coords recompute on visibility
change, scroll (capture), and resize so the popover follows the
trigger when the viewport moves; a requestAnimationFrame re-measure
after the first paint avoids a one-frame flicker before the card
has its rendered dimensions. Hover handlers are wired on both the
trigger AND the portaled card so moving the cursor from slot to
popover doesn't auto-dismiss after 100 ms. Top/bottom placement
and arrow-pointer logic preserved.
Both used z-50, so DOM stacking-order let the sidebar bleed over the
modal on narrow viewports. Bumped to z-[100] to match the convention
used by GitHubBackupSettings, FilamentHoverCard, and the Layout
confirmation modal.
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.
The picker that opens from <FilamentHoverCard> / SpoolBuddy's slot-action
sheet had two stacked filters that together blocked a real workflow:
1. AssignSpoolModal only listed spools whose tag_uid AND tray_uuid were
both null, hiding any Bambu Lab spool that had been auto-created from
RFID or scanned via SpoolBuddy NFC.
2. FilamentHoverCard rendered its inventory section only when the slot's
vendor was not 'Bambu Lab', so even with #1 fixed the assign button
wasn't visible on a BL slot.
Both filters blocked the same use case: a user has a Bambu Lab spool in
inventory but doesn't want to scan via SpoolBuddy NFC every time and
just wants to pick it from the list.
Both gates removed. Modal lists every spool that isn't already taken by
another (printer/ams_id/tray_id) tuple. Hover-card inventory section
renders for every vendor including Bambu Lab. The AMS-vs-external
special-case in the modal collapsed too — external slots used to be the
only path that allowed picking a tagged spool, that distinction is gone.
Empty slots lost their assign affordance entirely. A physically empty
slot has no spool to attach an inventory record to, and offering the
action there only led to users assigning the wrong spool to a slot the
printer hadn't actually loaded yet. Bambuddy: EmptySlotHoverCard's
inventory prop removed; PrintersPage drops the matching inventory
props on three sites. SpoolBuddy: slot-action picker gates the
assign/unassign block on slotActionPicker.tray !== null.
Modal also gets defensive hardening from the rollout investigation:
its own dedicated cache key (['inventory-spools', 'assign-modal']) so
it can't be poisoned by other components calling getSpools() with
different includeArchived args; getSpools(true) + client-side
!archived_at filter so the picker sees the full inventory regardless
of cache priming order; "Show all spools" toggle now bypasses BOTH
filters (was only bypassing material/profile, label was a lie); a
small "X fetched · Y archived · Z assigned" counter in the empty state
so future "missing spool" reports are debuggable from screenshots.
i18n.inventory.noManualSpools renamed to inventory.noAvailableSpools
with new copy ("No spools available. Add a spool to your inventory or
unassign one from another slot first.") since the empty-state premise
changed. Localised across all 8 languages.
15 frontend tests: assign/unassign render for vendor: 'Bambu Lab',
non-BL vendors unchanged, EmptySlotHoverCard renders no assign
affordance, configure button still works on empty slots, picker lists
BL spools alongside manual ones, picker drops spools assigned
elsewhere unless toggle is on, picker drops archived spools always,
toggle escape-hatch shows everything, empty-state copy update.
SpoolBuddy unassign now invalidates both ['spool-assignments'] and
['spool-assignments', printerId] so the modal's cache stays fresh
(dual cache-key consolidation deferred to its own PR).
Three related fixes reported together:
(1) After resetting an AMS slot, the printer card showed "Empty Slot"
with no Configure or Assign Spool actions while SpoolBuddy's AMS page
still let the user re-configure the same slot. Commit c9efa4b8 (#784)
added a `tray?.state === 10` gate to the EmptySlotHoverCard actions,
intended to hide them on physically-empty slots (state=9). In practice
firmware often reports state=9 (or omits state entirely) after a
user-initiated reset even when a spool is still present, so the gate
hit the wrong case. The gate was redundant anyway — EmptySlotHoverCard
only renders when tray_type is empty — so it's removed at both the
standard-AMS and AMS-HT render paths.
(2) After configuring a slot with a Generic profile, the Assign Spool
modal hid manually-added inventory spools even when material matched,
unless the user flipped "Show all spools". The filter required exact
slicer_filament_name equality, which manually-added spools don't
populate. Filter now prefers exact slicer-profile match when both
sides have one, and falls back to partial material match in either
direction (so a "PLA" spool shows up for a "PLA Basic" slot).
(3) On assign, the mismatch dialog fired on every Generic spool
because Bambu Studio / OrcaSlicer profile names carry an @printer
nozzle (variant) qualifier while the tray stores the bare base name.
Both the filter and checkProfileMatch now strip everything from @
onward before comparing.
Adds 3 regression tests covering each path.
Changed filament color circle borders from border-white/20 to
border-black/20 across all views so white spools are distinguishable
against light backgrounds.
- Change spool list from vertical to 3-column grid (2-col on small screens)
- Widen modal from max-w-md to max-w-2xl
- Increase scroll area from max-h-64 to max-h-96
- Show compact cards with name, color dot, and remaining/total weight
Backend:
- Add dual external spool support for H2D (vt_tray as list: Ext-L/Ext-R)
- Add cloud filament ID map endpoint (/cloud/filament-id-map)
- Fix RFID spool data erased by periodic AMS updates (skip tag matcher
for RFID-tagged trays)
- Fix AMS slot config overwrites RFID spool state
- Fix K-profile selection corrupts existing profiles on X1C/P1S
- Resolve K-profiles filament name via cloud filament ID map
- Update print scheduler and usage tracker for dual external spools
Frontend:
- Add printer model filtering to ConfigureAmsSlotModal (cloud/local/builtin
presets filtered by @BBL model suffix and compatible_printers)
- Add pre-population for configured slots (preset, color, K-profile)
- Add K-Profiles view with accurate filament name resolution
- Internationalize all ConfigureAmsSlotModal strings (en/de/fr/it/ja — 21 keys)
- Add 5 new ConfigureAmsSlotModal tests (model filtering, pre-selection,
color pre-population, i18n)
- Update PrintersPage for dual external spool rendering
Docs:
- Update CHANGELOG, README, website features, and wiki AMS docs