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.
Follow-up to 0f203ce: force color match now picks the right AMS slot, not
just the right printer. The slot mapper cleared tray_info_idx when applying
an override, so on a printer holding two same-colour PLA spools of different
variants (Basic GFA00 / Matte GFA01 / Silk GFA06) it could map to the wrong
one. It now keeps the variant for force_color_match overrides (both the 3MF
and no-3MF fallback paths) so the matcher pins the matching tray, and falls
back to type+colour when that variant isn't loaded. A manual filament swap
(a preference override) still clears the idx so it matches the swapped-in
spool rather than the old one.
The printer-card queue-compatibility hint applies the same variant rule.
Slicing for a P2S failed with "filament preset Bambu PLA Basic @BBL X1C 0.2
nozzle (slot 1) is not compatible with printer Bambu Lab P2S 0.4 nozzle" —
naming a profile shown nowhere in the dialog. The picked profile was
"Overture PLA Matte @0.2", whose inheritance chain roots in that X1C profile.
The dialog classifies a profile by its compatible_printers list and falls back
to reading the printer out of its name. That name carries no model, and the
list — present on the imported copy — is not shipped by every source: Bambu
Cloud omits it deliberately (rate limits), and Orca Cloud shipped it but
Bambuddy only mined filament type and colour from the same content.
Orca Cloud entries now carry their own compatible_printers, and the existing
same-name enrichment bridge carries the list onto entries that lack one, in
both directions between the cloud tiers. A bare "@<size>" name tag is read as
a nozzle size as a last resort: it can rule a printer out but never rules one
in, and implausible values are ignored rather than guessed at.
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.
Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
Focusing any text field on a SpoolBuddy screen (inventory Search, or the
Search / Color Name / Brand fields on write-tag New Spool) blanked the UI
with React error #130 ("Element type is invalid ... but got: object"). It hit
both internal and Spoolman inventories, so it was not data-specific.
The SpoolBuddy shell mounts VirtualKeyboard, an on-screen keyboard that pops up
on focusin for any input -- so every field on every SpoolBuddy page tripped it,
while the main app (no on-screen keyboard) was fine. VirtualKeyboard imports the
default export of react-simple-keyboard, a CommonJS package; under the current
bundler's CJS->ESM interop that default resolves to the module namespace object
({ KeyboardReact, default }) rather than the component, so <Keyboard> renders an
object as an element type and React throws. vitest's interop returns the real
component, so it only manifested in the browser build -- a runtime, not a type,
problem.
Add a small resolveInteropDefault helper that unwraps such an interop-wrapped
default: it returns the value as-is when already a usable element type
(function/class, tag string, or a $$typeof-marked forwardRef/memo/lazy) and
otherwise falls through to .default and named exports. VirtualKeyboard resolves
the real component through it.
A queue item's "Any <model>" button labeled itself from the file's slice
metadata while the scheduler used the row's target_model, so an X1C-sliced
item targeting H2D showed "Any X1C" above "assign to first idle H2D". The
mismatch itself was created silently: sliced-for metadata loads async, and
switching to model mode before it arrived pre-selected the alphabetically
first model (H2D on a mixed farm), after which the model dropdown hid
itself. Nothing validated compatibility, so the scheduler would hand X1C
G-code to an H2D.
Frontend: never default the target silently, keep the dropdown visible in
model mode (incompatible models disabled), label from the actual target,
warn on mismatch, block submit when incompatible.
Backend: new GCODE_COMPAT_FAMILIES table (X1/X1C/X1E/P1P/P1S interchange;
everything else exact-match; missing metadata never blocks). Queue create
and update reject incompatible targets with 400; the scheduler holds back
pre-existing mismatched rows with an actionable waiting_reason instead of
dispatching them.
Adds PHP to CURRENCY_SYMBOLS, which feeds both getCurrencySymbol() and the
Settings currency dropdown. Backend stores the code string and needs no change.
The Configure AMS Slot picker was hardwired to 0.4mm (nozzleDiameter
prop never passed from PrintersPage / SpoolBuddyAmsPage), so a 0.6
machine could only set 0.4 profiles on its trays. Resolve the real
installed nozzle per-AMS (ams_extruder_map on dual-nozzle) and pass it
in. Separately, nothing validated the sliced nozzle against the
installed one, so a mismatch reached the printer as a cryptic HMS
_8012 "Failed to get AMS mapping table". Add a fail-safe pre-dispatch
guard in _start_print that fails the item with an actionable message
before upload; no slice diameter or no reported nozzles = no-op.
Bundle import never delivered what it implied: BambuStudio's .bbscfg export
strips system processes/filaments, so importing a bundle left users without
process presets and slicing fell back to embedded settings on STL. Bundle
mode also hid the standard tier behind a constrained dropdown, the actual
trap reported here.
Removed end-to-end:
- backend: POST/GET/DELETE /slicer/bundles*, SliceRequest.bundle,
SliceBundleSpec, dispatch fork in library.py, bundle-context params on
the filament-requirements endpoints, bundle-fingerprint cache key in
slice_preview.py, SlicerApiService.{import,list,get,delete}_bundle and
slice_with_bundle, BundleSummary / BundleNotFoundError.
- frontend: BundlePicker + BundleStringDropdown, isBundleMode + every
branch, bundle state/queries/dispatch in SliceModal.tsx, SlicerBundle /
SliceBundleSpec types, three bundle API methods. buildCompatibilityIndex
loses its bundle path; presetCompatibility keeps compatible_printers
plus the @BBL fallback.
- SlicerBundlesPanel turns into a permanent static notice explaining the
removal, alternative import paths, and the new slice-time lookup order
(Imported > Orca Cloud > Bambu Cloud > Standard sidecar fallback).
- i18n: slicerBundlesRemoved.{title,description,alternatives,lookupOrder}
translated across all 11 locales; slice.bundle*, slicerBundles.* keys
removed.
Fixed (surfaced by removing bundle mode):
- _resolve_cloud and _resolve_orca_cloud now force type per slot and pin
from: "system" on the payload before json.dumps. Bambu Cloud ships
type as "printer"/"print" and routinely empty `from`; the BS CLI's
--load-settings parser rejects both with return -5 / "input preset
file invalid". Standard tier already did this; cloud paths now match.
Drop the off-white background on the A2L marketing render so it composites
cleanly on the dark theme (every other printer image in public/img/printers/
is RGBA with transparent corners; A2L shipped as opaque #F7F7F7). Resize to
320x320 to match the rest of the artwork.
Also wire A2L into getPrinterImage so the printer card actually shows the new
artwork -- without this the resolver fell through to default.png for both
the A2L display name and the N9 internal SSDP code.
- frontend/public/img/printers/a2l.png: new, 320x320 RGBA, transparent
- frontend/src/utils/printer.ts: A2L / N9 -> a2l.png, placed above the a1mini
branch
- frontend/src/__tests__/utils/printer.test.ts: 4 cases mirroring the X2D
shape -- display name, case-insensitive variants, N9 internal code,
regression guard against accidentally matching A2M / A1 / A1 Mini
100vh and window.innerHeight report the layout viewport on iOS Safari, so
the popover's Start Drying button rendered behind the bottom toolbar overlay.
Switch the popover maxHeight to 100dvh and default computePopoverPosition's
viewportHeight to visualViewport.height (with innerHeight fallback). The
flip-above decision and the body-scroll fallback now both run against the
actually-visible area, so the footer button stays reachable on iPhone.
Reporter on an A1 Mini saw the AMS slot Configure dropdown render no
Bambu / Generic filament profiles, and saw the Profiles tab strip
A1 Mini results when filtering by that model. Bambu rolled out a
profile rename mid-2026: the @BBL <code> suffix on 106 cloud profiles
shifted from the long display form to a terse model code -- e.g.
"Bambu PLA Basic @BBL A1 Mini ..." is now
"Bambu PLA Basic @BBL A1M ...". User-authored profiles still use the
long form. Bambuddy's filters did a verbatim uppercase compare
("A1M" vs "A1 MINI"), so every renamed cloud profile silently
disappeared from the picker.
Centralize the alias check in slicerPrinterMatch.ts. New
PRINTER_MODEL_SUFFIX_ALIASES table maps "A1 Mini" <-> "A1M"
bidirectionally; exported matchesPrinterModelSuffix() does the
case-insensitive compare with the alias fallback. Two consumer
sites swap to the helper:
* ConfigureAmsSlotModal.tsx (Orca cloud and Bambu cloud filter
branches) -- the AMS slot picker, hit directly and reached from
SpoolBuddy's AMS page via mapModelCode(printer?.model)
* slicerPrinterMatch.ts:classifyByBambuName -- the SliceModal
Process / Filament compatibility check
Backend printer_models.py also gets a "Bambu Lab A1M" -> "A1 Mini"
entry so server-side 3MF model normalization stays consistent if a
3MF ever embeds the short form.
Kept the alias table narrow on purpose. Wide-net aliasing (e.g.
"X1" <-> "X1C") would silently collapse physically distinct
printers. When Bambu introduces the next rename, it is one new row
in the table -- /api/v1/cloud/settings is the place to grep, called
out in the source comment.
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).
After the @BBL name fallback landed, IndividualGhost1905 reported that
an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants
into the main dropdown. The fallback's two extractors —
extractPrinterPresetModel and extractBblToken — both ended their regex
with `\s+[\d.]+\s*nozzle\s*$` and discarded the match, reducing
"Bambu Lab X2D 0.4 nozzle" and "0.40mm Strength @BBL X2D 0.8 nozzle"
to the same "X2D" string. Match was model-only; nozzle ignored.
Bambu's naming convention: 0.4 is the default and DROPS the suffix; 0.2
/ 0.6 / 0.8 carry an explicit "<size> nozzle" segment. So a process
preset with no suffix is implicitly 0.4 — not "any nozzle".
Have both extractors return { model, nozzle }, parsing the suffix out
instead of stripping. classifyByBambuName then requires both model AND
nozzle to compare equal; a null process nozzle counts as "0.4" per the
convention above. Differing nozzles fall into the existing "Other
printers" group — no new group label.
The bundle path was already nozzle-correct: a .bbscfg is scoped to one
printer-preset-name including its nozzle, and the bundle-side exact
match is therefore nozzle-aware. Only the @BBL name fallback needed
fixing. The `compatible_printers` tier is also unaffected (Bambu's
bundled `compatible_printers` lists include the full printer-preset
name with nozzle, so disambiguation already works).
If the selected printer preset name has no parseable nozzle (non-Bambu
/ hand-typed), the matcher degrades to model-only. Bambu printer
presets always carry one in practice; this is defensive.
9 new tests cover the matrix:
- 0.4 printer ↔ no-suffix process: match
- 0.4 printer ↔ 0.6 / 0.8 process: mismatch
- 0.6 printer ↔ 0.6 process: match
- 0.6 printer ↔ no-suffix process (=0.4): mismatch
- same rule applied to filament presets
- explicit "0.4 nozzle" suffix on process still matches 0.4 printer
- wrong-model still mismatches even when nozzles agree
- no-nozzle printer name degrades to model-only match
One existing test ("handles a trailing nozzle-size suffix on the @BBL
tag") had asserted that a 0.6-nozzle process matched a 0.4 printer —
the exact reporter complaint. Reframed: matching 0.4-suffix still
matches, 0.6-suffix now mismatches.
The first cut of #1325 swapped a stale hardcoded model table for
bundle-based compatibility matching: a cloud / standard process or
filament preset was classified by consulting the user's uploaded
Slicer Bundles (.bbscfg). That works perfectly when bundles cover
every printer in the user's cloud catalogue, and silently no-ops
otherwise - every cloud preset resolves to 'unknown', nothing moves
into "Other printers", and the dropdowns look identical to the
pre-fix state. The reporter saw exactly this on a clean install
with no bundles uploaded.
Restored BambuStudio's `@BBL <token>` name convention as a third
tier below the bundle path, but driven by the canonical backend
PRINTER_MODEL_MAP - exposed via a new GET /slicer/printer-models
route - rather than a manually-maintained frontend table. The
matcher inverts the registry into short-code -> printer-fragment
("X1C" -> "X1 Carbon"), normalises whitespace + case so "A1 mini"
and "A1 Mini" compare equal, and falls back to raw-token compare
for models not yet in the registry (so a future "Q1" matches
without a code change). Adding a new Bambu model still touches
exactly the one backend file already listed in the Bambu Model
Codes registry.
Tests: 2 new in test_slicer_presets.py (route returns the full
PRINTER_MODEL_MAP, route returns a copy not the live dict); 11
new in slicerPrinterMatch.test.ts covering registry-driven X1C
vs X1 Carbon, A1 vs A1 mini, H2D vs H2D Pro, P2S / H2C / H2S /
X2D (which the original hardcoded list was missing), raw-token
fallback, registry-not-loaded-yet degradation, and the
precedence rules between compatible_printers / bundle / @BBL
name. 38 slicer-presets + 36 slicerPrinterMatch + 34 SliceModal
tests green; backend ruff clean; frontend build clean.
Two recurring virtual-printer support pains, both on the Virtual Printers
settings page.
Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip
checklist — VP enabled, services running, bind interface still exists, access
code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT
/ discovery ports on the bind IP. start_server swallows per-service bind
errors, so a service object can exist while nothing is listening; probing the
bind IP from outside is the only reliable signal and it catches the common
"VP not visible in the slicer" bind-IP-conflict and stale-interface cases.
Slicer certificate: virtual printers present a TLS cert signed by a shared CA
the slicer must trust. Until now users had to docker exec in and cat
bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy
and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint.
GET /virtual-printers/ca-certificate returns only the public certificate; the
CA private key never leaves the backend. The CA is generated on demand so the
button works before the first VP is enabled.
Backend:
- services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes
- schemas/virtual_printer.py — VPDiagnosticResult
- CertificateService.get_ca_certificate_info() + manager helper
- routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic
Frontend:
- VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard
- caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/
non-secure-context fallback + downloadTextFile), de-duplicating the
existing FQDN-copy logic
- vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales
9 backend unit tests + 4 route integration tests + 6 frontend tests.
Backend ruff clean, frontend build clean, i18n parity green.
The process dropdown still mixed @BBL P2S presets into an X1C list:
slicerPrinterMatch matched cloud/standard presets by parsing the
@BBL <model> name suffix against a hardcoded model-code allow-list
that was missing P2S, H2C and X2D — so those presets resolved to
"unknown" and stayed in the main list instead of "Other printers".
Drop both hardcoded model tables. Compatibility now comes from the
user's uploaded Slicer Bundles: a bundle is scoped to one printer and
lists the presets it ships, so a preset matches a printer exactly when
some bundle for that printer contains it. New models are covered the
moment their bundle is uploaded.
The Slice dialog listed every process / filament preset regardless of
the chosen printer (#1325). Picking a printer profile now filters both
dropdowns to compatible presets; presets resolving to a different Bambu
model move into a trailing "Other printers" group.
Matching uses the slicer's own compatible_printers list for imported
(local) presets, and falls back to the "@BBL <model>" name suffix for
cloud / standard presets where no compatibility metadata is available,
so all three tiers are covered. Compatibility-unknown presets (custom
or untagged) are never hidden. The pre-pick and printer-switch paths
follow the same rule.
- UnifiedPreset gains compatible_printers, exposed for the local tier
- new frontend util slicerPrinterMatch.ts with the matching logic
- slice.otherPrinters added across all 9 locales
Adds BZD with symbol BZ$ to the Settings cost-currency picker so
users in Belize can track filament costs in their local currency
without doing 2:1 USD mental conversions.
The flame-icon onClick on PrintersPage computed popover position as a
fixed { top: rect.bottom + 4, left: Math.max(8, rect.right - 240) }
with no viewport-overflow check. The flame icon sits at the bottom of
the AMS info section on the printer card, so on most realistic viewports
rect.bottom + 4 + popover_height (~320px) overruns viewport.height and
the popover renders partially or entirely off-screen with the Start
button unreachable. Reporter (kleinweby, P1S + AMS-HT) worked around it
via DevTools to confirm the popover was actually there, just clipped.
Extract a computePopoverPosition() helper in utils/popoverPosition.ts:
- defaults to below + right-aligned to the trigger (preserves the
original visual layout when there's room),
- flips ABOVE the trigger when below would overflow AND above fits,
- stays below in the degraded case (popover taller than the viewport) —
at least the top is visible and the user can scroll inside the
popover; flipping to a top-clipped position would lose the action
buttons too,
- clamps the left coordinate so a trigger near either viewport edge
can't push the popover off-screen horizontally either.
Both PrintersPage callsites (the compact AMS row at :3498 and the
dual-nozzle layout at :4011) route through the helper.
This is part 1 of #1447. The functional drying bug — printer receives
the ams_filament_drying MQTT command, ACKs it, but never starts/stops
drying on Bambuddy's request while the printer's own touchscreen works
— is NOT addressed here. Diagnosing it needs the printer's actual
response payload (whether result: "fail" and the specific reason code),
which bambu_mqtt.py:918 currently doesn't log for ams_filament_drying.
Punted to a follow-up where I'll extend the existing extrusion_cali_* /
ams_filament_setting payload-logging path at :919-920 to cover
ams_filament_drying too, ask the reporter to retry, and fix the
command side based on what the printer actually returns.
PrintCalendar.tsx had three instances of the same UTC-shortcut anti-pattern:
1. Bucketing input dates via `date.split('T')[0]` — gives the UTC day
while the cell tooltip rendered local via `toLocaleDateString`. Same
data, two renderers, only one was tz-correct. Reporter on CDT (UTC-5)
saw evening prints jump to "tomorrow's" cell.
2. Per-cell lookup key built via `day.toISOString().split('T')[0]`. The
`day` Date objects produced by the calendar-generation loop are
local-tz (constructed via `new Date()` + `setDate`), so `toISOString`
shifted them back to UTC before the lookup — would have re-broken the
join even after the bucketing fix.
3. "Today" ring comparison used `new Date().toISOString().split('T')[0]`
too — at 23:00 local the ring would have moved to UTC-tomorrow's cell.
Fix adds a `localDateKey(input: string | Date): string` helper to
utils/date.ts that wraps parseUTCDate and formats via the local-tz
getters (`getFullYear` / `getMonth` / `getDate` with two-digit padding),
returning a stable comparable YYYY-MM-DD. PrintCalendar.tsx uses it in
all three spots so the buckets, the cell join, and the today ring share
the same local-tz axis as the user's tooltip label.
Backend stays UTC. Bucketing is a presentation concern and the browser
already knows the user's tz.
Stragglers flagged for follow-up: StatsPage.computeDateRange builds
dateFrom/dateTo for backend stats queries using getUTC* getters, so a
"this week" picked at 23:00 local on Sunday in CDT sends UTC-Monday-based
ranges to the backend. Fixing it properly also needs the backend to
filter on a tz-shifted UTC range, and Bambuddy has no user-tz setting
model today. localDateKey is in place for reuse when that work lands.
Two regressions reported in #1336:
1. spoolMatchesQuery did not include spool.id in the predicate, so
typing a numeric Spoolman ID into the Assign Spool dialog or the
Inventory page search returned no matches. Predicate now also
tests String(spool.id).includes(q).
2. The Unassign button in the spool edit modal was permanently
disabled for Spoolman-mode spools. The modal only ever queried
the legacy spool_assignments table (keyed by spool_id), but in
Spoolman mode the assignment lives in spoolman_slot_assignments
(keyed by spoolman_spool_id). Both the lookup query and the
unassign mutation now branch on the spoolmanMode prop and call
the Spoolman-flavored endpoints.
GitGuardian flagged "Bambuddy1!" and "LongerP@ssw0rd!" in password.test.ts
as potential leaked credentials. Replace with patterned placeholders
("Aa1!aaaa", "Aa1!Aa1!Aa1!") that still satisfy every complexity rule
(upper/lower/digit/special, >=8 chars) but won't trip secret scanners.
Create/Edit User modal previously had no hint about backend password
complexity, and the FE pre-check was only length>=6 — so any weak
password got bounced as a bare "HTTP 422" toast after the round-trip.
- New checkPasswordComplexity util mirroring backend's validator order
- Helper text under password inputs in both modals
- Submit disabled until every rule passes
- client.ts 422 parser falls back to JSON.stringify(detail) when the
mapped array is empty, so a bare status code never masks the real
Pydantic detail again
- i18n keys added in all 8 locales (EN/DE fully translated, others
per project's English-seed convention)
- 7 unit tests pinning the validator contract, including the
reporter's "12345678" input
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.
formatTimeOnly calls date.toLocaleTimeString([], …) which respects the
user's locale by design — the en_DK.UTF-8 locale uses "." as the time
separator, so the function correctly returns "02.30 pm" / "14.30" for
a Danish-English user. The two assertions hard-coded ":" as the
separator, which made the tests fail under any locale that doesn't
use ":". The implementation was right, the tests were wrong.
Switch the regex to use \D+ (any non-digit, one or more) for the
separator. This tests the actual contract — "hours and minutes,
separated somehow" — without coupling to a separator that varies by
locale (en_DK uses ".", some en_* locales use a narrow no-break space
at U+202F, most others use ":").
Verified passing under en_DK.UTF-8, en_US.UTF-8, and de_DE.UTF-8.
Audited every other toLocaleTimeString / toLocaleString call site in
the test suite — no other places hard-code separator characters.
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.
"Open in Slicer" emitted `orcaslicer://open?file=<URL>` and
`bambustudio://open?file=<URL>` by plain string concatenation, relying
on a stale comment that claimed the browser preserves URLs in the query
string. That ignores the slicer's own `url_decode()` on the received
query (BS post_init → url_decode + split_str; OrcaSlicer Downloader
regex + url_decode), so any already-percent-encoded character — most
commonly `%20` from filenames with spaces — decoded to a literal space
and the slicer's subsequent HTTP GET returned 0 bytes or 404.
All three URL forms now use `encodeURIComponent()` (matching what the
macOS `bambustudioopen://` branch was already doing, which is why the
bug didn't surface on macOS). Corrected the file-level comment to
document the actual invariant.
Regression test in slicer.test.ts feeds the exact issue reproduction
URL and asserts `%2520` appears in the generated href.
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
these identifiers existed in Bambuddy's registries, so the camera
service fell back to the chamber-image protocol on port 6000 (X2D
doesn't speak it), firmware-check logged "Unknown printer model: N6",
and the dual-nozzle K-profile paths — gated on the H2D serial prefix
"094" — would have treated X2D as single-nozzle.
Backend:
- Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
- supports_rtsp(): match the X2 display-name prefix and the N6 internal
code; camera now routes to RTSP on port 322.
- Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
the H2D-style cali_idx in-place edit path.
- is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
are sent as integers and external-spool ams_id 254/255 routing is
preserved (H2D-style deputy-nozzle addressing).
X2D uses hardened steel rods like P2S — it is intentionally placed in
STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
the classification.
Frontend:
- mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
- Enclosure-door badge and airduct-mode whitelists include X2D.
- MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
steel-rod lubrication, belt tension, cold-pull, and PTFE tube
(exported to enable direct unit testing).
Tests:
- test_printer_models.py: TestX2DModel (10 assertions).
- test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
- MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
regression, X1C/H2D/A1Mini regression, and model-name normalisation.
Docs:
- README: added X2 series to the supported printers table.
- CHANGELOG: new entry under 0.2.3b4 Fixed.
Credit to @krautech for the report and debug bundle, and to @legend813
for PR #989 which seeded most of the registry changes — rod-type
classification was corrected (steel, not carbon) and the dual-nozzle /
K-profile / is_h2d gaps were added on top.
Firmware update modal now shows every version from Bambu's wiki release
history, each badged Usable/Unavailable/Installed. Selecting a usable row
— newer or older than current — swaps the release notes and enables
install for that version, so rollback no longer requires hand-flashing.
Wiki scraper tightened to only read heading-anchor ids (h-XXXXXXXX-YYYYMMDD)
instead of any XX.XX.XX.XX substring, eliminating false positives like an
AMS firmware version mentioned in an H2D changelog being listed as H2D
firmware.
Refs #568
The Printer tab AMS popup and spool auto-provisioner resolved color
names from hardcoded tray_id_name tables with a suffix-code fallback —
and suffix codes like "R1" are not globally unique across material
families. A17-R1 (PLA Translucent Cherry Pink) fell through the
fallback and resolved to "Scarlet Red" (A01-R1, PLA Matte), baking
the wrong name into auto-created inventory spools.
The fix removes the hardcoded tables entirely. Backend resolves color
names via the existing color_catalog table by hex; frontend fetches a
compact {hex: name} map once per session via a new
GET /inventory/colors/map endpoint (auth-gated but not on
inventory:read — read-only views need it too) and stores it in a
ColorCatalogProvider context. A useSyncExternalStore hook cascades a
re-render into pages mounted before the fetch completes so they
refresh from HSL-fallback names once the catalog loads.
Existing auto-provisioned spools keep their stored names; only new
provisioning and live display benefit. Co-Authored-By is intentionally
omitted here per project convention — set it via git config if needed.
visibility into AMS spool assignments on printer cards without exposing
the full Inventory page (#635). The list_assignments endpoint now
requires this new permission instead of inventory:read. All default
groups (Administrators, Operators, Viewers) include it for backward
compatibility.
Closes#634
Add color-matching fallback for slot-to-tray mapping on printers that
don't provide the MQTT mapping field (snow encoding) or reject request
topic subscription. Matches 3MF filament slot colors against AMS tray
colors to resolve the correct physical tray. Returns no match when
colors are ambiguous (duplicate tray colors) to safely fall through
to existing fallbacks.
Mapping priority chain: print_cmd → MQTT snow → queue → color match →
tray_now → slot_id-1.
- Add formatStorageSize helper in FileManagerModal to display storage
in GB/MB instead of raw bytes
- Show "Overdue" label for queue items whose scheduled time has passed
by more than 1 minute
- Fix missing "in" prefix for English inDays translation
- Fix swapped justNow/now translations in Italian locale
- Fix whitespace formatting for waiting key across all locales
- Add trailing newline to date.ts and date.test.ts
refactor(utils): consolidate date and file size formatting
Extracts formatting logic into dedicated utility functions to improve
code maintainability and consistency across components.
Changes:
- Consolidate date formatting functions (formatRelativeTime, etc.)
- Consolidate file size formatting into single formatFileSize utility
- Fix file size calculation errors
- Update UI components to use new utilities
- Change "ASAP" to "Waiting" for unscheduled queue items
Tests:
- Add comprehensive unit tests for date utilities
- Add unit tests for file size formatting