Spoolman keeps a full spool's net weight on the spool (initial_weight)
and falls back to the filament's weight, but Bambuddy read only the
filament, so a 250 g spool of a 1000 g filament showed and synced as
1000 g. The list, weigh, AMS sync, SpoolBuddy scale, remain-% tracking,
fill bar and cost now share one lookup. Create writes initial_weight,
and a label-weight edit writes it instead of patching or duplicating the
filament. The spool form's cost per kg is converted at the spool's size
to and from Spoolman's per-spool price.
Spoolman resolves a spool's tare from the spool, then the filament, then
the vendor's empty_spool_weight. Bambuddy skipped the vendor and fell
back to 250 g, so weighing a spool whose tare was set only on its vendor
gave the wrong remaining weight. The inventory weigh action, the
SpoolBuddy scale and the displayed core weight now share one lookup, and
"keep old weight" on a filament change stamps a vendor-inherited tare.
A Ziro "Colorful Mist" -- yellow, cyan and pink, effect Tri Color --
hovered on the printer card as a single flat pink rectangle. A printer
reports exactly one tray_color hex per tray and nothing else, so
telemetry cannot describe a gradient or a surface effect and never will.
The header now paints the bound spool's own swatch whenever that spool
declares extra colour stops or an effect, through buildFilamentBackground
-- the builder the Inventory swatches already use, so the two surfaces
cannot drift apart. A plain single-colour spool keeps the flat
backgroundColor it has always had, and a slot with nothing bound is
untouched, so the common case goes nowhere near the gradient path.
The gate is "any stop at all", not "more than one". buildColorLayer
ignores rgba the moment stops exist, so a one-stop spool renders that
stop rather than the slot hex; skipping it would leave this card showing
a different colour from the Inventory row for the same spool, which is
the class of disagreement the shared builder exists to prevent.
isLightColor now tests the colour actually on screen. Once the spool's
swatch is painted the base is no longer the slot hex -- a single stop
replaces it outright, and an effect-only spool paints the spool's own
rgba -- so testing the slot hex would pick the text colour for a
background that is not there.
Above one band no single hex can decide legibility, and the name sits
dead centre where a multi-stop background is likeliest to change under
it. So a genuinely multi-band header puts the name on the same scrim the
vendor badge already uses. One stop, or an effect over one colour, still
vendor badge already uses. One stop, or an effect over one colour, still
leaves a real base colour to test and keeps the contrast rule it had.
Spoolman mode gains the gradient in the process. Spoolman has held the
stops in filament.multi_color_hexes all along and the label renderer has
been reading them for releases, but _map_spoolman_spool never returned
them -- so the identical roll registered in Spoolman rendered flat while
the internally-managed one did not. Both now share one parser rather
than reading the same field two ways.
What stays asymmetric is Spoolman's own limitation, and it is pinned by
a test rather than left to be rediscovered: Spoolman has no field for a
surface effect at all. Its only neighbouring field,
multi_color_direction, says how the stops are laid out, not that the
roll is silk or glitter. effect_type is therefore None for a Spoolman
spool instead of guessed at, and silk/sparkle/wood remain internal-only.
The other two halves of the report -- the header naming the colour
"White" instead of "Colorful Mist", and the print dialog offering
"A3: PLA (White)" -- were already fixed on dev by #2875 and by the
slot-naming change that landed the day after this was filed. Neither is
in 1.2.5.3, which is what the reporter is running.
Reporter saw a 544 g spool jump to 1000 g after pressing the eraser.
"Spools and remaining weights are not changed" - the dialog promised
this; the implementation did the opposite. Root cause was an
architectural conflation: `weight_used` did double duty as the
resettable "consumed since tracking started" counter AND as the basis
for the displayed remaining (`label_weight - weight_used`), so zeroing
it correctly cleared the stat but unavoidably reset remaining to full.
Spoolman has separate `used_weight` and `remaining_weight` fields, so
the API call there was correct - but Bambuddy's frontend was also
computing remaining as `label_weight - weight_used` for Spoolman
spools (ignoring Spoolman's real `remaining_weight` field), so the
same visual bug bit there too. Inventory-mode parity required fixing
both halves in one drop.
Internal mode
- New `weight_used_baseline` column (Float DEFAULT 0) on `spool`.
- Reset stamps `baseline = weight_used` and leaves `weight_used` alone.
- Displayed consumed = `weight_used - baseline`; remaining =
`label_weight - weight_used` (unchanged).
- Subsequent prints continue to grow `weight_used`, so the resettable
counter naturally tracks post-reset delta and remaining keeps
decrementing across the reset.
Spoolman mode
- `_map_spoolman_spool` now reads Spoolman's `remaining_weight` field
and returns a synthetic `weight_used = label - remaining` so the
frontend's remaining calc matches Spoolman's real stored value;
`weight_used_baseline = synthetic - real_used_weight` so the consumed
counter (`weight_used - baseline`) matches Spoolman's `used_weight`.
- Fallback path (no `remaining_weight` set) preserves the old behavior.
- Related fix: `update_spool` (Spoolman PATCH) was deriving the default
`weight_used` from `used_weight`, so editing unrelated fields AFTER
a reset would patch Spoolman with `remaining_weight = label - 0 =
label`, trampling the real value. Now derives from
`remaining_weight` so non-weight edits preserve physical state.
Frontend
- `InventoryPage` `totalConsumed` aggregate switched to
`Math.max(0, weight_used - (weight_used_baseline ?? 0))`.
- `ForecastPanel` `computeDeltaRate`, `totalUsedG`, and the per-spool
"consumed" table cell got the same treatment so forecast and
inventory aggregates stay coherent across a reset.
- `?? 0` keeps pre-migration installs rendering correctly until
`init_db()` runs the idempotent ALTER TABLE.
Migration
- `ALTER TABLE spool ADD COLUMN weight_used_baseline REAL DEFAULT 0`
via `_safe_execute` - SQLite and Postgres both accept it; verified
end-to-end on Postgres 16.
Reporter pgladel edited a spool's Color Name in Spoolman mode, hit
Save, and saw the value snap back to the subtype on the next read.
The earlier #1319 fix correctly handled the read/form-prefill half
(the color_name_is_synthesized flag, blank-on-synth form init), but
the write half assumed Spoolman has a `color_name` field on Filament.
It doesn't. Verified against the live FilamentUpdateParameters schema
on Spoolman 0.23.1 — the accepted fields are name, vendor_id, material,
price, density, diameter, weight, spool_weight, article_number,
comment, settings_extruder_temp, settings_bed_temp, color_hex,
multi_color_hexes, multi_color_direction, external_id, extra. No
color_name. Spoolman's PATCH happily returns 200 for
{"color_name": "Red"} and silently discards the unknown key, so
find_or_create_filament was either patching a void or creating
filament after filament with the same field-that-doesn't-stick (which
is what produced the "BB also created a bunch of new filaments"
duplicate trail on each save attempt).
The fix follows the same pattern as the existing BambuStudio slicer-
preset storage: persist color_name on spool.extra.bambu_color_name as
a JSON-encoded string, register the extra field via
ensure_extra_field before write (Spoolman 400s on unknown extra keys),
and read it back in _map_spoolman_spool with priority
extra > filament.color_name (forward-compat for any future Spoolman
release that adds the field) > subtype synth.
Dropped the now-dead color_name passing through
find_or_create_filament and create_filament — Spoolman would discard
it anyway and keeping the dead pipe risked the same confusion the
next time someone reads this code. The previous "match by name then
patch color_name" loop is gone; what survives is the name-match
resilience that lets an AMS-sync-created filament named "Glow" still
match the user-driven edit's composed "PLA Glow", which prevents
re-introducing the duplicate-filament trail.
The frontend form's color_name_is_synthesized handling is unchanged
— that part already worked.
* feat(auth): proxy OIDC provider icons server-side (#1333)
Strict img-src CSP blocked external OIDC icon hosts on the login page.
Loosening CSP was rejected via the MakerWorld precedent, so icons are
proxied: admin sets icon_url, backend fetches and caches the bytes in a
deferred BLOB column, the SPA renders from a same-origin
/api/v1/auth/oidc/providers/{id}/icon endpoint.
Editing a spool's color name on Spoolman-backed inventory appeared to
accept the new value but the inventory list column and the next edit
showed it back to the subtype. Three layers stacked to produce this:
1. find_or_create_filament matches by material/name/color_hex/vendor —
color_name is intentionally not part of the match key, but on a
match it returned the existing filament's id unchanged, silently
dropping the new value.
2. The read helper falls back to subtype when filament.color_name is
empty (kept on purpose: without it Spoolman installs that don't
fill the field render every spool as "Unknown color").
3. The edit form prefilled color_name from spool.color_name — which
on those installs was the synth value. Changing subtype but not
color_name silently round-tripped the OLD subtype back to Spoolman
as if it were a real user-set color_name.
Fixes:
- find_or_create_filament now patches the matched filament's
color_name via the existing patch_filament wrapper when the request
differs. Parameter convention: None = don't touch, "" = explicit
clear, any other string = set/update. A patch failure is logged but
does not block the match.
- The PATCH route uses model_fields_set to distinguish "field omitted"
from "field explicitly set to null" (mirrors the existing
storage_location pattern at the same site).
- The map helper returns color_name_is_synthesized: bool. The edit
form leaves the input blank when true, so the user sees the real
stored state and can't accidentally round-trip the synth value back.
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 SSRF guard added in this PR rejected all RFC-1918 private and loopback
addresses, which breaks Bambuddy's primary deployment topology — Spoolman
running on the same LAN as Bambuddy (192.168.x.x, 10.x.x.x, 127.0.0.1).
Users hit "Spoolman URL must not point to a private, loopback, link-local,
multicast, or unspecified address" on legitimate setups.
Rescope the guard to block what's actually dangerous in this context:
cloud metadata endpoints (AWS/Alibaba IMDS), multicast, unspecified,
non-http(s) schemes, and numeric-encoded IP bypasses. Loopback and
RFC-1918 ranges are now explicitly permitted.
Tests:
- test_ssrf_blocked_schemes_and_addresses updated with refined block list
- test_ssrf_allows_lan_spoolman_topologies (new) asserts loopback +
RFC-1918 are accepted so this regression cannot recur silently
- TestSpoolmanInventorySSRFSpoolBuddyPath parametrize lists trimmed
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.