mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
d82f4e032fefdb9269fe906ce50c6311cc3cd784
816
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b8916ac3de |
fix(presets): match Bambu cloud @BBL A1M as A1 Mini (#1649)
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.
|
||
|
|
19073f5c84 |
fix(vp): auto-derive access code from target printer in non-proxy modes
Non-proxy VPs (Archive / Review / Queue) with a target printer set up a live-mirror bridge that forwards the slicer's MQTT and RTSPS auth bytes through to the real printer. The slicer holds one code in its profile (the one it bound the VP with), and that code has to satisfy both the VP listener and the real printer at the far end of the bridge. If the codes diverge the bridge silently fails at the second hop — slicer reaches .49:8883, FINs before sending a ClientHello, retries identically. The wiki framed the code-match requirement as a camera-only concern; it isn't, all bridged protocols inherit. Fix removes the foot-gun instead of re-documenting it. When a target is selected on a non-proxy VP the access-code field switches to a read-only display showing the target's code with an Eye-toggle reveal; the backend auto-inherits on every create / update (any explicit access_code submitted alongside a target is silently overridden as belt-and-braces for non-UI clients). The required-when- enabling check now treats target-set as satisfying the access-code requirement. Standalone (no-target) non-proxy VPs still get the editable input + Save button. One-shot startup migration corrects any pre-existing mismatched rows: SELECTs diverged VPs and logs one INFO line per row for the audit trail, then UPDATEs via correlated subquery. Idempotent and portable between SQLite and Postgres. |
||
|
|
12d17bfbe7 |
fix(photo): source finish photo from forced timelapse + cleanup (#1397)
Bambu's end-gcode lowers the bed at gcode_state=FINISH. Bambuddy's live-camera grab captured the bed already dropped, ruining the photo framing. Source the photo from a brief Bambu timelapse instead — firmware stops timelapse recording AFTER toolhead parks but BEFORE bed-drop runs, so the last frame frames the finished print correctly. When capture_finish_photo is on AND the user did not opt in to timelapse for this print, force timelapse=True at dispatch + mark the new PrintArchive.bambuddy_forced_timelapse column. After extraction (success or failure), cleanup deletes the locally-attached file, clears archive.timelapse_path, and walks the four scanner directories (/timelapse, /timelapse/video, /record, /recording) trying FTP DELE against the original filename. User-opted-in timelapses pass through unchanged. Resolver lives at services/background_dispatch.py::resolve_effective_timelapse (module-level so the print queue can reuse it). Both dispatch paths wired: background_dispatch.py (Print Now / Reprint) AND print_scheduler.py:_start_print (the queue). Field testing caught the scheduler gap on the first round — AST regression test now asserts start_print(timelapse=...) references effective_timelapse, not the raw item.timelapse, so a future refactor can't silently drop it. Extractor: ffmpeg -i input.mp4 -update 1 -q:v 2 out.jpg. Decoded frames overwrite the same output file, so the file left on disk is the literal last frame regardless of duration. Bambu records one frame per layer-change, so a 16-layer cube produces a 0.6 s timelapse — the original -sseof -1.0 approach seeked before the start of the file and returned frame 0 (empty bed). Decoding every frame is fine; Bambu timelapses are short by construction even on hours-long prints. Migration adds bambuddy_forced_timelapse branched on is_sqlite() (DEFAULT 0 / DEFAULT FALSE — PG rejects DEFAULT 0 for BOOLEAN). Verified live on postgres:16-alpine. Photo-task wait_for budget extends 45s -> 75s when timelapse_was_active so the notification carries the bed-up photo instead of falling back to the live-cam grab on slow links. Scope limit, documented in the camera wiki: prints started directly on the printer touchscreen / Bambu Handy / Bambu Studio Send bypass both dispatch paths, so the override doesn't fire there. Future option: mid-print M981 S1 P20000 MQTT toggle in on_print_start. Setting description rewritten in all 11 locales to drop the "only works when timelapse enabled" caveat (Bambuddy now forces it) and explain the kept-or-deleted behaviour. |
||
|
|
eb418a5b37 |
feat(discovery): scan a custom subnet for cross-router printers (#1564)
SSDP multicast (239.255.255.250:2021) doesn't traverse routers, so a printer behind a router on a different L3 segment was invisible to "Discover Printers on Network". Docker mode had a CIDR text input but only as a fallback when zero interface subnets were detected; native mode had no subnet field at all. AddPrinterModal now surfaces an always-visible subnet picker. Detected interface subnets stay as dropdown options, plus a "Custom subnet..." sentinel reveals a CIDR text input. When custom is picked, discovery routes through POST /discovery/scan with the typed CIDR instead of SSDP (which would no-op against a foreign subnet anyway). Last custom CIDR persisted to localStorage so VLAN users don't retype every time. Scan button label and scanning / no-printers-found strings all key off (isDocker || useCustomSubnet) so wording stays "Scan Subnet..." / "Scanning subnet..." whether the user is on Docker or just picked Custom. Backend unchanged: SubnetScanner.scan_subnet() already accepts any CIDR, already caps at /22 (1024 hosts), POST /discovery/scan already takes user-supplied input. |
||
|
|
342ad31489 |
fix(projects): make edit modal scrollable so Save is reachable on short screens (#1642)
Reporter on a 1508x831 Pi display couldn't mark a project as Completed because the edit modal's height exceeded the viewport: the outer wrapper centers vertically and the inner card had no max-h and no overflow, so the top half scrolled above and the bottom half (Status dropdown + Save/Cancel) scrolled below. Workaround was a full page reload. Standard flex-modal-scroll fix: max-h-[calc(100vh-2rem)] + flex flex-col on the card; a flex-1 overflow-y-auto min-h-0 wrapper around the form fields; Cancel/Save moved into a flex-shrink-0 sibling with a border-t separator so they're always visible regardless of scroll position. Buttons stay inside <form> so type="submit" still works. |
||
|
|
4851f54595 |
feat(file-manager): split "All Files" into internal-only + External views (#1621)
Reporter linked a NAS and the auto-imported files drowned their own Bambuddy uploads in the "All Files" sidebar listing. There was no filter to escape it — only per-folder clicks. Restore the pre-external semantics: "All Files" now lists managed-storage files only. The combined across-every-external view moves to a new sibling sidebar entry, "External", that only appears when at least one external folder is linked. Backend: GET /api/v1/library/files gains internal_only and external_only query flags. Filter is on LibraryFile.is_external. Both flags set is a 400, not a silent pick-one. Frontend: new topLevelView state on FileManagerPage (default internal); the query passes the scope only when selectedFolderId is null. Mobile selector dropdown uses __top:internal / __top:external sentinels so the same state round-trips through option values. Empty-state copy distinguishes internal-empty from external-empty. |
||
|
|
9554ebd05d |
refactor(inventory): rename /reset-usage to /reset-consumed-counter to match what it actually does (issue #1644)
The old endpoint name implied that calling it would drop weight_used to
0. In practice it only stamps weight_used_baseline = weight_used so the
Inventory page's "Total Consumed" widget (weight_used - baseline) reads
0 going forward, while remaining (label_weight - weight_used) is
preserved. Calling the endpoint via curl and seeing weight_used
unchanged in the JSON response is confusing.
New paths:
- internal: /api/v1/inventory/spools/{id}/reset-consumed-counter
/api/v1/inventory/spools/reset-consumed-counter-bulk
- spoolman: /api/v1/spoolman/inventory/spools/{id}/reset-consumed-counter
/api/v1/spoolman/inventory/spools/reset-consumed-counter-bulk
Behaviour is unchanged in both modes; internal stamps the baseline
directly, Spoolman-mode PATCHes upstream used_weight=0 and the
_map_spoolman_spool read mapping reconstructs the same "displayed
consumed = 0, remaining unchanged" Bambuddy-visible shape. Parity
between modes was already in place and is preserved.
The Spoolman-client method reset_spool_usage keeps its name because it
describes what is sent upstream to Spoolman, not what Bambuddy's
endpoint promises to callers.
Frontend:
- api.resetSpoolUsage / bulkResetSpoolUsage (and Spoolman variants)
renamed to resetSpoolConsumedCounter / bulkResetSpoolConsumedCounter.
- Button labels: "Reset usage to 0" -> "Reset counter" / "Reset all
counters" (short, unambiguous); tooltips and confirm-modal bodies
still spell out the full semantics.
|
||
|
|
18d534c945 |
feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub / email+password); password defaults. Paste-flow PKCE because Orca's Supabase project only allowlists localhost redirect_to — open feature request at OrcaSlicer/OrcaSlicer#14028. Surfaces: - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same rich layout (search + 5 filter dropdowns + 3-column grouped grid + read-only detail modal) - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud > standard); separate status banner per cloud; metadata-aware pre-pick scores Orca filaments above local (Orca's sync_pull returns full content inline so filament_type / filament_colour come for free, no per-setting fetch rate-limit dance) - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed orca_<UUID> to match local_/builtin_); generic Bambu filament-ID derivation from parsed material (printer firmware can't grok Orca UUIDs); slot mapping persists preset_source='orca_cloud' - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the cloud preset list via Promise.allSettled (OrcaProfileMeta is structurally identical to SlicerSetting) Backend: - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange / single-use refresh rotation / get_user_info / list_profiles via the bare /sync/pull bootstrap path - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish, auth/password, status, logout, profiles, profiles/{id}); router-level _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers (SpoolBuddy kiosk) properly resolve their owner User; just-in-time refresh with atomic persist-before-API-call - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu Cloud fetcher (status vocabulary, 5min cache, permission shortcut); _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains orca_cloud + orca_cloud_status - services/preset_resolver.py: PresetRef.source extended with "orca_cloud"; _resolve_orca_cloud walks list + filters - 8 columns on users table for tokens (5 persistent) + transient PKCE handshake state with 10-min TTL (3); dialect-branched DATETIME / TIMESTAMP; auth-disabled mode falls back to Settings table - orca_cloud:auth permission folded into can_access_cloud API-key scope (same trust dimension) |
||
|
|
24ce250176 |
fix(labels): single PDF per click (#1628)
window.open(url, '_blank', 'noopener,noreferrer') returns null even on success per the WindowFeatures spec — `noopener` deliberately suppresses the return reference. The label-print modal treated null as "popup blocked → fall back to <a download> click", so the fallback fired on every click. Result: window.open opened the blob tab (downloading a random-named PDF on systems without an inline viewer) AND the fallback downloaded bambuddy-labels.pdf — two identical PDFs per click. Drop noopener,noreferrer. The blob is same-origin, the destination is a passive PDF preview tab with no script context, and noreferrer is no-op for blob URLs. window.open now returns a real window reference on success and the if (!win) fallback only fires on genuine popup-block. |
||
|
|
c571ad86dd |
feat(diagnostic): printer_publishing check + countdown UI (#1622)
The existing connection diagnostic proved TCP + TLS + auth + SUBSCRIBE but not that the printer was actually publishing reports. A wrong-cased serial passes mqtt_auth because the broker accepts the subscription regardless; the user-visible symptom is empty AMS / no K-profiles / no custom filaments in the slicer Device tab because the VP cached state is empty. Bambuddy already logged the actionable hint at bambu_mqtt.py:498 but only to container logs. New printer_publishing check turns that warning into a structured diagnostic result. Pass = bridge has seen at least one report since the last (re)connect; fail = zero reports across the wait window with fix-text pointing at the case-sensitive serial. Bounded 10s poll on the on-demand UI route, no wait on the support-package gathering path so bundling stays fast. Exits the moment a message arrives — typical wall-clock is 1-2s. Frontend renders an elapsed-seconds counter plus a "Listening for status report — up to 10s" hint during the pending state so the wait doesn't look hung. PUBLISH_WAIT_DEFAULT_SECONDS pinned on both sides. report_messages_since_connect exposed as a public property on BambuMQTTClient so the diagnostic doesn't reach into private state. |
||
|
|
a1cb5d5b4d |
fix(backup): interpret scheduled-backup HH:MM as local time, not UTC (#1602 follow-up)
The Scheduled Local Backups time-of-day picker was interpreted as UTC by _calculate_next_run, so a UTC+3 user had to enter 18:00 to get a 21:00 local backup. The UI labeled the field "UTC" but it was still surprising. Picker is now interpreted in the container's local timezone, resolved from the TZ env var via zoneinfo.ZoneInfo (same source the Support page's environment.timezone shows). UTC fallback when TZ is unset or unrecognised. The /local-backup/status endpoint exposes the resolved zone, and the UI renders it next to the field via a new backup.localTimeHint i18n key with real translations in all 10 non-English locales. One-time behaviour change for users who entered a UTC time as a workaround: the first scheduled cycle after upgrade will run at their local TZ offset earlier than expected. Re-enter the time as local once and it is correct from then on. No migration is shipped; migrating around a DST boundary would be ambiguous. |
||
|
|
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.
|
||
|
|
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. |