github maziggy/bambuddy v0.2.5b1-daily.20260604
Daily Beta Build v0.2.5b1-daily.20260604

pre-release4 hours ago

Note

This is a daily beta build (2026-06-04). It contains the latest fixes and improvements but may have undiscovered issues.

Docker users: Update by pulling the new image:

docker pull ghcr.io/maziggy/bambuddy:daily

or

docker pull maziggy/bambuddy:daily


**Tip:** Use [Watchtower](https://containrrr.dev/watchtower/) to automatically update when new daily builds are pushed.

Added

  • Orca Cloud profile sync — end-to-end integration with the slicer + SpoolBuddy surfaces (OrcaSlicer/OrcaSlicer#14028 filed for upstream allowlist broadening) — Bambuddy now reads, lists, and slices with profiles from your Orca Cloud account alongside the existing Bambu Cloud integration. OrcaSlicer 2.4.0-alpha shipped its own cloud (Supabase-backed at auth.orcaslicer.com / api.orcaslicer.com); this integrates with it using the in-source publishable client key, a standard PKCE handshake, and the /api/v1/sync/pull profile-sync endpoint. Four sign-in providers: Google, Apple, GitHub (paste-flow PKCE) and email+password (direct grant — Orca's web sign-in offers it even though their desktop SDK refuses); UI defaults to password with the three OAuth options listed below. UX shape: the Cloud Profiles tab is now two — "Bambu Cloud" (existing, unchanged) and "Orca Cloud" (new); the paste flow's "page will fail to load — that's expected" instruction is rendered as a prominent amber callout so the connection-refused page isn't mistaken for a Bambuddy error. The Orca Cloud tab renders the same rich profile-browser layout as Bambu Cloud (search + 5 filter dropdowns + 3-column grouped grid + click-to-detail) via a parallel OrcaCloudProfilesView component. We chose paste-flow rather than a clean OAuth callback because Orca's Supabase project only honors localhost in its redirect_to allowlist. Slicer integration: the unified-presets endpoint surfaces Orca Cloud as a 4th tier above Bambu Cloud > local > standard; _dedupe_by_name and the SliceModal dropdowns both updated to walk all 4 tiers. The dedicated _fetch_orca_cloud_presets extracts filament_type and default_filament_colour inline from each profile's content (cheap because /sync/pull returns full content per profile — no rate-limit dance like Bambu Cloud's per-setting fetch), so multi-color pre-pick scoring works against Orca presets too. A separate CloudStatusBanner instance shows Orca Cloud's auth status independently of Bambu's. AMS slot integration: ConfigureAmsSlotModal accepts orca_cloud as a new preset source (prefixed orca_<UUID> to match the existing local_* / builtin_* convention), gracefully tolerating raw UUIDs from historical saves; Orca presets are treated like local imports for tray_info_idx derivation (no Bambu setting_id, generic filament-ID map by parsed material). Slot mapping persisted with preset_source='orca_cloud'. SpoolBuddy integration: SpoolFormModal and SpoolBuddyWriteTagPage fetch Bambu + Orca filaments in parallel via Promise.allSettled and concat; ConfigureAmsSlotModal opens from SpoolBuddyAmsPage's Configure flow with Orca presets surfaced first. Storage: 8 new columns on users (5 persistent + 3 transient PKCE state with 10-min TTL), dialect-branched DATETIME / TIMESTAMP, verified on SQLite and Postgres. Auth-disabled mode falls back to global Settings table. Refresh rotation: Supabase issues single-use refresh tokens; service refreshes just-in-time (<5min leeway) and persists the new pair BEFORE the downstream call so a mid-flight crash doesn't strand the user. Cloudflare: api.orcaslicer.com is behind a UA-only gate; Bambuddy/<version> clears it (no TLS-fingerprint games). Per the [[bambu-compliance-outreach]] posture we identify honestly. Preset resolver: PresetRef.source extended to 'orca_cloud' | 'cloud' | 'local' | 'standard'; _resolve_orca_cloud lists, filters, and forwards profile content. Permissions: new explicit orca_cloud:auth flag (per [[feedback_specific_scopes_over_folding]]); folded into the existing can_access_cloud API-key scope (same trust dimension as Bambu Cloud — extending automatically rather than requiring a per-key opt-in). The orca_cloud router carries the same _cloud_api_key_gate + cloud_caller() deps as the Bambu Cloud router — a copy-paste miss caught only when the SpoolBuddy kiosk's API-keyed requests came back with empty preset lists from /orca-cloud/profiles because the plain require_permission_if_auth_enabled dep returns None for API-key callers, falling through to the global Settings table that doesn't carry per-user Orca tokens. Load-bearing gotchas surfaced and fixed during the build (captured in the orca-cloud-integration project-memory file so future contributors don't re-discover them): (a) Supabase silently falls back to the project Site URL when a client passes its own state to /auth/v1/authorize — overrides GoTrue's internal redirect_to tracking, browser lands at cloud.orcaslicer.com instead of localhost; we don't send state, PKCE alone gives CSRF protection. (b) cursor=0 returns 410 cursor_too_old; bare /sync/pull with no cursor parameter is the first-sync bootstrap, same as Orca's own client. (c) The /api/v1/sync/profiles constant is declared in source but isn't deployed — returns 404. (d) Orca's content.type vocabulary is printer / print / filament, not the BambuStudio machine / process / filament triplet you'd guess from the wider source; without alias mapping every printer + process profile gets silently dropped (caught against a real account showing 54 filament + 0 process + 0 printer instead of 54+18+3). (e) Naive datetimes from Postgres TIMESTAMP WITHOUT TIME ZONE columns get .astimezone() interpreted as local time on the read path, shifting freshly-stored pending PKCE state by the host's TZ offset and instant-firing the 10-min TTL — _as_utc normalises on load. Tests: 32 unit tests on the OrcaCloudService (PKCE / token exchange / single-use refresh rotation / rejected-refresh-clears-tokens / JIT refresh / profile walk + content.type mapping); 6 preset-resolver orca tier tests (permission gate, content unwrap, auth error 401, not-found 400, dispatcher routing); 6 new orca-fetch tests in test_slicer_presets.py paralleling the Bambu Cloud fetcher (status vocabulary, permission shortcut, cache hit, type vocabulary); existing SliceModal vitest updated for the 4-tier shape; 6 frontend OrcaCloudView tests (all four sign-in providers + paste flow + connected + disconnect). i18n: ~35 new keys translated in all 10 non-English locales (de/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW); brand-name "Bambu Cloud" / "Orca Cloud" cognates allowlisted in the parity check; existing tier.cloud relabelled from "Cloud" to "Bambu Cloud" everywhere it was previously generic. Service worker: bumped to v29/v28 with a forced reload-on-activate so the SpoolBuddy kiosk (Pi + Chromium + locked into kiosk mode, no devtools, no way to navigate or refresh) picks up the new bundle on a single restart instead of needing two. Verified: backend ruff clean; full pytest pass at 5648 across the suite (-n 30 in 84s); frontend eslint + build + vitest 2051 clean; i18n parity green at 5054 leaves × 11 locales.

Changed

  • Empty AMS units no longer trigger hourly humidity/temperature notifications (#1619) — The hourly AMS sensor recorder in backend/app/main.py::record_ams_history fanned out humidity and temperature alarms for every AMS unit above threshold without checking whether the unit was actually loaded with filament. Empty AMS units still report ambient sensor readings, so users with one loaded AMS and one empty one got useful alarms for the loaded unit and steady noise for the empty one every hour. The reporter's workaround (disable all AMS humidity notifications) also killed the useful alarms — not a real choice. New _ams_has_filament(ams_data) helper inspects the firmware-reported tray_exist_bits hex bitmap (one bit per tray slot, "0" / "00" = empty unit) with a fallback to the tray array's tray_type strings for early-pushall shapes where the bitmap is missing. The recorder gates the alarm dispatch on this check per-AMS-unit, so a multi-AMS printer with one loaded + one empty still alarms on the loaded one. Sensor history still records regardless of the gate so the System page humidity/temperature charts stay continuous — the only thing the gate suppresses is the outbound notification. 9 unit tests in test_ams_alarm_gating.py cover the bitmap-zero-is-empty case, single/multi/all-loaded variants, the tray_exist_bits missing → tray-array fallback, garbage bitmap → fallback, blank bitmap → fallback, non-string bitmap → fallback (Bambu sometimes sends int), whitespace-only tray_type not counting as loaded, and defensive non-dict tray entries.

Added

  • VP MQTT bridge surfaces why net.info[].ip rewrite didn't arm (#1429 defensive)MQTTBridge._refresh_ip_encoding had 4 silent early-return paths (target_client is None, printer client has no ip_address yet, no host interface shares a subnet with printer IP X and bind_address is 0.0.0.0/empty, invalid IPv4 …). When the rewrite silently no-op'd on a user's setup, the only signal was the absence of the MQTT bridge IP encoding armed INFO line — diagnosing which path was firing meant grepping the source. Each path now emits one MQTT bridge IP encoding NOT armed: <specific reason> INFO line; the message names the actual failure (target IP, the missing-interface case, etc.). Throttled via a _not_armed_reason dedup field so an idle unarmed bridge doesn't spam one line per 30s refresh tick — only state changes log. Cleared on successful arm so a regression (e.g. printer client unbinds) re-emits the diagnostic. 5 new tests in TestNotArmedDiagnosticLogging pin each path's specific reason text, the once-per-state-change throttle, and the arm-clears-dedup behaviour. Not a fix for #1429 itself — the bridge logic is unchanged; this just turns the silent failure into visible signal so the next "fix didn't work for me" report can be triaged in one round-trip instead of multiple.

Fixed

  • Label printing produced two identical PDFs per click (#1628)LabelTemplatePickerModal.tsx::openBlobInNewTab called window.open(url, '_blank', 'noopener,noreferrer') and treated a null return as "popup blocked → fall back to <a download> click." Per the WindowFeatures spec, noopener deliberately forces window.open to return null even on success, so the if (!win) fallback fired on EVERY click. Path 1 (window.open) opened the blob tab — on Linux Chromium without an inline PDF viewer the OS saved a random-named copy (the zo70GhSL.pdf / f7w0OcDi.pdf files in the reporter's screenshot). Path 2 (fallback) downloaded a second copy named bambuddy-labels.pdf. Two identical PDFs per click. Fix: drop noopener,noreferrer. The blob is same-origin (created via URL.createObjectURL from our own fetch response), the destination is a passive PDF preview tab with no script context to abuse window.opener, and noreferrer is a no-op for blob URLs. After removal, window.open returns a real window reference on success → if (!win) only fires on genuine popup-block, single PDF per click. Existing 17 vitest cases in LabelTemplatePickerModal.test.tsx still pass; the change is comment + one parameter.

Changed

  • AMS drying now enabled for H2C starting at firmware 01.02.00.00 — H2C was previously in _DRYING_UNSUPPORTED_MODELS alongside the A1 family. Moved to _DRYING_MIN_FIRMWARE with the same 01.02.00.00 floor as H2S / P2S. Both SSDP model codes the H2C advertises (O1C, O1C2 — single- and dual-nozzle variants) get the same firmware gate so the supports_drying() check fires correctly regardless of which form is in the printer record. Test coverage extended in TestSupportsDrying: H2C / O1C / O1C2 cases added to the with-firmware pass set, the old-firmware fail set, and removed from the unsupported-models loop.

Added

  • Connection diagnostic now verifies the printer is actually publishing on its report topic (#1622) — The existing checks proved TCP + TLS + auth + SUBSCRIBE, but a printer with a wrong-cased serial — or one that simply isn't publishing for some other reason — would still pass mqtt_auth because the broker accepts the subscription regardless. The user-visible symptom in that case was "AMS / K-profiles / custom filaments missing on the slicer side": the VP bridge had nothing cached to mirror because no reports ever arrived. Bambuddy already logged Connected and subscribed, but the printer has sent zero status reports. The most common cause is a wrong or mis-cased serial number… at bambu_mqtt.py:498 when this happened, but the only way to see it was to grep container logs. New printer_publishing check turns that warning into a structured diagnostic result. Pass = the bridge has seen at least one report since the latest (re)connect; fail = zero reports across the wait window with a fix-text pointing at the case-sensitive serial. The check exposes report_messages_since_connect as a public property on BambuMQTTClient so the diagnostic doesn't reach into private state. Bounded wait with countdown UX: the bridge resets the counter to 0 on every (re)connect, so a fresh reconnect would otherwise be reported as fail before the printer's first idle push lands. The on-demand UI check polls for up to 10s (PUBLISH_WAIT_DEFAULT) at 0.5s intervals and exits the moment a message arrives — typical wall-clock is 1-2s, not the full 10. The check returns max_wait_seconds in its params so the frontend can render a countdown next to the spinner instead of looking hung. The Connection Diagnostic modal (ConnectionDiagnostic.tsx) now displays an elapsed-seconds counter (Running diagnostic... (3s)) plus the waitingForReportHint line (Listening for the printer to publish a status report — this can take up to 10 seconds.) during the pending state for the existing-printer flow. PUBLISH_WAIT_DEFAULT_SECONDS = 10 is pinned in the frontend to match the backend constant; the 2 new i18n keys ship in all 11 locales. The support-package gathering path stays fast: it calls run_connection_diagnostic without wait_for_publish_seconds, getting an instant pass/fail with no max_wait_seconds exposed. 6 new tests covering pass-on-reports-seen, fail-on-zero-after-wait, skip-on-disconnect, skip-on-missing-client, instant-no-wait-path, plus updated all-healthy + disconnected-state assertions to include the new check. i18n strings (title / pass / fail / skip) shipped in all 10 non-English locales with real translations — no English fallbacks per the project's hard rule. 5011 leaves × 11 locales in parity. Why this directly closes #1622: the reporter's bridge to printers 2 + 4 (P1S + A1 Mini real targets) repeatedly hit keep-alive timeouts and force-reconnected; on every reconnect the printer published nothing in the stale window, leaving the VP cached state empty. The slicer Device tab pulls AMS / cali_id / custom filaments from cached state — empty cache = empty dropdown. The reporter's H2D bridge stayed healthy throughout and its slicer Device tab populated correctly. The in-app Connection Diagnostic had passed (port_mqtt: pass, mqtt_auth: pass) because it didn't observe publish behaviour. The new check catches this class of failure on the user's first try.

Fixed

  • Scheduled local backup time is now interpreted as local time, not UTC (#1602 follow-up) — Pre-fix: the time-of-day picker in Settings → Scheduled Local Backups stored the value as HH:MM and _calculate_next_run in backend/app/services/local_backup.py interpreted it as UTC (datetime.now(timezone.utc).replace(hour=..., minute=...)), so a UTC+3 user who wanted a 21:00 local backup had to enter 18:00. The UI hinted at this with a literal "UTC" label, but it was still surprising. Post-fix: the picker is interpreted in the container's local timezone, resolved from the TZ env var via zoneinfo.ZoneInfo (same source the Support page's environment.timezone already shows). UTC fallback when TZ is unset or unrecognised — preserves the legacy behaviour rather than crashing. The UI now shows the resolved zone name next to the time field (Local time (Europe/Berlin) / Yerel saat (Europe/Istanbul) / etc.) via a new i18n key backup.localTimeHint with real translations across all 10 non-English locales, replacing the old backup.utc literal. New timezone field on /api/local-backup/status exposes the resolved zone to the UI. One-time behaviour change for existing users: anyone who entered a UTC time as a workaround (per #1602's UTC+3 reporter — "I have to write 18:00 to get 21:00 local") will see the first scheduled cycle after upgrade run at their local TZ offset earlier than expected. Re-enter the time as local once and it's correct from then on. No migration is shipped; the population is small and migrating around a DST boundary would be ambiguous. Tests (backend/tests/unit/test_local_backup.py): existing 5 cases pinned with monkeypatch.setenv("TZ", "UTC") so they don't depend on the test runner's TZ; 5 new — Europe/Berlin local→UTC, Europe/Istanbul (the #1602 reporter's zone) local→UTC, no-TZ-env UTC fallback, unrecognised-TZ UTC fallback, DST spring-forward gap (Europe/Berlin 2026-03-29 02:30 wall-clock doesn't exist) asserting no crash. All 30 tests pass. Frontend i18n parity green at 5007 keys across 10 non-English locales.

Don't miss a new bambuddy release

NewReleases is sending notifications on new releases.