github maziggy/bambuddy v0.2.3b4-daily.20260416
Daily Beta Build v0.2.3b4-daily.20260416

pre-release7 hours ago

Note

This is a daily beta build (2026-04-16). 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.

New Features

  • Move Build Plate from Printer Card (#791) — The printer card controls row now has a Z-jog badge between the speed control and the stop/pause buttons. Click the up/down arrows to move the build plate; click the middle label to switch the step size (1 / 10 / 50 mm). When the printer is not homed (typical right after a print finishes), the first jog opens a Bambu Studio-style warning modal with Home Z, Move anyway (bypasses soft endstops for this move), or Cancel. After the first "Move anyway" in a session, subsequent jogs skip the dialog. Disabled while a print is running. Backed by new POST /printers/{id}/bed-jog and POST /printers/{id}/home-axes endpoints, both gated behind printers:control. Thanks to cadtoolbox for the request.
  • Printer Card Status Badges & Quick Controls — The Printers page printer card now exposes new at-a-glance controls inspired by the Home Assistant Bambu Lab integration:
    • Enclosure Door badge in the top status row (DoorOpen/DoorClosed icons, green when closed, yellow when open). Detection uses the right MQTT field per printer family — home_flag bit 23 on X1/X1C/X1E and the top-level stat hex string bit 23 on P1/P2/H2 — and falls through the existing WebSocket push (status-change dedup key now includes door state, so toggling the door alone triggers a live badge update without waiting for the 30 s REST poll).
    • Airduct Mode badge beside the print speed control (Snowflake/Flame icons, sky for Cooling and orange for Heating). One-click dropdown switches the printer between cooling and heating via the existing set_airduct MQTT command. Gated to P2S/H2D/H2C/H2S.
    • Force Refresh menu entry in the printer card kebab menu (RotateCw icon) that re-requests a full pushall MQTT status report from the printer without forcing a reconnect.
  • AI Print-Failure Detection via self-hosted Obico ML API (#172) — New Settings → Failure Detection tab wires Bambuddy to a self-hosted Obico ml_api container (no Obico account, no cloud, no WebSocket). While a print is running, the detection service periodically hands the printer's camera snapshot URL to the ML API, which returns YOLO failure-detection scores. Scores are smoothed over time using Obico's own EWM + short/long rolling-mean math (30-frame warmup, alpha = 2/13, short window ≈ 5 min at 10s/frame, long window ≈ 20 h) so a single noisy frame cannot trigger an action. Sensitivity (Low / Medium / High) scales the LOW/HIGH thresholds; when the smoothed score crosses HIGH, the configured action runs exactly once per print: Notify only, Pause print (MQTT pause command), or Pause and cut power (pause + turn off any smart plug linked to that printer). A per-printer toggle lets you monitor all connected printers or just a subset. The Status card shows whether the service is running, the active thresholds, each monitored print's current verdict (safe / warning / failure), and a live rolling detection history. Requires that the External URL setting (General tab) points to a hostname/IP reachable from the ML API container, since the ML API fetches snapshots by URL.

Improved

  • Firmware Update Modal Shows All Announced Versions (#568) — The firmware update dialog now lists every version announced on Bambu Lab's wiki release history, not just the single newest one. Each row shows whether an offline firmware file is actually available for that version — rows marked Usable (green) can be installed, rows marked Unavailable (gray) are announced but have no downloadable package yet (common for hot-fix releases like 01.01.03.00 which Bambu only ships as OTA). The currently installed version is highlighted with a blue Installed badge. Selecting any usable row swaps the release-notes block at the top to that version's notes and enables the Install button for it — including older-than-current versions, so you can roll back to a previous firmware without having to hand-flash a file. The wiki scraper was tightened to only extract version numbers from heading anchors (e.g. id="h-01030000-20260303") so incidental version mentions in release-note prose — like an AMS firmware reference in an H2D changelog — no longer get mistaken for H2D firmware releases. Thanks to Cornelicorn for the request.
  • Spoolbuddy Device Controls in Settings (#962) — Each Spoolbuddy device card in Settings → Spoolbuddy now exposes five one-click actions alongside the existing Unregister button: Update (trigger daemon software update), Restart Browser (kiosk UI), Restart Daemon, Reboot (device), and Shutdown. Each action shows a confirmation dialog before queueing the command; buttons are disabled when the device is offline. Uses the existing /spoolbuddy/devices/{id}/update and /spoolbuddy/devices/{id}/system/command endpoints — no new backend work needed. Thanks to TravisWilder for the request.
  • Support Bundle Covers All Settings & SpoolBuddy — The support bundle / bug-report payload now dumps every row in the Settings table instead of filtering by a hard-coded allowlist: sensitive keys (tokens, passwords, URLs, paths, emails, etc.) have their values replaced with [REDACTED] but the key itself is kept, so new config flags automatically show up in future bundles without a code change. Also adds an integrations.spoolbuddy section listing registered SpoolBuddy devices (firmware version, NFC/scale hardware, calibration, online state, uptime) — anonymized, no hostnames/IPs/device IDs.
  • Settings Search Finds More Cards — The cross-tab search field at the top of Settings now finds Sidebar Links, Spoolman, Spool Catalog, Color Catalog, all four Failure Detection sections, Advanced Email Authentication, SMTP Test, Authenticator App (TOTP), Email OTP, 2FA Linked Accounts, Single Sign-On (OIDC), LDAP Server Configuration, and the four Backup sub-cards (GitHub, History, Local, Scheduled). Powered by a new module-level registry (frontend/src/lib/settingsSearch.ts) so future settings register themselves next to their component instead of being forgotten in a central array.

Changed

  • Plate-Clear Confirmation Disabled by Default — New installs ship with Settings → Workflow → "Require Plate-Clear Confirmation" off. Multiple new users reported queued prints appearing to not start because the prompt was waiting for acknowledgement; opt in from Workflow if you want the confirmation gate.

Security

  • Dependency Updates for Published Advisories — Bumped two dependencies flagged by vulnerability scanners. python-multipart 0.0.22 → 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a denial-of-service triggered by large preamble or epilogue data around a multipart boundary — the 0.0.26 release now skips the preamble before the first boundary and silently discards the epilogue after the closing one. Bambuddy uses python-multipart transitively through FastAPI/Starlette for form and file-upload parsing, so any authenticated endpoint accepting multipart/form-data (e.g. backup restore, project thumbnail upload) was exposed. dompurify 3.3.3 → 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (the function-form ADD_TAGS could bypass FORBID_TAGS); Bambuddy's two call sites (ProjectDetailPage, ProjectPageModal) only use array-form ALLOWED_TAGS/ALLOWED_ATTR, so the specific bypass was not reachable, but the bump still hardens the sanitizer against future misconfiguration and clears the audit warning.

Fixed

  • Bambu Lab X2D Support (#988) — The newly released X2D (dual-nozzle, enclosed, hardened steel rod gantry, AMS 2 Pro compatible) identifies itself as internal model code N6 via SSDP/MQTT, and serials begin with 20P9. Because neither the code nor the prefix existed in any of Bambuddy's model tables, multiple paths silently fell back to wrong defaults: the camera service routed to the chamber-image protocol on port 6000 (which the X2D doesn't speak) instead of RTSP on port 322 — the reporter saw Chamber image: data is not a valid JPEG spam and no stream; the K-profile edit/delete path conditioned its in-place cali_idx write on the H2D serial prefix 094 and would therefore have treated X2D as a single-nozzle printer even though its dual-extruder layout matches H2D; the firmware-update check logged Unknown printer model: N6; and the virtual-printer model registry had no way to emulate X2D. Added the N6 → X2D mapping across every registry (PRINTER_MODEL_ID_MAP, PRINTER_MODEL_MAP, ETHERNET_MODELS, STEEL_ROD_MODELS, CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys and wiki path, virtual-printer SSDP product names and serial prefix, DB migration vp_model_fixes), extended supports_rtsp() to match X2 display names and the N6 internal code (camera now goes to port 322), expanded the dual-nozzle serial prefix check in kprofiles.py and the K-profile delete command in bambu_mqtt.py to also accept 20P9 so the H2D-style cali_idx in-place edit path runs on X2D, added X2D to the is_h2d model-family gate that selects the integer-format timelapse/bed_leveling/flow_cali/vibration_cali/layer_inspect fields in the MQTT print command, and added X2D to the frontend's door-badge and airduct-mode whitelists, mapModelCode lookups on both the Printers page and Spoolbuddy AMS page, and the MaintenancePage wiki-URL resolver (X2D inherits P2S's steel-rod lubrication, belt-tension, nozzle cold-pull and PTFE wiki pages, since its hardware is closer to P2S than to H2). Credit to krautech for the report and the debug bundle, and to legend813 for the initial PR (#989) that seeded most of the registry changes — the classification was corrected (X2D uses hardened steel rods like P2S, not carbon rods) and the dual-nozzle/K-profile gaps were added on top.
  • Print Speed Icon Not Updating Live When Changed on Printer (#993) — Changing the print speed mode from the printer's own panel (instead of from Bambuddy) did not update the speed icon on the Printers page card; the new value only appeared after a full page reload. The MQTT parser was already tracking spd_lvl and updating state.speed_level correctly, but the WebSocket serializer (printer_state_to_dict) was missing the field — so live status pushes never carried speed_level, and the frontend's merge-over-old-cache update left the icon stuck on its previous value. The REST /status endpoint used on initial page load already included it, which is why reloads worked. Added speed_level to the WebSocket payload. Thanks to chesterakl for reporting.
  • Camera Popup Shows "Valid camera stream token required" With Auth Enabled (#979) — When Camera View Mode was set to "Window" and authentication was enabled, clicking the camera button opened a popup that immediately failed with "Valid camera stream token required", while the embedded overlay kept working. Two root causes: (1) window.open(...) passed noopener in the popup features, which severed the opener link and prevented the browser from copying sessionStorage (where the auth token lives) into the popup — so the new window booted unauthenticated and the POST /printers/camera/stream-token fetch returned 401, leaving the <img> src without the required ?token= query param; (2) even once the token arrived, CameraPage computed its URL from the module-level stream-token cache on render and never re-rendered when the cache was updated in a useEffect, so the first paint locked in a tokenless URL that the backend kept rejecting. Fixed by dropping noopener from the camera popup features (same-origin, trusted window) so sessionStorage is inherited, subscribing CameraPage to the camera-stream-token React Query so it re-renders the moment the token resolves, and appending the token directly from the reactive query value instead of the effect-synced module cache — the <img> src stays empty until the token is ready, so no tokenless request ever leaves the popup. Embedded-overlay mode was unaffected. Thanks to VREmma for the reproducer.
  • Obico ML API Got 401 When Fetching Snapshot with Auth Enabled (#172) — The Obico failure-detection service handed the ML API container a snapshot URL (/api/v1/printers/{id}/camera/snapshot) for it to GET directly, but when Bambuddy authentication was enabled the endpoint returned 401 and the ML API surfaced "Failed to get image" (visible as a 400 from the ML API back to Bambuddy). The detection service now appends a short-lived camera-stream token to the snapshot URL — the same token scheme already used by <img>-based camera consumers, which the snapshot endpoint already accepts. The token is cached on the service and refreshed before its 60-minute expiry, so no extra per-call DB churn. When auth is disabled the token is simply ignored. Thanks to fblix for reporting.
  • Obico Detection Timed Out on Slow Snapshot (#172) — Second wave of #172 — once the auth/401 fix landed, fblix reported a new failure mode: Read timed out. (read timeout=5). Bambuddy's /camera/snapshot endpoint takes 5–10 s on cold calls (it spins up a TLS proxy + ffmpeg + waits for the next RTSP keyframe), but Obico's ML API server.py has a hardcoded timeout = (0.1, 5) on the URL it fetches — any snapshot that doesn't return within 5 s gets the entire detection cycle reported as "Failed to get image". Raising the timeout in the user's container was only a workaround; every Obico container would have the same ceiling. Fixed by flipping the flow around: the detection loop now captures the JPEG locally (with a long 20 s timeout that we control), stashes the bytes under a random 32-byte single-use nonce, and hands Obico's ML API a new /api/v1/obico/cached-frame/{nonce} URL that returns the pre-captured bytes instantly. Obico's 5 s timeout no longer races the capture pipeline — its fetch is a pure in-memory lookup. The nonce is the credential (URL-safe, 256 bits of entropy), single-use (popped on read), and expires in 30 s, so the endpoint can be unauthenticated without widening the camera access surface. External cameras (MJPEG/RTSP/HTTP snapshot) are captured via the same capture_frame helper used by the snapshot endpoint. Thanks to fblix for the detailed reproducer with the exact timeout numbers.
  • Direct Print from Library Not Attributed to User — Clicking the Print button on a library file dispatched the job with no created_by_id, so the resulting archive had no owner and the print didn't show up in per-user statistics. The Queue and Reprint paths already forwarded the authenticated user; the library POST /files/{file_id}/print endpoint now does the same, reading the user from the JWT and passing it through to the dispatcher so direct prints are attributed like queued and reprinted ones.
  • Add/Edit Printer Modal Clipped on Short Viewports (#964) — On short or zoomed-in browser windows, the Add Printer and Edit Printer dialogs exceeded the viewport height with no scroll, hiding the lower fields (Access Code, Model, Location) and the Save button. Users had to zoom the browser out to complete the form. The modal overlay now scrolls and the card caps at calc(100vh - 2rem) with internal overflow so every field stays reachable regardless of viewport height. Thanks to MartinNYHC for reporting.
  • AMS Drying Silently Does Nothing (#971) — Clicking Start Drying on a supported printer (e.g. P1S with AMS 2 Pro) could publish the MQTT command successfully but leave the AMS idle with no UI feedback. Two issues: (1) the firmware rejects the command when dry_sf_reason reports a blocking state (most commonly code 8 — AMS 2 Pro external power adapter not plugged in — but also "AMS busy", "already drying", etc.), and Bambuddy parsed that array but never surfaced it to the user; (2) the payload sent filament: "", which some firmwares treat as an invalid-field refusal. The /drying/start endpoint now inspects the live dry_sf_reason for the target AMS unit and returns a descriptive 409 (e.g. "Plug in the external AMS power adapter to start drying") instead of silently publishing, and backfills an empty filament from the first loaded tray's type (defaulting to PLA) so the printer never rejects the command for a missing field. Thanks to MartinNYHC for reporting.
  • Webhook Tokens Leaked into Logs When Debug Logging Enabled (Security) — Turning on Settings → Support → Debug Logging elevated the httpx and httpcore loggers to DEBUG, which caused httpx to log the full URL of every outbound HTTP request. For Discord notifications and generic webhook notifications, the URL is the secret — the bearer token is embedded in the path — so any user who enabled debug logging (typically to capture logs for a bug report) was writing their Discord webhook token to bambuddy.log and then pasting it into GitHub issues or support bundles. httpx/httpcore are now pinned to WARNING regardless of the debug toggle; paho.mqtt still honours debug. If you enabled debug logging while notifications were sending, rotate any exposed Discord/webhook URLs — the token is in the path, so the whole URL must be regenerated in the provider's UI.
  • Queue Item Stuck in "Printing" When Start Command is Dropped (#967) — If the physical printer dropped or ignored the MQTT project_file start command (same half-broken-session shape as #887/#936), the queue item was permanently orphaned in the printing status at 100% because the scheduler optimistically flipped the DB row to printing right after the publish succeeded locally and had no watchdog to revert it. Recovery required manually editing the SQLite print_queue table. A new watchdog now captures the printer's pre-dispatch state and polls for up to 45 s after start_print() returns; if the printer never transitions, the item is reverted to pending so the scheduler picks it up again, and the MQTT session is force-reconnected so the retry lands without a printer reboot. Thanks to stringham for reporting.
  • Queued Prints Require Printer Reboot to Start (#936) — On some printers, a queued print would be uploaded via FTP and the project_file MQTT command would be sent, but the printer never transitioned out of FINISH/IDLE and required a power cycle to unstick — after which it often started a previously cancelled print rather than the intended one. Root cause is a half-broken MQTT session (same shape as #887): the printer keeps publishing telemetry so Bambuddy reports it as connected, but our publishes on the command topic never reach the firmware. Existing recovery only triggered via the developer-mode probe path, which skips printers that already have a known developer_mode value. The print-dispatch verifier now treats an unacknowledged project_file (state unchanged after 15 s) as the same "commands not reaching printer" signal and forces a fresh MQTT session so the next dispatch can land without a printer reboot. The existing dev-mode probe path is refactored to share the same helper.
  • Clear Plate Confirmation Bypassed on Power Cycle (#961) — With Auto Off enabled and another job queued, the smart plug would cut power when a print finished and immediately re-power when the scheduler saw the queue, at which point the printer booted fresh into IDLE and the next job auto-dispatched without the "Clear Plate & Start Next" confirmation. Root cause: the plate-cleared gate lived only in the in-memory PrinterManager._plate_cleared set, and the scheduler's idle check treated IDLE as always-idle regardless of whether a previous finish had been acknowledged — so the gate was lost across both Bambuddy restarts and the IDLE-on-boot state transition. The gate is now an awaiting_plate_clear column on the printers table, set by on_print_complete when a print finishes or fails, cleared by the /printers/{id}/clear-plate endpoint and by the scheduler when it dispatches the next job, and rehydrated from the DB into PrinterManager on startup. _is_printer_idle now short-circuits to not-idle whenever require_plate_clear is on and the printer is awaiting ack, regardless of the currently reported state — so the prompt survives Auto Off cycles, Bambuddy restarts, and the printer booting back into IDLE. The clear-plate endpoint no longer requires the printer to currently report FINISH/FAILED (it accepts the ack whenever the awaiting flag is set), and the Printers page widget prompts based on the flag rather than the reported state. Thanks to miaopas for reporting.
  • Insecure Temp File Creation in Backup Export — The manual backup download endpoint used tempfile.mktemp(), which is vulnerable to a symlink race condition (CWE-377). Replaced with tempfile.mkstemp() which atomically creates the file, eliminating the TOCTOU window.
  • Spoolman Iframe Blocked After 0.2.3b4 Security Headers — The Spoolman page (Inventory → Spoolman iframe) failed to load when Spoolman was served from the same host as Bambuddy via a reverse proxy. The security-headers middleware added in 0.2.3b4 set X-Frame-Options: DENY on every response, which blocked even same-origin iframing. Relaxed to SAMEORIGIN so Spoolman (and any other same-origin tool behind the same reverse proxy) can be embedded again, while still preventing cross-origin clickjacking.
  • Large 3MF Print Restart Mid-Job Kept Duplicate Archive With Wrong Duration (#972) — Second wave of #972 reports — a reproducer on a 37.5 MB BambuStudio-pushed print to an A1 surfaced three distinct problems that compounded across a Bambuddy container restart mid-print. (1) Archive start_time lost: the print-start handler only deduped existing printing archives by filename and marked them cancelled once older than 4 h — so a 13 h print that had a restart 10 h in got its archive cancelled, a brand-new archive created with started_at = now(), and the final duration displayed as ~1.5 h for a job that actually ran 13 h. Fixed by persisting the MQTT-provided subtask_id on every archive row (new subtask_id column, auto-added via the existing inline migration runner) and matching on that id first, regardless of age. Same id means same print; the row is resumed in place with its original started_at. Also revives Stale-cancelled rows from the legacy path if an earlier Bambuddy version already ran the old cancel-then-recreate logic. (2) 3MF search retried non-existent paths for ~48 min: the path order was /cache/ → /model/ → /data/ → /data/Metadata/ → /, and every missing path burned the full retry budget (user had ftp_retry_count = 10 with 30 s delay ⇒ 11 × 30 s × 4 missing paths ≈ 22 min before the real / root path was even tried). BambuStudio/OrcaSlicer actually push to / on A1-family printers, so the "most likely" path was tested last. Fixed by reordering to try / first, and by raising a new FileNotOnPrinterError sentinel from download_to_file when the FTP response is a 550 (file not found) so with_ftp_retry's non_retry_exceptions short-circuits instead of waiting out the full delay ×11 retries against a path that will never have the file. Transient errors (425 "can't open data connection", SSL EOF, connection resets) still retry as before. (3) Same 36 MB downloaded twice — the cover-thumbnail endpoint and the archive-metadata handler each opened their own FTP session for the same file during the print, and the second session often hit 425 because the first was still using the printer's single FTP socket. Added a small in-memory _threemf_path_cache keyed on (printer_id, normalized filename): whichever flow fetches the 3MF first populates the cache, the other flow reuses the file read-only, and on_print_complete evicts the entry + deletes the temp file. Normalization collapses Broly_X, Broly_X.3mf, Broly_X.gcode.3mf, Broly X, and case variants to the same slot so both flows agree on the key. Net effect for the reproducer: what took ~48 min with a lost start time now takes seconds and the archive keeps its original row + timestamps. Thanks to mstko for the reproducer and support bundles.
  • Large 3MF Files Silently Dropped After Print Finish (#972) — After large prints, the Files tab rows arrived with no thumbnail, no filament breakdown and no cost — the archive row got created as a fallback with no 3MF even when the file was sittable on disk. Two root causes in the 3MF-fetch path. (1) The configured ftp_timeout setting (default 30 s, reporter had raised it to 300 s) was only plumbed through as the FTP socket timeout; the outer asyncio.wait_for wrapping run_in_executor was stuck on the hardcoded 60 s default, so the user's 300 s value never applied — every 3MF download was capped at 60 s regardless. (2) asyncio.wait_for cannot cancel run_in_executor threads: when the 60 s outer timeout fired, the executor thread kept running ftplib.retrbinary and frequently completed the download successfully ~30–60 s later — logging "Successfully downloaded … N bytes" and caching the working FTP mode — but by then the async wrapper had already returned False, so the retry loop kept re-attempting the same path, each attempt truncating the file the zombie thread had just written. After all 4 attempts the wrapper reported failed after 4 attempts and the archive was persisted as a fallback (no 3MF, empty file_path). The async wrapper now (a) accepts and uses timeout at each call site so ftp_timeout controls both the asyncio deadline and the socket deadline, and (b) salvages a post-timeout success: when the executor thread has set an explicit completion flag and the file is on disk, the wrapper returns True instead of discarding the result. Also fixes a cosmetic // prefix in the directory-search download path (posixpath.join replaces string concatenation that produced "//file.3mf" when the search dir was "/"). Thanks to MartinNYHC for the report and PurseChicken for the P1S support bundle.
  • SD Card Badge Removed — After four rounds of fixes the printer-card SD status badge still flipped red on H2D when unrelated activity happened on the network (e.g. powering on an A1 caused every H2D to go red simultaneously). The underlying problem is that Bambu firmware SD-state signaling is not reliably derivable from MQTT: the legacy top-level sdcard field is only sent on some pushes with inconsistent typing, and home_flag bits 8-9 are cleared on heartbeat pushes even when a card is inserted, with no reliable way to distinguish heartbeats from full status reports. The badge has been removed entirely from the Printers page card and the Printer Info modal. Underlying state.sdcard parsing is retained (simplified to a plain truthy read of the sdcard field only, no more home_flag derivation, no heartbeat latches) because the firmware-update precondition check still needs to know whether a card is inserted before starting an update. Thanks to MartinNYHC for the extensive reporting across all four rounds. Previously, this entry described the H2D badge flap and its three attempted fixes — kept here for history: The original bug toggled between "inserted" (green) and "not inserted" (red) every few seconds on H2D. Root cause: the MQTT parser used a strict identity check (data["sdcard"] is True) on the top-level sdcard field, but real firmware ships that field inconsistently — bool on some models, int 1, or a string enum like "HAS_SDCARD_NORMAL" on others — so any message carrying a non-bool value flipped the state to False. Fixed by deriving the badge from home_flag bits 8–9 (HAS_SDCARD_NORMAL / HAS_SDCARD_ABNORMAL) when present — the canonical firmware source, same as door and store-to-SD parsing — and falling back to a truthy check on the top-level field for firmwares that only send that. Follow-up: the badge was still flapping because Bambu firmwares send partial MQTT pushes that carry the legacy sdcard field alone (without home_flag), and the fallback was re-engaging on every such push. The parser now latches home_flag as the canonical source for the session once seen, so partial pushes carrying only sdcard can no longer flip the badge; the latch resets on reconnect so a firmware change still re-learns. Second follow-up: on H2D the badge still showed red on initial Printers-page navigation and flipped to green on reload, because H2D also sends heartbeat-style home_flag pushes where bits 8–9 are clear even when a card is inserted. Downgrades from true→false now require three consecutive clear reads (upgrades false→true still apply immediately), so a single heartbeat no longer turns the badge red. Third follow-up: the three-strike counter still lost the race on idle printers — once an A1 or other printer connecting nearby triggered a burst of MQTT activity, idle H2Ds could accumulate ≥3 heartbeat pushes before the next full status report and all flip to red simultaneously. Reworked the derivation: the legacy top-level sdcard field is now authoritative when present (truthy check covers bool/int/string firmware variants), home_flag bits 8–9 are only consulted on full push_status reports (identified by the presence of multiple state markers like gcode_state, mc_percent, nozzle_temper, print_type, stg_cur, or ams), and bare heartbeat pushes carrying home_flag alone no longer affect SD state at all. Thanks to MartinNYHC for reporting.
  • CSP Blocked Sidebar Iframes, Service-Worker Registration, and Google Fonts — The strict Content-Security-Policy header added in 0.2.3b4 broke three things at once: (1) custom sidebar links pointing at external HTTPS URLs (e.g. a Grafana/telemetry dashboard) rendered in ExternalLinkPage were blocked because no frame-src was declared and iframes fell back to default-src 'self'; (2) the inline service-worker registration <script> at the bottom of index.html was blocked by script-src 'self', silently preventing the PWA service worker from installing; (3) the import of Google Fonts' Inter from index.css was blocked by style-src and font-src. Fixed by adding frame-src 'self' https: for user-configured HTTPS iframe targets, moving the inline SW-registration script into /sw-register.js so script-src 'self' covers it without needing 'unsafe-inline' or per-build hashes, and allowing https://fonts.googleapis.com in style-src and https://fonts.gstatic.com in font-src. frame-ancestors 'none' is preserved so Bambuddy itself still cannot be framed cross-origin.

Don't miss a new bambuddy release

NewReleases is sending notifications on new releases.