mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
2b378a429e2efb33907fe0fd0ee6d014942579a2
891
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f68eda4da5 | Post work pr #1571 | ||
|
|
e38267065c |
fix(frontend): #1602 render print-run / spool-usage / camera-token / SpoolBuddy timestamps in local time
Four frontend formatDate / formatDateTime helpers called new Date(iso) directly on backend timestamps that have no timezone indicator. Per ECMAScript, a bare "2026-06-02T07:50:00" is parsed as local time, so a UTC-stored value got displayed as if its numeric components were already local — visually identical to UTC. Same shape as the #504 fix from Feb 2026, which patched 13 sites but missed these four: PrintLogTable and SpoolUsageHistory hadn't been written yet; CameraTokensPage and SpoolBuddySettingsPage existed but were overlooked. Reporter #2 (@IndividualGhost1905) confirmed with a UTC+3 host: print log shows UTC clock value, system date shows local. PrintLogTable is the "logs/completion time" they called out; the other three are the same pattern in nearby surfaces. Replaced the bare new Date(iso) calls with parseUTCDate(iso) from utils/date.ts — the same helper every other date formatter in the codebase already uses. It appends "Z" to naive ISO strings and parses TZ-tagged strings as-is. CameraTokensPage.isExpired got the same fix because comparing a misparsed Date against Date.now() would produce false "not expired" / "expired" results around the TZ-offset boundary. Reporter #1's printer-card ETA complaint (10:50 + 57m showing 09:48) is NOT addressed by this fix. That ETA comes from formatETA(remainingMinutes) which is purely client-side (new Date() plus minutes from the WebSocket payload, then toLocaleTimeString) — for it to render UTC the browser timezone itself would need to be UTC, which is a browser / OS config issue. Audit confirmed no other regressions: grepped every new Date( call in frontend/src/. Remaining sites either pass an epoch-ms number, use the result only for .getTime() arithmetic where the offset cancels, already wrap in parseUTCDate fallback, or consume a backend timestamp that includes "+00:00" (FailureDetectionSettings reads obico_detection.py's tz-aware isoformat). |
||
|
|
5d6d928b3f |
fix(virtual-printer): #1429 net.info[*].ip cache leak + mode wire-value rename
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy. |
||
|
|
171848a0fc |
fix(slicer-presets): #1581 SliceModal refresh + invalidate on local-profile delete/import
Two-part fix for the reporter's "removed profiles still show on the slice
menu" symptom.
Local half (real bug). LocalProfilesView's import and delete mutations
invalidated ['localPresets'] (the management view's own query) but not
['slicerPresets'] (the SliceModal's unified preset query, staleTime 60s).
A freshly-deleted preset kept rendering in the slice dropdown until that
staleTime elapsed plus a refocus/remount. Both mutations now also call
queryClient.invalidateQueries({queryKey: ['slicerPresets']}).
Cloud half (opt-in cache bypass). _fetch_cloud_presets keeps a 5-minute
per-(user, token) in-process cache (slicer_presets.py:69, balances
"users see freshly-saved presets quickly" against "busy install doesn't
hit Bambu Cloud once per modal open"). Users delete cloud presets in
Bambu Studio / Bambu Handy, not in Bambuddy, so there's no event hook
to invalidate on. Rather than shorten the TTL globally, the listing
endpoint gains an opt-in ?refresh=true query param that bypasses both
the cloud cache AND the 1-hour bundled-preset cache for that one call;
the fresh result is still written back so subsequent normal callers
keep hitting the cache.
New SliceModal "Refresh" button. Lives in the preset section header
next to the cloud-status banner. Calls getSlicerPresets({refresh: true})
and writes the fresh slots into the ['slicerPresets'] cache via
queryClient.setQueryData so the spinner stops immediately rather than
triggering a second refetch. RefreshCw icon spins while in-flight;
disabled during slice enqueue to prevent double-fire.
|
||
|
|
b7d7c82501 | fix(security): WebSocket auth gate + audit-driven hardening sweep | ||
|
|
ec51394196 |
fix(security): GHSA-r2qv-8222-hqg3 — allowlist API-key permissions (CVSS 9.9)
API-key permission gates went from a 17-entry admin denylist with the three
documented scope flags (can_read_status / can_queue / can_control_printer)
enforced only inside /api/v1/webhook/* to an explicit per-Permission
allowlist consulted by every dependency:
- core/auth.py: _APIKEY_SCOPE_BY_PERMISSION maps every non-admin
Permission to one scope flag on APIKey; unmapped = 403.
_check_apikey_permissions now takes the api_key and checks the flag.
- require_any_permission_if_auth_enabled + require_ownership_permission
were returning None for any valid key with zero scope check; both now
invoke _check_apikey_permissions and fail closed.
- Two new scope flags on api_keys: can_manage_library (LIBRARY_UPLOAD /
UPDATE_OWN / DELETE_OWN / MAKERWORLD_IMPORT) and can_manage_inventory
(INVENTORY_CREATE / UPDATE / DELETE / FORECAST_WRITE — required by
SpoolBuddy kiosks). Default TRUE, backfilled from can_queue so existing
"queue-only" keys keep working and hardened "read-only" keys do not
silently gain writes.
- CLOUD_AUTH now routed through can_access_cloud for defence-in-depth
alongside the existing _cloud_api_key_gate.
- Migration column-existence check (_api_keys_column_exists) gates the
backfill so user-edited values are never overwritten on restart.
Structural drift backstop: test_every_permission_has_a_classification fails
CI on any new Permission added without an explicit scope mapping —
prevents the denylist-shape regression that grew the prior surface.
Backend 5469 tests green; ruff clean. Frontend build green; i18n parity
green across 9 locales (5005 leaves each, +6 new keys). Wiki permissions
table + allowlist callout + upgrade notes updated.
|
||
|
|
40cd45e2d5 |
fix(library): render 3D preview for sliced .gcode.3mf files (#1543)
Reporter exported a multi-plate .gcode.3mf from Bambu Studio to the
shared folder Bambuddy watches and the 3D preview tab came up empty;
if he re-uploaded the same file via the file manager, the preview
worked. Root cause: two paths classify file_type differently. The
shared-folder scan at backend/app/api/routes/library.py:1343-1348
does a compound-extension check and tags the file `gcode.3mf`; the
upload path at the same file's :1588 does a single `ext[1:]` and tags
it `3mf`. Then frontend/src/components/ModelViewerModal.tsx:71-73 had
hasModel = normalizedType === '3mf' || 'stl'
hasGcode = normalizedType === 'gcode' || '3mf'
Neither matched `gcode.3mf`, so the capabilities object landed with
both flags false and the modal rendered an empty bed.
FileManagerPage.tsx:858 also gated the Preview-3D context action on
`file_type === '3mf' || 'gcode' || 'stl'`, so for shared-folder files
the menu entry didn't even appear, and the type pill at :765-770 had
no colour case for `gcode.3mf` so it fell through to the generic
gray.
Fix (frontend-only, no backend churn):
- ModelViewerModal.tsx introduces an
`isThreeMfFamily = normalizedType === '3mf' || normalizedType === 'gcode.3mf'`
predicate used in two branches — the capabilities check
(`hasModel = isThreeMfFamily || 'stl'`, `hasGcode = isThreeMfFamily
|| 'gcode'`) and the plates-loading branch that previously hard-
gated on `!== '3mf'` and would have returned setPlatesData(null)
for the shared-folder file.
- FileManagerPage.tsx adds `gcode.3mf` to the Preview-3D action gate
and shares the gcode blue type-pill colour so sliced-output files
are visually distinguishable from source 3MFs.
The compound `gcode.3mf` classification on the backend is intentionally
preserved — it carries useful "this is a sliced output" semantics that
other UI surfaces could use later. The `canOpenInSlicer` and
`sliceableType` checks at ModelViewerModal.tsx:269, 277-280 are
deliberately left alone — a sliced output isn't openable in the slicer,
and `sliceableType` already explicitly excludes `.gcode` and
`.gcode.3mf` per the comment.
Out of scope (separate Bambu-Studio format limitation, not a Bambuddy
bug): Vlado's secondary observation that the upload-path 3D preview
"shows only one plate" even though his project has 5 plates — Bambu
Studio's .gcode.3mf export contains the model data and g-code for the
active plate only, not the entire multi-plate project. The print
picker enumerates plates via gcode_*.gcode entries inside the zip
(a separate code path), which is why the user can still pick the
plate at print time. The empty-bed fix is the data point that closes
the user-visible bug.
|
||
|
|
9347921378 |
fix(inventory): drop profile-only mismatch popup and clarify reconfigure intent (#1552)
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.
|
||
|
|
632334953c |
fix(inventory): support transparent / clear filament end-to-end (#1545)
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).
|
||
|
|
2241924312 |
fix(library): reject FAT32-illegal filename chars at rename/upload/queue time (#1540)
Bambu printer SD cards are FAT32/exFAT, which forbids < > : " / \ | ? * plus control chars and trailing dots/spaces. Library rename only blocked path separators, so a name like L|R.3mf was accepted and only failed later at FTP upload with 553 Could not create file - far from the rename action that caused it. Bambu Studio refuses these names in its save dialog; Bambuddy now does the same. New backend/app/utils/filename.py centralises validation. Wired into update_file, upload_file, print_library_file, and queue add. Existing rows with bad names are left alone (no silent rewrite of user data); users get an actionable 400 pointing at rename. Frontend rename modal mirrors the same set client-side with inline error. New fileManager.invalidFilenameChar i18n key translated across all 9 locales. 26 new tests in test_filename_validation.py. |
||
|
|
d0ff6f7dc1 |
fix(spoolbuddy): tare banner now resolves to complete or timed-out (#1536)
The TARE button on Settings → Scale set a "Tare command sent. Waiting for device..." banner with no mechanism to clear it. The daemon writes back through /calibration/set-tare which stamps last_calibrated_at on the device row, but handleTare was set-and-forget — the banner stayed forever. The "Calibration complete!" success banner had the same shape. Snapshot last_calibrated_at when TARE is pressed, set an awaiting state, invalidate the device-list query every 1s while waiting (so detection responds within ~1s, not the 10s background poll), and when the snapshot advances flip the banner to "Tare complete!" with a 3s auto-dismiss. A 15s timeout falls open to "Tare timed out — is the SpoolBuddy daemon running?" so a dead daemon doesn't trap the user on the spinner. The calibration-complete and calibration-failed banners now share the same auto-dismiss helper. |
||
|
|
e34958c3fa | Post work PR #1501 | ||
|
|
4cce575bcc |
fix(stats): cancelled bucket icon now uses semantic warning token (orange) (#1390 follow-up)
Reporter flagged that the new Cancelled row's Ban icon rendered colourless while Successful and Failed used green/red icons in the same widget. Switch the Cancelled icon to text-status-warning (amber-500) so all three rows now use semantic status tokens consistently, and the colour matches the orange Archives + notification badges already use for cancelled status. |
||
|
|
1e734fb7c6 |
fix(stats): cancelled prints get their own bucket; gauge denominator excludes them (#1390 follow-up)
Reporter (@IndividualGhost1905) saw Total: 20 / Success: 18 / Failed: 1 and asked where the 20th print went. The Quick Stats endpoint counted status == "completed" → Successful and status == "failed" → Failed, but used a raw count(*) for Total Prints, so the four other PrintLogEntry statuses (aborted, stopped, cancelled, skipped) silently inflated the total without showing up in any breakdown row. The earlier #1390 round had committed a test locking in this exact behaviour ("uses total_prints as denominator so cancelled/stopped events count"), which was wrong: it conflated user intent with print quality. Three-bucket classification, applied across the whole stats surface and matching how the rest of the codebase already groups statuses (main.py:430, 1729; failure_analysis status filter): successful = completed failed = failed + aborted (printer-detected quality failures) cancelled = stopped + cancelled + skipped (user/queue stopped) Quick Stats endpoint returns the new cancelled_prints field; ArchiveStats.cancelled_prints defaults to 0 so older fixtures still parse. SuccessRateWidget gauge now divides by successful + failed only — a cancelled roll no longer drags the gauge down — and a Cancelled row appears in the breakdown so the missing prints don't silently vanish from Total Prints. Failure Analysis service applies the same denominator fix to both the headline failure_rate and the per-week trend, so a week with several cancellations and zero failures reads as 0% rather than a misleading "failed / total". i18n: new stats.cancelled key in all 9 locales with real translations (no English fallback), parity script clean. Tests: the existing 'uses total_prints as denominator' assertion is inverted to assert the new behaviour (40 / 20 / 35 → 67% gauge, Cancelled: 35 visible). The unchanged-display path (140 / 10 / 0 → 93%) still holds since 140 / (140 + 10) = 93.33% rounds the same. 33 StatsPage tests + 6 backend stats/failure tests green. |
||
|
|
3b9633a178 |
● feat(support): include sanitized connection / VP / log-health diagnostics in support bundle and bug report (#1506 follow-up)
The three diagnostic surfaces shipped earlier this month ( |
||
|
|
6591fc011f |
fix(slicer): filter process / filament presets by nozzle diameter too (#1325 follow-up #2)
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.
|
||
|
|
5f473801f8 |
fix(file-manager): list-view actions clipped + preview modal slice button now respects Slicer API setting
Two small bugs reported in chat:
1. List-view actions column was clipped off the right edge. The grid
template fixed the actions column at 80px - a sliced 3MF row renders
7 icon buttons (Print, Schedule, Slice, 3D Preview, Download, Rename,
Delete) which need ~220px. The wrapper's overflow-hidden (there for
rounded corners) then swallowed everything past column 80px.
Switched the trailing column from 80px to min-content so it sizes to
the widest action strip across all rows, and replaced overflow-hidden
with overflow-x-auto so narrow viewports get a scrollbar instead of
vanished icons. Rounded corners survive because the border-radius is
on the wrapper itself, not on the overflow box.
2. The 3MF preview modal's "Open in Slicer" button always launched the
external slicer (BambuStudio / Orca) regardless of Settings -> Workflow
-> Slicer -> Use Slicer API. With the API enabled, the file-row Cog
already opens the in-app Bambuddy SliceModal - the preview-modal
button should match.
Added an optional onSliceWithBambuddy callback to ModelViewerModal.
When set AND settings.use_slicer_api is true AND we're in library mode
on a sliceable type (.3mf/.stl/.step/.stp), the header button becomes
a Cog labelled "Slice" and calls the in-app handler. Otherwise it
falls back to the existing ExternalLink "Open in Slicer" button.
FileManagerPage wires the callback to close the viewer and open the
SliceModal with the same file, gated on the same isSliceableFilename
+ library:upload permission checks the row's Cog already uses. No new
i18n keys - reuses slice.action ("Slice") that the file row already
ships in all 9 locales.
|
||
|
|
3058c5789b |
fix(slice): @BBL name fallback for users without slicer bundles (#1325 follow-up)
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. |
||
|
|
4686d108ef |
feat(slice): cross-printer re-slicing across nozzle classes + multi-plate slice-all
Re-slicing a 3MF authored for a single-nozzle printer (X1C, P1S, A1, P2S)
onto a dual-nozzle printer (H2D / H2D Pro) — or vice versa — previously
failed with "G-code in unprintable area of multi-extruder printers" (the
source's bed-coordinate layout lands in the H2D's per-nozzle dead zone)
or, on multi-color projects, a hard SIGSEGV inside the slicer's ZFiller
polygon-clipping. Earlier shipped a fail-fast 400 guard; this drop lifts
it and actually does the conversion by forwarding the sidecar's existing
--arrange flag when the source and target nozzle classes differ. BS
itself reconciles the embedded project_settings.config against the new
printer that way, the same way the GUI's "Switch Printer" operation
does. The guard becomes a kept-for-compat no-op.
Slice-all-plates added to the SliceModal: a checkbox for multi-plate
sources sends plate=0 to the backend, which forwards --slice 0 to the
BS CLI. Same-class slice-all produces one multi-plate output 3MF in a
single sidecar call. Cross-class slice-all loops per plate (BS's
--arrange is project-wide and would otherwise consolidate every plate's
objects onto one bed) and merges the per-plate outputs into one
multi-plate 3MF locally via the new merge_plate_3mfs helper. The toast
shows "Plate 2 of 5 — Generating G-code (47%)" through the loop.
Three side fixes surfaced during testing:
- substitute_unused_plate_filaments overwrites unused-slot filaments
with the slot-1 selection before slicing so BS's loaded-filament
temperature validator doesn't reject a PLA print whose unused slot 2
defaulted to ABS in the dropdown
- re-sliced archive thumbnail now prefers the source's per-plate
render (Metadata/plate_N.png) over the project-wide MakerWorld cover
art, because BS CLI with --arrange skips writing a fresh per-plate
preview
- re-sliced archive bed_type now lifts from the sliced output's
curr_bed_type onto the PrintArchive column the card actually reads
Schema: SliceRequest.plate range relaxed from ge=1 to ge=0 to admit
the "all plates" sentinel; SlicerApiService.slice_with_profiles /
slice_with_bundle take an `arrange` parameter.
Tests: 26 in test_slicer_3mf_convert (count / merge / substitute /
extract), 3 in test_slicer_api (arrange wire format), 9 in
test_library_slice_api (guard no-op, bed_type lift, thumbnail
fallback, new cross-class slice-all loop integration test), 2 in
test_archive_service (Auxiliaries fallback), 4 in SliceModal.test
(plate=0 toggle), 2 in SliceJobTrackerContext.test (multi-plate toast
prefix). 659 backend + 42 frontend green; backend ruff clean,
frontend build clean, i18n parity green at 4984 keys × 9 locales.
|
||
|
|
ebba1385d3 |
feat(spoolbuddy-settings): show CPU load on the device card
The SpoolBuddy daemon already reports load_avg (1/5/15 min) and cpu_count in its heartbeat, but the Settings -> SpoolBuddy card only showed CPU temp / memory / disk / system uptime. Adds a fifth tile rendering the 1-min load with core count and percent-of-cores -- e.g. "1.20 / 4 (30%)" -- next to the existing CPU temp tile. Falls back to a bare load number when cpu_count is missing and hides the tile when the daemon doesn't emit load_avg. settings.spoolbuddy.cpuLoad translated across all 9 locales. |
||
|
|
7ea4410b21 |
fix(queue): insufficient-filament warning now fires on every dispatch path (#1496)
The pre-print deficit warning from #720 only ran inside the PrintModal submit flow. Both the green ▶ button on a staged queue row (POST /queue/{id}/start) and the Virtual Printer queue-mode intake bypassed it — auto_dispatch=True VP intakes would dispatch unsupervised onto spools that physically can't complete the print. Extracted the deficit check into backend/app/services/filament_deficit.py (single source of truth, both internal inventory and Spoolman modes). POST /queue/{id}/start returns 409 with a structured deficit payload unless ?skip_filament_check=true. The dispatch scheduler runs the same check before each _start_print; a deficit promotes the item to manual_start + sets a new filament_short flag (idempotent migration on print_queue). The flag clears automatically on the next tick when the operator swaps a spool to one with enough material. Frontend ▶ catches the 409 and opens a confirm modal showing each shorted slot's required vs remaining grams; the row now renders a yellow "Insufficient filament" badge when filament_short is set. Translated across all 9 locales. |
||
|
|
32fcd85827 |
fix(library): "All Files" view now shows files inside subfolders (#1499)
The File Manager sidebar's "All Files" entry was passing include_root=true to GET /library/files because the boolean was derived from `selectedFolderId === null`. The backend's include_root flag means *root files only* when folder_id is null, so a library where every file lived in a subfolder rendered as empty. Pass include_root=false from "All Files" so the backend returns every active file across folders. Adds a regression test that mounts the page, mocks the endpoint to distinguish include_root=true vs false, and asserts both a root file and a nested file appear. |
||
|
|
e222a0ef0e |
feat(system): log-health scanner + Add/Edit-Printer setup pre-flight
Adds a passive log-health check that complements the active Connection Diagnostic. Scans Bambuddy's recent app log against a curated allowlist catalog of known failure signatures (rejected access code, FTPS :990 timeout, FTPS TLS failure, flapping MQTT, unreachable camera, SQLite "database is locked" contention), dedupes and classifies each finding as layer8/environment/bug, and deep-links to the troubleshooting wiki. Sample log lines are sanitized before they leave the process. Exposed via GET /system/health and surfaced on two surfaces sharing one SystemHealthPanel component: a System Health section on the System page, and inline in the bug reporter when the form opens. The Add-Printer and Edit-Printer dialogs gained a setup-time pre-flight: saving runs the connection diagnostic and, on a failed check, warns with a "save anyway" escape hatch instead of silently saving a printer that will immediately show offline. Log read/parse/sanitize primitives extracted from routes/support.py into a shared services/log_reader.py (behaviour-preserving); affected support tests repointed accordingly. Tests: test_log_health.py (11), test_system_api.py (2 new), SystemHealthPanel + BugReportBubble + AddPrinterPreflight + EditPrinterPreflight (8 frontend). All strings translated across the 9 locales. Backend ruff clean, full unit suite green, frontend build + eslint clean, i18n parity green. |
||
|
|
6bc6a1d683 |
feat(virtual-printer): setup diagnostic + one-click slicer-certificate export
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.
|
||
|
|
ed31b8f4a4 |
fix(ui): collapse bug-report connection diagnostic for multi-printer setups
The "Report a Bug" panel scans every configured printer on open and shows
connection problems inline. The first cut rendered a full ~6-row checklist
per problem printer, stacked — on a large fleet that pushed the description
box and Submit button below the fold in the max-w-md / max-h-80vh panel.
The diagnostic section is now a compact summary: one "N of M printers have
connection issues" line plus the affected printers as collapsed rows
(healthy printers count toward M, render no detail). Each row expands on
demand to that printer's full checklist via the shared Collapsible widget;
a single problem auto-expands since that's the case where inline detail is
wanted without a click. Panel height is now fixed regardless of fleet size.
- BugReportBubble.tsx: diagnostic query pairs each result with the printer
name (new DiagnosticEntry type); render block uses Collapsible rows
- i18n: new bugReport.diagnosticSummary ({{problems}}/{{total}}) replaces
static diagnosticHeading; diagnosticIntro reworded count-neutral — all 9
locales
- 2 new BugReportBubble tests (collapsed-then-expand; single auto-expand)
|
||
|
|
4925b4c830 |
fix(slice): re-slice correctness — model label, honest errors, filament usage, nozzle guard
Five follow-up fixes to cross-printer re-slicing, all surfaced while
testing archive re-slices.
1. Re-sliced archive now records the printer it was sliced FOR.
slice_and_persist_as_archive copied sliced_for_model from the source
archive, so re-slicing X1C->H2D still showed "X1C sliced". Read it
from the freshly-sliced 3MF's parsed metadata instead, falling back
to the source only when absent.
2. Real slicer rejections are surfaced instead of silently masked.
_run_slicer_with_fallback retried with the 3MF's embedded settings on
any sidecar 5xx — including genuine content rejections (object off
the bed, incompatible filament temps), which "succeeded" only by
re-slicing for the source's original printer. A new
_slicer_rejection_message detects the slicer's own error string and
surfaces it as a 400; the embedded-settings fallback is kept only for
true CLI crashes.
3. A failed slice opens an error modal, not a 3s toast. The slicer's
reason is actionable and a toast hides it before it can be read. New
AlertModal (acknowledge-only); SliceJobTrackerContext shows it on a
failed job. New slice.failedTitle key in all 9 locales.
4. Sliced files no longer report "0 g" filament usage. The sidecar
doesn't always populate the X-Filament-Used-* headers;
ThreeMFParser._parse_gcode_header now also reads the slicer's own
"total filament weight/length" from the G-code header, and both
slice-persist paths fall back to it when the sidecar reports 0.
5. Nozzle-class re-slice guard. Re-slicing across the single-nozzle <->
dual-nozzle boundary (e.g. X1C -> H2D) fails BambuStudio's
multi-extruder validation; both slice routes now reject it up front
with a clear 400. The dual-nozzle model classification — previously
an inline tuple duplicated across start_print and the K-profile
routes — is centralized into DUAL_NOZZLE_MODELS / is_dual_nozzle_model
in printer_models.py, consumed by all three sites and the guard.
Full cross-nozzle-class re-slicing (dual-nozzle project_settings
reconciliation) remains separately tracked.
Tests: _slicer_rejection_message, _canonical_printer_model,
guard_nozzle_class_reslice, is_dual_nozzle_model, the G-code-header
filament parse, AlertModal, and end-to-end slice-API coverage including
an X1C-archive-to-H2D 400. Backend ruff + i18n parity clean; frontend
build clean.
|
||
|
|
16da533c9a |
fix(static): serve /fonts/*.woff2 — self-hosted Inter font (#1460 follow-up)
The browser console logged "downloadable font: rejected by sanitizer" for inter-latin.woff2 on every load. The #1460 PWA fix added @font-face rules pointing at /fonts/inter-latin.woff2 and bundled the woff2 files into static/fonts/, but main.py only mounts /assets, /img and /icons as static directories. With no /fonts mount, /fonts/*.woff2 fell through to the SPA catch-all and returned index.html with 200 OK; the browser's OpenType sanitizer rejected the HTML-as-a-font. Add a /fonts StaticFiles mount alongside /img and /icons. The woff2 files themselves are valid (verified — Inter variable, latin and latin-ext subsets). Also bump the service worker STATIC_CACHE version (v26 -> v27). sw.js lists the two font URLs in STATIC_ASSETS, and cache.addAll() treats the 200 OK HTML as a successful fetch — so it had cached index.html under the font URLs and served it cache-first. The version bump makes the activate handler purge the poisoned cache and re-fetch the real fonts. |
||
|
|
51b0d28b05 |
fix(camera): show diagnostic in window-mode camera page (#1395)
The #1395 camera-diagnostic follow-up (stethoscope control-bar icon, Diagnose button in the stream-error state, CameraDiagnoseModal) shipped wired into EmbeddedCameraViewer.tsx only — the embedded camera mode. CameraPage.tsx, the standalone window opened at /camera/{id} when camera_view_mode is "window" (the default), never got it. A reporter on window mode could not see the stethoscope regardless of container rebuilds or cache clears: the camera.diagnose strings are in the bundle but come from EmbeddedCameraViewer, which never renders in window mode. Switching to overlay mode surfaced it instantly. Port the diagnostic into CameraPage.tsx: Stethoscope control-bar button between Refresh and Fullscreen, a Diagnose button next to Retry in the streamError block, and the CameraDiagnoseModal render. No new i18n keys — camera.diagnose.* already exist in all 9 locales. The backend per-model camera-profile fix from the same issue is view-mode-agnostic and unaffected. |
||
|
|
50d1984820 |
fix(printer): stop the File Manager polling the printer over FTPS every 30s (#1480)
FileManagerModal ran its file-listing query with refetchInterval 30000, opening a fresh FTPS connection (full TLS handshake) to the printer every 30s while the modal was open. On fragile controllers like the P1S that load tipped MQTT, FTP and the camera into simultaneous timeouts. Drop the interval; the listing still refreshes on open, directory change, after upload/delete, and via the manual Refresh button. Also log STL thumbnail failures with a traceback (exc_info) so a data-specific "str / str" TypeError can be located from a support bundle. |
||
|
|
e0247fc6a6 |
fix(slicer): filter process/filament presets by uploaded bundles, not preset names (#1325)
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. |
||
|
|
17e39921bb |
fix(pwa): add in-app install button and self-host the Inter font (#1460)
Bambuddy installed as a PWA on desktop but not on Android. Two causes:
- Chrome for Android removed the automatic install banner in Chrome 108.
With no beforeinstallprompt handler, Android had no install path. New
InstallAppButton captures the event and re-fires it from the sidebar.
- index.css pulled Inter from fonts.googleapis.com: breaks offline, trips
CSP, and the service worker answered the failed cross-origin request
with cached index.html. Inter is now self-hosted; the SW skips all
cross-origin requests and caches the font; CSP drops the Google hosts.
|
||
|
|
7aad3fb395 |
fix(inventory): show group totals on collapsed grouped rows (issue #1368)
With "Group similar" enabled, the collapsed group row showed a single member's values — a group of five 1 kg spools displayed 1000 g instead of 5 kg (#1368). The group header now aggregates across all members: the table view's Label / Net / Gross / Used / Remaining columns and the grid card's weight figure show group totals. Identity columns (Material, Brand, Colour) and the Cost/kg rate stay per-spool-correct since they are the group key; per-spool-only fields with no meaningful total (dates, location, note, tag ID) keep the representative member's value. The expanded per-spool rows are unchanged. New aggregateGroupSpool helper with 4 unit tests; the table group component's header prop is renamed representative -> headerSpool. Frontend-only — all data was already in the spool list. |
||
|
|
e738645b0d |
feat(slicer): filter slice profiles by printer + default from the 3MF (issue #1325)
The Slice dialog listed every process / filament preset regardless of the chosen printer, and always defaulted to the first listed preset rather than what the 3MF was prepared with (#1325). 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. Compatibility-unknown presets are never hidden. Defaults: the printer and process dropdowns default to the preset names embedded in the source 3MF's project_settings.config when those presets are available; the per-slot filament and process pre-picks prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. - UnifiedPreset gains compatible_printers, exposed for the local tier - plates endpoints return embedded_printer / embedded_process - new frontend util slicerPrinterMatch.ts; extract_embedded_presets_from_3mf - slice.otherPrinters added across all 9 locales |
||
|
|
7eba29624b |
feat(slicer): filter process & filament profiles by selected printer (issue #1325)
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 |
||
|
|
a51d59eabf |
feat(i18n): add Spanish (es) locale
Full European Spanish translation — frontend/src/i18n/locales/es.ts covers all 4899 keys with placeholders, plural forms, and inline markup preserved. Registered in i18n/index.ts (resources, supportedLngs, availableLanguages) and selectable as "Español". check-i18n-parity.mjs auto-discovers the new file; added an ES_COGNATES allow-list for genuine Spanish cognates and brand/format tokens so Check 4 does not flag them as untranslated leaks. Brings the supported-language count to 9. |
||
|
|
76e327f4a1 |
feat: connection diagnostic for "printer won't connect" triage
A triage review of the last 200 closed issues found ~1/3 were
user-side setup errors — printer not in LAN developer mode, blocked
ports, Docker bridge networking, wrong access code, cross-subnet —
each costing a multi-round-trip support exchange.
Add a Connection Diagnostic that runs those checks automatically:
- backend/app/services/printer_diagnostic.py: TCP probes of MQTT
8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network
mode, printer/host subnet match, MQTT credential class; each
check returns pass/fail/warn/skip with a localized fix.
- Routes: GET /printers/{id}/diagnostic (saved printer) and
POST /printers/diagnostic (pre-save Add-Printer flow).
- ConnectionDiagnostic.tsx: modal + shared checklist, surfaced from
the printer card actions menu, an offline-printer quick button,
the Add-Printer dialog, and a new System-page section.
- The in-app bug reporter scans configured printers when the form
opens and always shows the result inline — a healthy confirmation,
or the detected problem and its fix.
- config.yml troubleshooting link repointed to the rendered wiki
page; bug_report.yml gains a diagnostic checkbox.
Diagnostic strings translated across all 8 locales. Backend service
unit tests (15) + frontend modal tests (3). Ruff clean, frontend
build clean, i18n parity green.
|
||
|
|
016e01781a |
fix(profiles): keep the Local Profiles search bar mounted when nothing matches (#1470)
The search bar was rendered only when totalCount > 0, but totalCount is the sum of the post-filter preset lists. A query that matched nothing dropped it to 0 and unmounted the search bar along with the columns, so the user couldn't clear the query without a page refresh. Gate the search bar on hasAnyPresets (pre-filter count) instead, so it stays visible as long as any preset is imported. Split the empty state: "No local presets yet" when nothing is imported, vs a new "No presets match your search" message when a query filtered everything out. |
||
|
|
8cff425c8d |
fix(printers): AMS drying popover scrolls instead of clipping the Start button (#1458)
#1447 added computePopoverPosition() to flip the drying popover above the trigger when below would overflow. Its degraded branch (popover taller than the viewport) keeps the popover below and its comment promised "the user can scroll inside it" -- but the popover div was overflow-hidden with no max-height, so the Start button stayed unreachable on short viewports. Make the promise true: the popover is now a flex column capped at calc(100vh - top - 8px); the body scrolls (overflow-y-auto) and the header/footer are shrink-0, so the Start button is pinned and always reachable. Covers the MacBook + iPhone 15 Pro Max PWA cases in #1458. |
||
|
|
ed27b27adb |
feat(slice): cross-printer re-slicing — drop the gate, the banner, and the dead plumbing
Step 0 empirical test on 2026-05-20 disproved the "CLI cannot re-slice a
3MF for a different printer" assumption: feeding an 18-color H2D-bound
Trent900.3mf to the X1C bundle via /slice produces valid X1C G-code in
1.8s, with bed (256x256), kinematics, nozzle count, machine_start_gcode,
and bed_exclude_area all coming from the target bundle.
- SliceModal: drop !printerMismatch from isReady; remove the banner and
the sourcePrinterModel / printerProfileName / printerMismatch state
entirely. Cross-printer slicing is now indistinguishable from a normal
slice; the picker already shows the target printer.
- Remove slice.printerMismatch from all 8 locales.
- API cleanup: drop source_printer_model from /library/files/{id}/plates
and /archives/{id}/plates responses, drop the field from
frontend/src/types/plates.ts (PlateMetadata + LibraryFilePlatesResponse),
delete extract_source_printer_model_from_3mf from threemf_tools.py and
its 6 unit tests. Zero remaining consumers.
- i18n discipline cleanup in SliceModal.tsx (same drop): strip every
inline English defaultValue / positional fallback from t() calls (22
sites). Add slice.bundle / slice.bundleNone / slice.bundleAllRequired
to all 8 locales — they had no entry in any locale file and were being
served from the inline English fallback for every non-English user.
- Tests: rewrite the mismatch-warning test to assert "no banner, Slice
enabled" when models differ (regression guard); delete 2 obsolete
tests covering gate states that no longer exist.
|
||
|
|
fd620df3d6 |
feat(currency): add Belize Dollars (BZD) to currency dropdown (#1454)
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. |
||
|
|
12b0c138f7 |
fix(spoolman): clear stale fallback-tag links on assign + link, prefer slot-assignment over tag-link in UI (#1457)
Reporter on a P1S with non-RFID spools saw an old, almost-empty spool in
the AMS hover card's "Spulen-ID" block while the "Zugewiesen" block
correctly showed the freshly assigned full spool. Two layers compounded:
(1) Non-RFID slots fall back to a deterministic per-slot tag
(hash(serial) + ams_id + tray_id). The Link / Assign routes wrote
that tag to Spoolman extra.tag but never cleared it from the
previous holder on re-binding.
(2) The frontend's hover-card resolver preferred the (stale) tag-link
over the user's explicit slot-assignment. Same precedence bug in
SpoolBuddy's fill-bar resolver and slot-action picker.
Frontend: swap precedence at 5 sites — slot-assignment outranks tag-link
everywhere. FilamentHoverCard's existing match-dedupe then collapses the
two "Open in Inventory" buttons back into one.
Backend: new _clear_stale_tag_links() in spoolman_inventory.py, called
from POST /spoolman/inventory/slot-assignments (with the slot's
deterministic fallback tag) and POST /spoolman/spools/{id}/link (with
the literal tag being bound — works for RFID and fallback). Best-effort:
Spoolman 5xx and per-spool patch failures log + continue, never wedge
the bind. get_fallback_spool_tag_for_slot promoted to a public helper
mirroring the frontend's signature exactly.
|
||
|
|
0f68039416 |
Fix: AMS drying popover no longer renders off the bottom of the viewport (#1447 part 1)
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.
|
||
|
|
fcee1a6f7e |
Fix: Print Activity heatmap buckets by local date, not UTC date (#1446)
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.
|
||
|
|
a4e8ae3ab4 |
Fix: SpoolBuddy Write-Tag page honours Spoolman mode + complete ID surface (#1439)
Two bugs surfaced by @flom89 in the same thread: (1) The Write-Tag page hardcoded api.getSpools and friends regardless of inventory backend. Users in Spoolman mode saw internal spools they never created and a successful tag write would have bound the NFC tag to the wrong backend (the backend write-tag route is mode-aware; the frontend was driving it with the wrong IDs). Fix follows the InventoryPageRouter pattern: detect spoolmanMode from getSpoolmanSettings, gate the spool fetch on spoolmanModeReady to avoid the initial wrong-backend request, and branch all 6 API call sites (list, autocomplete, untag, K-profile save, single create, bulk create — the bulk variant returns a different envelope shape so the duck-typed check from SpoolFormModal is mirrored). (2) The spool ID was missing from three more SpoolBuddy components beyond the first round of #1439 work — SpoolInfoCard (the dashboard's right-side found-tag panel — the "main screen" view the reporter named), InventorySpoolInfoCard, and SpoolBuddyAmsPage's assigned-spool block. Same #1385 pattern (#<id> in muted small monospace with shrink-0). |
||
|
|
235a189e31 |
Fix: 3D preview no longer freezes the page on complex multi-part 3MFs (#1412)
The browser-side 3MF parser in ModelViewer.tsx runs entirely on the main thread (JSZip extract + DOMParser + vertex/triangle iteration), and Bambu's external-component shape chains one of these per part. Multi-color parted statues from MakerWorld can spend tens of seconds in straight-line JS, during which the modal close button can't fire and the browser shows "page unresponsive". Add nextTick() yields at four hot spots: - every 20 000 vertex iterations inside parseMeshFromDoc - every 20 000 triangle iterations inside parseMeshFromDoc - the matching loops in parse3MF's direct-mesh path - once per top-level <object> iteration in parse3MF - once per <component> iteration in parse3MF This doesn't make parsing faster — it surrenders control to the browser between batches so the modal stays interactive. Proper Web Worker refactor is a tracked follow-up. |
||
|
|
1677efb2c6 |
fix(labels): replace incorrect ams_30x15 preset with correct AMS holder sizes (#1426)
Reporter — the same person who originally requested the labels feature in #809 — discovered that the ams_30x15 preset's 30x15 mm dimension didn't actually fit any variant of the MakerWorld AMS Filament Label Holder (model 752566) it advertised. Two new presets replace it: - ams_holder_74x33 (74 x 33 mm) matches the printable label STL bundled in the MakerWorld project - ams_holder_75x55 (75 x 55 mm) fits the cardstock-insert variant the reporter validated on bench Both cross the 20 mm height threshold so they land in the roomy layout branch — swatch on the left, QR on the right, multi-line text (brand, material, hex code, spool ID) in the middle. The old 30x15 mm preset couldn't fit a QR code; the new ones do. No DB migration: the preset name was never persisted. Callers scripting the old ams_30x15 value get a clean 422 at the route's Literal validator with the new valid values listed. i18n: replaced inventory.labels.templates.ams.{label,hint} with amsHolderSmall and amsHolderLarge across all 8 locales with real translations; parity guard cleaned of the stale English-fallback cognate entries. Parity holds at 4856 leaves per locale. Tests: backend label renderer + integration tests cover both new presets; LabelTemplatePickerModal test updated for the 6-button grid and the new template value in the API-call assertion. |
||
|
|
9bcaafbc1a |
fix(assign-spool): refresh printer status after assignment so card updates without manual Force-refresh (#1414 follow-up)
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. |
||
|
|
e2df0fc601 |
fix(ams): physically-empty slots report state=9 and render distinctly from reset slots (#1322 follow-up)
Two-part fix for the #1322 follow-up by @RosdasHH. Data layer. The previous narrow heuristic in printer_manager.py only caught the bare {"id": N} payload firmware sends right after a printer restart. In steady-state operation — and on the more common post-Reset-Slot path on P1S and A1 Mini BMCU — firmware sends a populated payload and signals emptiness via the tray_exist_bits bitmask. We already parse that bitmask and use it to wipe stale tray_type / tray_color / tag_uid fields, but never touched the state field, so downstream readers (printers.py API serializer, inventory.py's tray_state in {9, 10} short-circuit, AMS card) saw state: null and had to guess from absent payload fields. Fix lifts tray["state"] = 9 (int — not "9"; inventory.py:1358 uses == not `in {...}` so a string would silently miss and the reporter's deadlock would come back) to the outer `if not slot_exists` branch, so the bitmask path now writes the canonical "no spool" code for every empty slot regardless of stale fields. The narrow heuristic in printer_manager.py:797 stays as belt-and-suspenders for any MQTT path that doesn't flow through _handle_ams_data. UI layer. With the data flow now consistent, the AMS slot card renders physically-empty slots distinctly from reset slots, per reporter's mockup. New helper getEmptySlotKind(tray) returns "physical" (state ∈ {9, 10}), "reset" (any other empty state), or null (loaded). The inline label below the slot circle reads "Empty" for physical and "Reset" for reset; pre-fix both showed an em-dash. FilamentSlotCircle gains an emptyKind prop that picks a quieter dashed border colour for reset slots so the visual hierarchy reads loaded > reset > physically empty. EmptySlotHoverCard gains a kind prop and switches between "Empty slot" and "Slot reset — no spool assigned". |
||
|
|
fc32b388de |
fix(stats): align Filament Used / By Time / Success Rate with Total Consumed and Total Prints (#1390 follow-up)
Three independent root causes behind the divergences the reporter flagged after the archived-spool fix shipped — fixed together. (1) Filament Used vs Total Consumed. _compute_run_filament_grams returned the slicer estimate for completed prints even when inventory had measured the actual AMS weight delta. That made Stats and Inventory two different sources of truth: Stats showed slicer-estimate grams, Inventory showed AMS-tracked grams, and the two never agreed. Reordered the helper so the tracked spool delta (same source that drives weight_used behind Total Consumed) takes priority for every status. Slicer estimate stays as the fallback when no inventory was tracked; partial-progress scale stays as the fallback for failed/cancelled with no tracker. The _run_cost block right next to it was already tracker-first; only filament_used_grams was inconsistent. (2) Printer Stats By Time vs Quick Stats Print Time. /archives/slim only set actual_time_seconds when status == "completed". For failed/cancelled rows the frontend fell back to print_time_seconds (the slicer's full-print estimate — wrong number for a print that failed at 15%). Quick Stats already summed elapsed duration across all statuses, so the two halves of the page disagreed by the (estimate - actual-elapsed) gap on every non-completed event. Dropped the completed-only gate; failed/cancelled now report measured elapsed. (3) Success Rate %. Was successful / (successful + failed), excluding cancelled / stopped from the denominator. With "Total Prints: N" displayed right above the gauge that produced confusing numbers — 4 successful, 0 failed, 48 cancelled showed 100% out of an apparent 52 prints. Switched to successful / total_prints — matches the count the user reads from the widget header. |
||
|
|
ae0f485a84 | Post work PR #1413 |