2212 Commits
Author SHA1 Message Date
MartinNYHC 361041d0c4 Merge pull request #1029 from maziggy/0.2.3
**Bambuddy v0.2.3**

## ⚠ Upgrade Notes — Read Before Updating

The in-app **Update** button does not reliably perform the one-time migration from 0.2.2.x to 0.2.3. Please do this one upgrade from the command line using one of the paths below. Once you're on 0.2.3, the in-app Update button works normally again for all future releases. Full guide: [UPDATING.md](https://github.com/maziggy/bambuddy/blob/main/UPDATING.md).

**Docker**

First make sure your `docker-compose.yml` `image:` line points at `:latest` or `:0.2.3` — if it pins an older tag (e.g. `:0.2.2.2`), `docker compose pull` will just re-fetch that tag. If your compose file is older than 0.2.3, also refresh it from the repo; recent releases added `cap_add: NET_BIND_SERVICE`, extra virtual-printer ports for bridge mode, and an optional PostgreSQL block.

```bash
docker compose pull
docker compose up -d
```

**Native install (`install.sh` or manual `git clone`)**

The bundled `update.sh` now stops the service, snapshots the database via the built-in backup API, fast-forwards to `origin/main`, installs Python dependencies, rebuilds the frontend, restarts the service, and rolls back automatically if any step fails.

```bash
sudo /opt/bambuddy/install/update.sh
```

**Installed from a GitHub ZIP/tarball?** Those installs have no `.git` directory and cannot be upgraded in place. See the [UPDATING.md recovery procedure](https://github.com/maziggy/bambuddy/blob/main/UPDATING.md#installed-from-a-github-zip-or-tarball-download) for backup-and-reinstall steps.

**Take a backup first.** `update.sh` takes one automatically; Docker and fully-manual paths do not. Settings → Backup → **Create Backup** produces a full ZIP.

---

**Highlights**

0.2.3 is the largest Bambuddy release yet — the cumulative result of four beta cycles (b1 → b4) and post-b3 work on `dev`. Big-ticket additions since 0.2.2.2: **Two-Factor Authentication and OIDC/SSO**, **LDAP / Active Directory**, **Obico AI print-failure detection**, **optional PostgreSQL database support**, **scheduled local backups**, **X2D printer support**, and the first official **SpoolBuddy** beta — a 7" touchscreen kiosk with NFC tag writing and scale-based spool tracking. The core app gains shortest-job-first queue scheduling, auto-print G-code injection for bed-clearing systems, printer search and filters, direct printing from the project view, firmware rollback, and a lot of polish.

**New Features**

- **Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO (#933)** — Full 2FA implementation with admin UI. Supports TOTP authenticator apps and email-delivered OTP. OIDC/SSO works alongside local accounts and LDAP. Contributed by @netscout2001.
- **Obico AI Print-Failure Detection (#172)** — Self-hosted Obico ML API integration for spaghetti/failure detection across your whole printer fleet. Captures snapshots locally and serves them via nonce URLs so the ML API can reach them without exposing auth tokens.
- **X2D Printer Support (#988, #989)** — Full support for the Bambu Lab X2D including dual-nozzle, camera, K-profile, and maintenance tracking. Seeded by @legend813's #989 (rod-type classification and registry scaffolding) with dual-nozzle / K-profile / `is_h2d` gaps filled in on top. Thanks to @krautech for the report and debug bundle.
- **Firmware List with Rollback (#568)** — Settings → Firmware now lists every announced firmware version with a usable / unavailable indicator and supports rolling back to an older version.
- **Build-Plate Z-Jog Control (#791)** — Jog the build plate directly from the printer card.
- **Airduct Mode + Status Badges on Printer Card** — Airduct mode control and live status badges on each printer card, with a force-refresh button.
- **Collapsible Folders for Printer Filters (#968)** — Collapse / expand folder groups in the printers sidebar. Contributed by @cadtoolbox.
- **China Region for Cloud Token-Based Login (#1013)** — Token-based login now supports Bambu's China region. Contributed by @Minidoracat.
- **Traditional Chinese (zh-TW) Locale (#1017)** — Full zh-TW translation and a sync of 74 missing keys into zh-CN. Contributed by @Minidoracat.
- **Spoolman Link Modal Shows Vendor Name (#958)** — Vendor name displayed alongside filament in the link modal. Contributed by @shrunbr.
- **Auto-Link Existing Accounts Toggle for OIDC (#973)** — Optional setting to auto-link an OIDC login to an existing local account by matching email address. Contributed by @netscout2001.
- **SpoolBuddy Device Control Buttons in Settings Card** — Restart daemon, restart browser, reboot, and shutdown controls for each registered SpoolBuddy device from the Settings card.
- **Expanded Settings Search** — Module-level registry makes Settings search cover every sub-page, not just the top-level nodes.
- **Support Bundle Includes Settings + SpoolBuddy Devices** — Support bundles now contain all settings (with sensitive values redacted) and the list of registered SpoolBuddy devices, so bug reports have more context out of the box.
- **Scheduled Local Backups (#884)** — Settings → Backup now includes a "Scheduled Backups" card that automatically creates complete backup snapshots (database + all data directories) on an hourly, daily, or weekly schedule with configurable time-of-day and retention count. Backups are written as ZIP files to a configurable output directory (defaults to DATA_DIR/backups/), which Docker users can mount as a volume to their NAS or external storage. Each backup in the list can be downloaded, restored directly from the UI, or deleted individually. The manual backup download endpoint has also been optimized to stream directly from disk instead of loading the entire ZIP into memory. Works with both SQLite and PostgreSQL. Fully localized across all 7 UI languages.
- **Optional PostgreSQL Database Support** — Bambuddy can now use an external PostgreSQL database instead of the built-in SQLite. Set the DATABASE_URL environment variable to connect to Postgres. SQLite remains the default. All features work with both backends including full-text archive search, backup/restore, health diagnostics, and cross-database restore (import a SQLite backup into PostgreSQL with automatic type conversion and FK handling).
- **LDAP Authentication (#794)** — Users can now authenticate against an LDAP / Active Directory server. Configure the LDAP server URL, bind DN, search base, and user filter in Settings → Authentication → LDAP. Supports StartTLS, LDAPS (SSL), and plaintext connections. LDAP groups can be mapped to BamBuddy groups (Administrators, Operators, Viewers) for automatic role assignment. Auto-provisioning creates BamBuddy accounts on first LDAP login. Local admin accounts remain as fallback when the LDAP server is unreachable.
- **LDAP Default Fallback Group** — Settings → Authentication → LDAP → Advanced now has a "Default group" selector. When an LDAP user authenticates but is not listed in any mapped LDAP group, they are automatically assigned to this fallback group instead of being left without permissions.
- **Shortest Job First Queue Scheduling (#879)** — New SJF toggle badge on the queue page header. When enabled, the scheduler starts shorter print jobs before longer ones instead of FIFO order. A starvation guard ensures long jobs that get skipped once are protected from being skipped again. Print duration is cached on queue items at creation time from the 3MF metadata.
- **Auto-Print G-code Injection (#422)** — Configure custom start and end G-code snippets per printer model in Settings (Workflow tab) for bed-clearing systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. When adding a print to the queue, enable "Inject G-code" to have the scheduler inject the configured snippets into the 3MF before uploading. The original file is never modified — injection creates a temporary copy for upload only.
- **Print Files Directly from Project View (#930)** — The project detail page now lists printable files from every linked library folder inline, with Print Now and Add to Queue action buttons on each sliced file. Prints triggered from the project view are automatically associated with the originating project. Contributed by @legend813.
- **Printers Page Search and Filters (#852)** — The Printers page now has a live search bar and two filter dropdowns (status and location). Search matches printer name, model, location, and serial number. The status filter is reactive to WebSocket status updates. Contributed by @legend813.
- **Queue Timeline View (#823)** — Production schedule view showing estimated print completion times, grouped by hour, with live progress bars. Filter by All/Printing/Queued and navigate between days.
- **Staggered Batch Start for Multi-Printer Jobs (#752)** — Stagger print starts across multiple printers to avoid simultaneous bed heating power spikes. Configure group size and interval in the Print/Schedule dialog or set defaults in Settings → Queue.
- **Plate-Clear Confirmation Setting (#752)** — New toggle in Settings → Queue to skip plate-clear confirmation for farm workflows where plates are verified physically.
- **Per-User Statistics Filtering (#730)** — Admins can filter the Statistics page by user to see individual prints, filament usage, and costs.
- **Bulk Printer Actions (#825)** — Select multiple printer cards and apply bulk Stop, Pause, Resume, Clear Notifications, or Clear Bed. Select by state or location.
- **Prefer Lowest Remaining Filament (#805)** — Optional setting to prefer AMS spools with the least remaining filament during auto-matching, helping consume partial spools first.
- **REST/Webhook Smart Plug Type (#472)** — New generic HTTP smart plug type for openHAB, ioBroker, FHEM, Node-RED, etc. Configure ON/OFF URLs, methods, headers, and optional status polling.
- **Configurable Default Print Options (#858)** — Set default print options (bed levelling, flow calibration, vibration calibration, timelapse, etc.) in Settings → Workflow.
- **Batch Print Quantity (#342)** — Print multiple copies of a file in one step with a quantity field in the Print/Schedule dialog.
- **GitHub Backup: Spool Inventory & Print Archives (#870)** — Optional backup of spool inventory and print archive metadata to GitHub.
- **External Folder Subfolder Preservation (#890)** — Scanning an external folder now mirrors the real directory structure into the file manager folder tree instead of flattening all files into the root.
- **SpoolBuddy Quick Menu (#893)** — Swipe down from the top of the SpoolBuddy display to open a quick-access control panel. Toggle printer power via smart plugs directly from the display, and manage the system with restart daemon, restart browser, reboot, and shutdown controls.
- **SpoolBuddy Device Management Tab** — Settings → SpoolBuddy now lists every registered device with live connection status, system details, hardware health flags, and an Unregister button. A yellow warning banner flags likely crash-duplicates when more than one device is registered.

**Improved**

- **Default Plate-Clear Confirmation Off on Fresh Installs** — Fresh installs default the Plate-Clear Confirmation setting to off, matching the expectation that farm operators confirm plates physically. Existing installs keep their current preference.
- **i18n Parity Gate Extended to All Locales** — The CI gate that checks translation drift now inspects every locale file (de, fr, it, ja, pt-BR, zh-CN, zh-TW) with a strict / informational tier split, so translation regressions can't sneak in invisibly.
- **SpoolBuddy Auto-Wake on NFC/Scale (#945)** — The kiosk display now wakes automatically when a spool is placed on the scale or an NFC tag is scanned, without requiring a touch first. Thanks to @TravisWilder.
- **SpoolBuddy Kiosk LCD Now Powers Off on Idle (#937)** — The "screen blank timeout" setting now actually powers off the HDMI panel's backlight via swayidle + wlopm instead of just painting a CSS overlay. Thanks to @TravisWilder.
- **AMS Drying Support for P2S** — Remote AMS drying and queue auto-drying now work on P2S printers with firmware 01.02.00.00 or later.
- **AMS Drying Support for H2S (#886)** — Remote AMS drying and queue auto-drying now work on H2S printers with firmware 01.02.00.00 or later.
- **Assign Spool Modal Filtering (#889)** — Improved filtering logic for faster spool selection. Contributed by @Keybored02.
- **SpoolBuddy Spool Detail Card (#866)** — Improved spool detail card UI with better layout and information density. Contributed by @Keybored02.
- **REST Smart Plug: Separate Power/Energy URLs (#472)** — REST smart plugs can now use individual URLs for power and energy data with unit multipliers for conversion.
- **Standardized Webhook Notification Payloads (#871)** — Webhooks now include structured event data fields alongside existing title/message fields for easier automation.
- **Database Engine Info on System Page** — Shows the active database engine (SQLite or PostgreSQL) and its version.
- **Plate Number in Printer View (#881)** — Printer cards now show the plate number alongside the filename for multi-plate 3MF prints.
- **Printer Name in Queue for Model-Based Jobs (#881)** — Queue items assigned to a printer type now show the actual printer name once the scheduler assigns one.
- **Developer Mode Detection for A1/P1 Printers** — Printers without the fun MQTT field now have developer mode detected via a probe command on reconnect.
- **Queue Page Visual Refresh** — Compact stats bar, color-coded left borders, collapsible history section, and condensed history rows.
- **SpoolBuddy Kiosk Performance Optimizations** — Reduced idle CPU from ~3.3 to ~0.9 on Raspberry Pi.
- **SpoolBuddy Inventory Page** — New kiosk page with spool grid, search, filter pills, and tap-to-detail view.
- **SpoolBuddy Auto-Navigate on Tag Scan** — Automatically navigates to dashboard and wakes screen when a tag is scanned.
- **SpoolBuddy Swipe to Switch Printers** — Left/right swipe cycles through online printers on the touchscreen.
- **Settings Menu Layout** — Improved settings page menu organization.

**Fixed**

- Virtual Printer Dropping Null-Terminated MQTT Payloads from OrcaSlicer Linux (#927) — OrcaSlicer on Linux appends `\0` to MQTT payloads; the parser silently dropped them. Fixed.
- OIDC Callback Code/State Too Short (#1024) — Raised `code` and `state` max length from 512 to 2048 to match provider payload sizes. Contributed by @netscout2001.
- OIDC Issuer Trailing Slash Mismatch (#995) — Normalised trailing slash on both sides of issuer comparison so Authentik logins succeed. Contributed by @netscout2001.
- OIDC Discovery URL Trailing Slash (#985) — Strip trailing slash from issuer URL before building the discovery URL. Contributed by @netscout2001.
- Archive Reprints Colliding With Originals (#1011) — Unique per-submission IDs prevent reprints from overwriting the original archive entry.
- Obico /p/ Endpoint Incompatibility — Reverted POST-bytes approach; Obico `/p/` is GET-only.
- Obico Snapshot 401 / "Failed to get image" (#172) — The ML API couldn't fetch snapshots from Bambuddy when auth was enabled. Snapshots are now captured locally and served via nonce URLs so the ML API can reach them without a token.
- Obico Snapshot Capture PIDs Swept by Stream Cleanup (#172) — Stream cleanup no longer kills in-flight Obico snapshot captures.
- Obico POST Image Bytes to ML API (#1003) — Adjusted the ML API integration to POST image bytes directly instead of a callback URL.
- MQTT Zombie Session Detection (#887) — Detects dead-but-reconnected MQTT sessions via `ams_filament_setting` response tracking and force-closes them with probe-timeout retries.
- MQTT Self-Heal on Dispatch Timeout (#936) — Half-broken MQTT sessions self-heal instead of hanging indefinitely.
- SD Card Badge Flap on H2D — Multiple fixes: partial pushes no longer flap the badge, heartbeat bursts no longer flip it red, the badge is now hidden entirely when the printer is offline, and the overall SD badge behaviour was reverted to match pre-regression behaviour.
- SD Card and Door Badges Hidden When Printer Offline — Badges are no longer shown on offline printer cards.
- X2D Missing from Add/Edit Printer Dropdowns (#988) — The Add/Edit Printer modal now lists X2D.
- Speed Level Missing from WebSocket Status (#993) — `speed_level` is now forwarded in the websocket status payload so the printer card updates correctly.
- Large 3MF Metadata Lost on FTP Timeout (#972) — Metadata for large 3MF files is now recovered after an FTP timeout instead of being dropped.
- Archive Resume on Subtask ID / Short-Circuit 550 / Cache 3MF (#972) — Archive downloads now resume on `subtask_id`, short-circuit when the FTP server returns 550, and cache the 3MF for reuse.
- Add/Edit Printer Modal Not Scrollable on Short Viewports — Modal is now scrollable when the viewport is too short.
- Library Prints Not Attributed to User — Prints started from the library are now attributed to the authenticated user instead of the service account.
- Build-Plate Gate Bypassed by Auto Off Power Cycle (#961) — Plate-clear gate is now persisted so Auto Off power cycles can't bypass the queue confirmation.
- Stuck Queue Items on Ignored Start Command — The scheduler now reverts a queued item to `pending` when the printer ignores the start command.
- CSP Blocking Sidebar iFrames, Service Worker, and Google Fonts — Relaxed Content-Security-Policy rules so same-origin iframes, the service worker, and Google Fonts load correctly.
- X-Frame-Options Blocking Same-Origin iFrames — Relaxed to `SAMEORIGIN` so same-origin iframes load.
- New-Window Camera View Broken with Auth Enabled (#979) — The camera-in-new-window link now works when authentication is enabled.
- SpoolBuddy Kiosk Unusable on First Boot — Full-mode install now makes the kiosk immediately usable on first boot.
- SpoolBuddy API Key Not Auto-Provisioned — Full-mode install now auto-provisions the kiosk API key.
- AMS `dry_sf_reason` Not Surfaced / Filament Not Backfilled (#971) — Expose dryer state reasons and backfill filament assignments on AMS state changes.
- Toast Callback Fires After Unmount — `setToasts` is now guarded against post-unmount async callbacks.
- Backup File Name — Minor fix to the downloaded backup filename format.
- H2C Nozzle Rack Slot Numbering Off When Slot 1's Nozzle Is Mounted (#943) — Rack slots shifted by one position when slot 1's nozzle was picked up into a hotend. The rack base is now hardcoded to match the fixed H2C rack ID layout. Thanks to @netscout2001.
- Energy Snapshot Capture Crashes on PostgreSQL — The hourly energy snapshot loop failed on PostgreSQL because the tz-stripping hook didn't handle nested parameters from insertmanyvalues. Now recursively strips tzinfo at any depth.
- Wrong Filament Color Name on AMS Popup (#857) — Colors outside a hardcoded list showed wrong names. Rewrote the resolver to use the color_catalog table as the single source of truth. Thanks to @lightmaster.
- LDAP Auto-Provisioning Fails on Upgraded SQLite Installs (#794) — First LDAP login on an upgraded SQLite install hit a NOT NULL constraint. Migration now patches sqlite_master directly. Thanks to @DylanBrass.
- Energy Statistics Empty for Date Ranges in Total Consumption Mode (#941) — Added persisted energy start column and hourly snapshot loop for accurate date-range totals. Per-print energy tracking is now restart-resilient. Thanks to @TheMadMike23.
- Virtual Printer "Synchronizing device information" Times Out in Orca (#927) — MQTT command handling silently dropped requests when the slicer's cached serial didn't match. Both directions are now serial-adaptive.
- External Sidebar Link Icon Not Showing (#878) — Custom icons returned 401 because the sidebar img tag didn't use a stream token.
- SJF Toggle Disappears After Clicking (#879) — The toggle was inside the Pending section header which unmounted when the last pending item started. Moved to the page header.
- Project Breadcrumb Shows i18n Key (#931) — Breadcrumb showed the raw translation key instead of translated text. Contributed by @legend813.
- SpoolBuddy Update Fails in Docker — Multiple fixes for ssh failures under arbitrary UIDs. Entire update path is now subprocess-free (uses cryptography + asyncssh). Docker image now bakes .git/HEAD for correct branch detection.
- Camera Stream Reconnect Counter Off-by-One + ffmpeg Log Flood (#925) — Counter could show "6 of 5", and failed ffmpeg spawns logged the full banner.
- LDAP POSIX Primary Group Ignored — Users whose role came from their POSIX primary group landed without permissions.
- Support Bundle Leaks Virtual Printer IP Address — Added `_ip` to the sensitive key filter for redaction.
- "Build Plate Cleared" Button Unclickable After Second Print (#912) — React Query mutation state from the first confirmation persisted, blocking subsequent clicks.
- Spoolman Location Not Cleared on Spool Removal from AMS (#921) — Auto-sync set locations for new spools but never cleared stale ones, causing double-booked slots.
- Spool Weight Not Updated After Print (#839) — Filament usage tracking failed silently in five scenarios: fallback archives without 3MF, external/VT tray spools, notifications showing "Unknown", auto-archive disabled, and queue/reprint prints with auto-archive disabled. All five paths are now fixed.
- Ghost Jobs From SQLite Lock on Print Completion (#897) — Queue status update could fail silently on SQLite lock, leaving jobs permanently stuck in "printing". Now retries with backoff.
- Multi-Plug Automation Only Working for First Plug (#903) — When multiple smart plugs were assigned to a printer, only the first was automated. All automation paths now control every assigned plug.
- SpoolBuddy Inventory Not Updating on Spool Changes (#905) — Spool CRUD endpoints now broadcast websocket events for instant updates.
- AMS Slot Changes Fail Until Reconnect (#887) — After a keep-alive timeout, paho-mqtt auto-reconnects but silently ignores commands. Added probe timeout with retry and force-close.
- Spool Manager Deducts Double Filament (#880) — Two independent deduction paths ran in the same event loop cycle. AMS weight sync now skips updates while a print session is active.
- File Manager Stale UI After Deleting Folders/Files — Delete endpoints relied on auto-commit after response. Added explicit commit before returning.
- Thumbnails Broken After Backend Restart — Stream tokens lost on restart. Frontend now auto-refreshes failed token-protected image loads.
- SpoolBuddy Kiosk Screen Blanks on Boot — Added consoleblank=0 to kernel cmdline and immediate anti-blank loop.
- Queue Widget Ignores Plate-Clear Setting (#752) — Button showed even when plate-clear confirmation was disabled.
- WebSocket Crash on Printers Without fun Field (#873) — Race condition in developer mode probe caused repeating crashes on A1, P1, X1Plus.
- Filament Hover Card Behind Sidebar (#900) — Hover card z-index conflicted with mobile sidebar.
- Docker Install Script (#915) — Removed deprecated `--bind` flag from `docker_install.sh`.
- Bed Cooled Notification Never Firing (#872) — Replaced polling-based monitor with event-driven approach.
- Filament Color and Subtype Inconsistencies (#857) — Fixed generic color names, missing Silk+/Tough+ subtypes, and misclassified gradient/dual-color filaments.
- External Spool Print Fails on Printers With AMS (#854, #859) — Fixed use_ams flag and ams_id mapping.
- Wrong Filament Mapping (#851) — Contributed by @behrinml.
- External Folder Scan 500 on 3MF Files (#846) — Crash from raw thumbnail bytes in JSON serialization.
- Archives Capped at 50 Items (#843) — Removed hardcoded limit, added pagination.
- Filament Usage Not Recorded When Auto-Archive Disabled — Tracking now runs before the archive check.
- AMS History Cleanup Crash — Fixed naive/aware datetime mismatch.
- SpoolBuddy NFC Write Fails on NTAG Tags — Multiple PN5180 state machine and CRC fixes for NTAG 213/215/216.
- SpoolBuddy Scale First Reading Always Wrong — NAU7802 ADC stale first reading polluted the moving average.
- Print Fails on Files With Spaces in Name (#824) — Spaces in MQTT url field caused firmware to ignore the print command.
- Virtual Printer Proxy A1 Printing Fails (#757) — Ports 2024-2026 weren't proxied.
- H2D External Spool Print Fails (#797) — "Failed to get AMS mapping table" on H2D external spool prints.
- Spool Assignment on Empty AMS Slots (#784) — Assigning spools to truly empty slots created a stuck state.
- Log Flood: "State is FINISH but completion NOT triggered" (#790) — Diagnostic message fired on every MQTT update in terminal state.
- ffmpeg Process Leak Causing Memory Growth (#776) — Camera processes accumulated over time, consuming GB of RAM.
- Database Connection Pool Exhaustion on Large Farms — Increased pool from 30 to 220 connections.
- Sidebar Bottom Icons Cut Off With Smart Plugs (#862) — Fixed footer overflow.
- Native Install Misdetected as Docker in LXC Containers — Fixed detection logic.
- P1S/P1P Printer Card Showing "Printing" When Idle (#813) — Stale MQTT connection not triggering reconnect.
- MQTT Connected/Disconnected UI Bouncing — Stale reconnect timer.
- Rapid MQTT Disconnect/Reconnect Bouncing (#813) — Fixed rapid reconnect cycling.
- SpoolBuddy Various Kiosk Fixes — Virtual keyboard layout, boot splash, settings layout, read tag diagnostics, assign spool modal clipping, low filament warning slot number, status bar updates, dashboard crash on null fields.

**Security**

- Webhook Tokens Leaked in httpx Debug Logs — Debug logging could write webhook tokens to disk. Filtered so secrets don't hit the log.
- Path Traversal in File Upload Endpoints — Archive upload endpoints used client-supplied filenames directly without stripping directory components. All upload endpoints now sanitize filenames. Reported by Sacha Vaudey.
- Unauthenticated Bug Report Endpoints — Bug report endpoints had no authentication. Now require appropriate permissions. Reported by Sacha Vaudey.
- API Key Empty Printer List Grants Full Access — An API key with `[]` was treated as global access. Now `null` = global, `[]` = no access. Reported by Sacha Vaudey.
- Missing HTTP Security Headers — Added X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. Reported by Sacha Vaudey.
- Camera Snapshot Temp Files World-Readable — Switched to mkstemp with 0600 permissions. Reported by Sacha Vaudey.
- Token-Based Auth for Media Endpoints — Camera streams, snapshots, thumbnails, and timelapse videos now require a stream token when auth is enabled.
- Dependency updates — Pillow 12.1.1 → 12.2.0 (CVE-2026-40192), pytest 9.0.2 → 9.0.3 (CVE-2025-71176), python-multipart 0.0.22 → 0.0.26, dompurify 3.3.3 → 3.4.0, vite 7.3.1 → 7.3.2 (#909), plus aiohttp, cryptography, and Pygments CVE fixes.

**Contributors**

Thank you to the contributors who helped make this release possible:

- **@netscout2001** — Two-Factor Authentication + OIDC/SSO implementation, OIDC callback length, OIDC issuer trailing-slash fixes (×2), auto-link existing accounts toggle, H2C nozzle rack slot report (#933, #973, #985, #995, #1024, #943)
- **@legend813** — Printer search filters, project view printing, project breadcrumb i18n fix, X2D seed PR (#852, #920, #930, #931, #932, #989)
- **@Keybored02** — Spool detail card, assign modal filtering, SpoolBuddy init improvements, missing spool notifications, mid-print reassignment (#866, #889, #787, #789, #814)
- **@Minidoracat** — China region for cloud token-based login, traditional Chinese (zh-TW) locale, zh-CN sync (#1013, #1017, #1025)
- **@cadtoolbox** — Collapsible folders for printer filters (#968)
- **@shrunbr** — Vendor name in Spoolman Link Modal (#958)
- **@TheMadMike23** — Energy statistics date-range report (#941)
- **@behrinml** — Filament mapping fix (#851)
- **Sacha Vaudey** — Responsible disclosure of five security vulnerabilities
v0.2.3
2026-04-19 10:06:00 +02:00
maziggy e7672e34ac chore(ci): silence false-positive security findings
- Bandit B108: mark 3 dummy /tmp paths in test fixtures as nosec
  - CodeQL py/ldap-injection: already RFC 4515 escaped via _ldap_escape()
  - CodeQL py/incomplete-url-substring-sanitization: test-only assertions
  - GitGuardian: replace sample passwords with <placeholder> strings in
    notification-template preview data
2026-04-19 10:02:31 +02:00
maziggy 56b83ca020 chore(ci): silence false-positive Bandit B108 + CodeQL LDAP/URL findings 2026-04-19 09:59:09 +02:00
maziggy 10c261dcf2 chore(tests): suppress B108 on dummy /tmp test fixtures 2026-04-19 09:48:10 +02:00
maziggy 61e40f0d09 Merge origin/main — commits already present via dev 2026-04-19 09:44:36 +02:00
maziggy ed17728cef Added UPDATING.md 2026-04-19 09:42:25 +02:00
maziggy 71afe35a49 Added new update.sh and update docs 2026-04-19 09:08:30 +02:00
maziggy 8f9eb0d433 chore(i18n): extend parity gate to all locales with strict/info tiers
Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR
  drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a
  STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest
  report informationally until their drift is caught up. ja notably has 27 real
  placeholder bugs worth fixing before promotion to strict.
2026-04-19 08:36:13 +02:00
maziggy 946ebb6307 Bumped version 2026-04-19 08:28:25 +02:00
maziggy 8af2492543 Post work PR #1025 2026-04-19 08:24:23 +02:00
Minidoracat a584e671ec feat(i18n): add zh-TW locale and sync 74 missing keys in zh-CN (#1017) (#1025)
* fix(i18n): sync zh-CN to match en structure

- Add 74 missing keys covering login.resetPassword, printers.firmwareModal
  badges, settings.spoolbuddy device management, settings.tabs.spoolbuddy,
  spoolbuddy.settings system config
- Fix fileManager.uploadFailed placeholder bug (had stray {{count}} copied
  from zipFilesFailed; en value is plain "Upload failed")

Refs #1017

* feat(i18n): add zh-TW locale and enforce 3-way parity

- Add frontend/src/i18n/locales/zh-TW.ts (Traditional Chinese, Taiwan usage)
  with full key set aligned to en.ts
- Register zh-TW in frontend/src/i18n/index.ts: import, resources,
  supportedLngs, availableLanguages
- Add frontend/scripts/check-i18n-parity.mjs: TypeScript Compiler API-based
  3-way gate checking key set equality, placeholder equality, and legacy
  _plural / _one+_other suffix handling across en / zh-CN / zh-TW
- Wire check:i18n into test:run npm script so frontend-tests CI job
  (ci.yml:227) gates future locale drift

Fixes #1017
2026-04-19 08:17:09 +02:00
maziggy bb999c6805 Post work PR #1024 2026-04-19 08:13:17 +02:00
Sn0rrii e958b10f75 fix(oidc): raise callback code/state max_length from 512 to 2048 (#1024)
Facebook and some other OAuth providers issue authorization codes that
exceed 512 characters. Pydantic rejected these with 422 string_too_long.
The OAuth spec defines no maximum code length; 2048 aligns with common
provider limits.

Also adds three integration tests to verify 512-char and 2048-char codes
are accepted while 2049-char codes are correctly rejected.
2026-04-19 08:10:56 +02:00
maziggy 68920f8c62 Fix virtual printer dropping null-terminated MQTT payloads from OrcaSlicer Linux (#927)
OrcaSlicer's Linux BBLNetworkPlugin publishes MQTT payloads with the
  C-string null terminator included in the length, so decoded messages
  arrived as `{…}\x00`. The strict json.loads() raised JSONDecodeError
  and the publish handler silently returned — pushall, get_version, and
  project_file were never answered, and the slicer hit its 60 s sync
  timeout. Print_queue mode only (proxy mode tunnels MQTT). The b069b521
  serial-adaptation fix was correct but ran past this earlier silent
  failure.

  _handle_publish now strips trailing \x00/whitespace before parsing and
  logs the raw payload on any remaining decode failure so future silent
  variants are visible in support bundles.
2026-04-19 08:04:05 +02:00
maziggy 2366a1a0b3 Fixed backup fie name 2026-04-18 19:04:31 +02:00
maziggy 8c4253c5f1 Updated .gitignore 2026-04-18 14:54:01 +02:00
MartinNYHC e21af6d2bc Fix Discord link in README.md 2026-04-18 14:25:40 +02:00
MartinNYHC 86a640f072 Fix duplicate forum link in README
Removed duplicate forum link and adjusted formatting.
2026-04-18 14:24:47 +02:00
MartinNYHC 566f6cc680 Update Discord link in README 2026-04-18 14:23:30 +02:00
maziggy d2ef8834a9 Revert "Updated README"
This reverts commit 9b7a13b4ad.
2026-04-18 14:22:52 +02:00
maziggy 9b7a13b4ad Updated README 2026-04-18 14:21:51 +02:00
maziggy 6cb3457102 Updated README 2026-04-18 14:20:45 +02:00
maziggy b92d4a7445 Post work PR #1013 2026-04-18 12:39:07 +02:00
Minidoracat baf0716a9a feat(cloud): support China region for token-based login (#1013)
feat(cloud): support China region for token-based login

The /cloud/token endpoint always used the global Bambu API endpoint,
so users with China-region access tokens could not validate their
token. The password login flow already exposes a region selector; this
brings the token flow to parity.
2026-04-18 12:30:01 +02:00
maziggy 115d6fe627 fix(mqtt): unique per-submission IDs for archive reprints (#1011)
Archive reprints and library-file prints built the MQTT project_file
  command with hardcoded project_id="0", subtask_id="0", task_id="0".
  Printers key per-job state (including gcode_start_time) on those IDs,
  so reprints looked like continuations of the same job and third-party
  MQTT observers (OctoEverywhere) reported compounding durations across
  repeat replays — a 40 min job reprinted from archive showed ~1h40m,
  and a second reprint of the same file showed ~4h. BambuStudio mints
  fresh IDs per submission; bambu_mqtt.start_print() now does the same
  using an epoch-millisecond timestamp for all three fields. md5 is
  deliberately left empty to avoid activating firmware md5-validation
  against a digest we can't compute without re-reading the upload.

  Added 6 regression tests in TestStartPrintUniqueIdentityFields
  covering non-zero IDs, md5 stays empty, uniqueness across successive
  submissions, numeric-string format, and blast-radius guard on
  unrelated payload fields. Updated CHANGELOG.
2026-04-18 09:09:21 +02:00
maziggy a2c7fd4542 fix(obico): revert POST-bytes approach — Obico /p/ is GET-only
The 0.2.3b4 #1003 "fix" POSTed JPEG bytes as multipart form data,
  but Obico's /p/ endpoint is declared methods=['GET'] upstream and
  reads ?img=URL from the query string. Every POST was 405'd by
  Flask's router before any handler ran, which is why the Obico
  container logs were silent while Bambuddy kept reporting
  "ML API call failed for printer N:" with a blank suffix —
  raise_for_status() on the 405 produced an exception whose str()
  rendered empty.

  Restored the pre-#1003 nonce-URL approach (commit 3e434458):
  capture locally with a 20s timeout we control, stash the JPEG
  under a single-use 32-byte nonce, hand Obico a
  GET /api/v1/obico/cached-frame/{nonce} URL that resolves in
  <50ms so its hardcoded 5s read timeout never races RTSP.

  Also guards against future silent exceptions: the error format
  now falls back to type(exc).__name__ when str(exc) is empty.
  Detection also early-returns with an explicit error if
  external_url is unset instead of handing Obico a URL it can't
  resolve.

  The #1003 reverse-proxy scenario (Authelia/Authentik/CF Access
  in front of Bambuddy) is addressed by documenting that the
  /api/v1/obico/cached-frame/ path must be whitelisted from
  external auth at the proxy layer — it is already public on
  Bambuddy's side.

  Backend: services/obico_detection.py, api/routes/obico.py,
  main.py (PUBLIC_API_PATTERNS).
  Frontend: FailureDetectionSettings banner + client.ts type +
  all 7 locales restored.
  Tests: 15 unit + 5 integration tests pass.
2026-04-18 08:50:46 +02:00
maziggy 464d56ea0d fix(install): make SpoolBuddy kiosk usable on first boot in full-mode install
Full-mode install booted into an unusable kiosk:
  - Chromium opened before uvicorn → "can't connect to localhost"
  - After reload, requires_setup=true hijacked /spoolbuddy → /setup
  - Touch-only Pi has no keyboard to complete the setup wizard
  - Declining auth left the user at / instead of the kiosk

  Fixes, bundled:

  1. backend/app/cli.py kiosk-bootstrap now, in one DB transaction:
     - creates a scoped API key (can_read_status=True, rest false)
     - upserts setup_completed=true
     so AuthContext never redirects and the kiosk URL loads directly. Users
     who want auth can still enable it from the admin UI; the provisioned
     key keeps working.

  2. install.sh full-mode runs the CLI as the bambuddy service user after
     create_bambuddy_service and sed-replaces the CHANGE_ME_AFTER_SETUP
     placeholder in spoolbuddy/.env.

  3. The generated spoolbuddy-kiosk-launch polls ${backend_url}/health for
     up to 60s before exec'ing chromium, so cold boots wait for uvicorn
     instead of flashing ERR_CONNECTION_REFUSED.

  Standalone mode was unaffected — users supply a real key from their
  existing Bambuddy before install.
2026-04-18 08:14:53 +02:00
maziggy 3502ab33c5 fix(install): auto-provision SpoolBuddy kiosk API key in full-mode install
Full-mode install wrote CHANGE_ME_AFTER_SETUP as SPOOLBUDDY_API_KEY because
  no admin exists yet to create a real one. On reboot the kiosk launched with
  that placeholder, AuthContext rejected it, and the user hit the Bambuddy
  login page instead of the kiosk. Standalone mode was unaffected — users
  paste a real key from their existing Bambuddy before install.

  Adds backend/app/cli.py with a kiosk-bootstrap subcommand that creates a
  scoped APIKey row directly in the DB (can_read_status=True, everything else
  false) and prints the full key to stdout. install.sh full-mode runs it as
  the bambuddy service user after create_bambuddy_service, captures the key,
  and sed-replaces the placeholder in spoolbuddy/.env. Idempotent with
  --force for re-installs.

  Drops the outdated "create an API key and edit .env" next-step block since
  the kiosk is now provisioned automatically.
2026-04-18 07:40:38 +02:00
maziggy 8e7a3bf1f8 Updated CHANGELOG 2026-04-17 09:54:56 +02:00
maziggy a95a3c52ee fix(mqtt): detect zombie sessions via ams_filament_setting response tracking (#887)
After hours idle the MQTT connection can degrade so telemetry still
  flows but published commands never reach the printer.  The existing
  dev-mode probe only ran on first connect; this adds tracking for
  user-initiated ams_filament_setting commands — two consecutive
  unanswered commands (10 s timeout each) trigger force_reconnect.
2026-04-17 09:29:46 +02:00
maziggy 475e34ebda fix(obico): POST image bytes directly to ML API instead of callback URL (#1003)
The ML API previously called back into Bambuddy to fetch snapshots,
  which failed behind reverse proxies with external auth (Authelia, etc.).
  Now the detection loop captures the JPEG locally and POSTs it directly
  as multipart form data — no callback URL, no nonce cache, no
  external_url dependency.
2026-04-17 09:06:31 +02:00
maziggy c6aba21278 fix(printer): add X2D to add/edit printer model dropdowns (#988)
Both the Add Printer and Edit Printer modals had hardcoded model lists
  missing the X2D — manual printer setup had no way to select the new
  model. Auto-discovery via SSDP and virtual printer model selection
  (dynamic from backend) were unaffected.
2026-04-17 08:50:49 +02:00
maziggy ef37ffa7c7 fix(obico): exclude snapshot capture PIDs from stream cleanup (#172)
The periodic camera cleanup task scans /proc for ffmpeg processes and
  kills any not in the active-streams registry. The Obico detection
  service's capture_camera_frame_bytes() spawns short-lived ffmpeg for
  snapshots but never registered the PID — so cleanup killed it as
  "orphaned" mid-capture (SIGKILL, exit -9), producing false errors and
  missed detection frames.

  Track capture PIDs in _active_capture_pids and exclude them from the
  cleanup kill list.
2026-04-17 08:38:22 +02:00
maziggy 3e434458a4 fix(obico): capture snapshots locally and serve via nonce URL (#172)
Obico's ML API has a hardcoded 5s read timeout on the URL it fetches, which
  our /camera/snapshot regularly exceeds on cold calls (TLS proxy + ffmpeg +
  RTSP keyframe wait). The detection loop now captures the JPEG locally with
  a 20s timeout we control, stashes the bytes under a single-use 32-byte
  nonce, and hands Obico a new /api/v1/obico/cached-frame/{nonce} URL that
  returns the cached bytes instantly. The 5s ceiling is no longer a factor.

  The nonce is the credential (URL-safe, 256 bits of entropy, single-use,
  30s TTL) so the endpoint can be unauthenticated without widening the
  camera access surface. Replaces the previous camera-stream-token snapshot
  URL approach, which remained vulnerable to the upstream 5s timeout even
  when auth was disabled.

  Thanks to @fblix for the detailed reproducer with timeout numbers.
2026-04-16 11:09:54 +02:00
maziggy 6fb814c5ea feat(printer): add X2D support — camera, dual-nozzle, K-profile, maintenance (#988)
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
  steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
  model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
  these identifiers existed in Bambuddy's registries, so the camera
  service fell back to the chamber-image protocol on port 6000 (X2D
  doesn't speak it), firmware-check logged "Unknown printer model: N6",
  and the dual-nozzle K-profile paths — gated on the H2D serial prefix
  "094" — would have treated X2D as single-nozzle.

  Backend:
  - Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
    PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
    CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
    virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
  - supports_rtsp(): match the X2 display-name prefix and the N6 internal
    code; camera now routes to RTSP on port 322.
  - Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
    kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
    the H2D-style cali_idx in-place edit path.
  - is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
    timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
    are sent as integers and external-spool ams_id 254/255 routing is
    preserved (H2D-style deputy-nozzle addressing).

  X2D uses hardened steel rods like P2S — it is intentionally placed in
  STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
  the classification.

  Frontend:
  - mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
  - Enclosure-door badge and airduct-mode whitelists include X2D.
  - MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
    steel-rod lubrication, belt tension, cold-pull, and PTFE tube
    (exported to enable direct unit testing).

  Tests:
  - test_printer_models.py: TestX2DModel (10 assertions).
  - test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
    TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
  - MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
    regression, X1C/H2D/A1Mini regression, and model-name normalisation.

  Docs:
  - README: added X2 series to the supported printers table.
  - CHANGELOG: new entry under 0.2.3b4 Fixed.

  Credit to @krautech for the report and debug bundle, and to @legend813
  for PR #989 which seeded most of the registry changes — rod-type
  classification was corrected (steel, not carbon) and the dual-nozzle /
  K-profile / is_h2d gaps were added on top.
2026-04-16 10:40:32 +02:00
Sn0rrii 071570f754 fix(oidc): normalise trailing slash on both sides of issuer comparison (#995)
PyJWT compares the iss claim against discovery_issuer with an exact string
match. Authentik (and similar providers) include a trailing slash in the JWT
iss claim while the discovery document issuer may omit it, or vice-versa.

Disable PyJWT built-in issuer validation and compare both sides after
rstrip('/') to make the check slash-agnostic.

Adds a regression test that verifies a login succeeds when the provider is
configured without a trailing slash but the JWT iss claim carries one.
2026-04-16 09:40:50 +02:00
maziggy 46c246c504 fix(archive): resume on subtask_id, short-circuit 550, cache 3mf (#972)
Second wave of #972 — reproducer on a 37.5 MB BambuStudio print to an A1
  showed three stacking root causes when Bambuddy restarts mid-print.

  1. Archive start_time lost on container restart. The name-based dedup
     cancelled any "printing" archive older than 4h and recreated it with
     started_at=now(), so a 13h print that saw a restart 10h in ended up
     showing ~1.5h duration. Persist MQTT subtask_id on every archive and
     match on that first, regardless of age — same id means same print,
     resume in place. Also revives Stale-cancelled rows for users
     upgrading mid-print.

  2. 3MF FTP search tried non-existent paths for ~48 min. Order was
     /cache → /model → /data → /data/Metadata → / with 11×30s retries
     each; BambuStudio actually pushes to / on A1, so the real path was
     tested last. Reorder to / first, and raise a new FileNotOnPrinterError
     sentinel from download_to_file on 550 so with_ftp_retry short-circuits
     via non_retry_exceptions. 425 / SSL EOF / connection resets still
     retry as before.

  3. Cover endpoint and archive flow downloaded the same 36 MB twice and
     competed for the printer's single FTP socket, producing 425 errors
     that fed cause-2's retry storm. Add an in-memory _threemf_path_cache
     keyed on (printer_id, normalized filename); whichever flow fetches
     first populates it, the other reuses the file read-only. Eviction
     runs on on_print_complete and deletes the temp file.

  Backend: 14 new tests across test_bambu_ftp.py and a new
  test_subtask_archive_resume.py. Existing suite: 2737 pass. ruff clean,
  frontend build clean.
2026-04-16 09:36:44 +02:00
maziggy 58d33cdb9c fix(toast): guard setToasts against post-unmount async callbacks
An async handler (e.g. LoginPage's catch-handler on a failed login
  request) can call showToast AFTER Vitest's afterEach has unmounted the
  ToastProvider. The existing unmount effect clears timers it knows about,
  but a showToast scheduled POST-unmount lands a fresh 3s setTimeout that
  the cleanup never saw. The callback then runs against a torn-down jsdom
  — setToasts → React scheduler → accesses window → ReferenceError as an
  uncaught exception in test output ("Vitest caught 1 unhandled error").

  Add an isMountedRef flipped to false in the unmount cleanup; every
  setToasts call (showToast, showPersistentToast, dismissToast, both
  dispatch-toast auto-dismiss timers) short-circuits when the provider is
  gone. The timer callback re-checks the ref as a belt-and-braces guard
  for timers scheduled after cleanup already ran.

  In production this only hardens the code; the provider lives at app
  root and does not unmount during normal navigation.

  Tests: 4 new regression cases in __tests__/contexts/ToastContext.test.tsx
  covering post-unmount showToast / showPersistentToast / dismissToast
  no-oping, and the auto-dismiss timer firing post-unmount without
  throwing. Full suite: 1381 pass, 0 unhandled errors (was 1377 + 1
  uncaught exception). TypeScript check clean.
2026-04-16 09:36:27 +02:00
maziggy 63b3cad8d8 chore(deps): bump python-multipart 0.0.22→0.0.26 and dompurify 3.3.3→3.4.0
python-multipart 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a
  DoS triggered by large preamble/epilogue data around a multipart
  boundary. Bambuddy consumes python-multipart transitively through
  FastAPI/Starlette for form and file-upload parsing, so multipart routes
  (backup restore, project thumbnail upload, etc.) were exposed.

  dompurify 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (function-form
  ADD_TAGS could bypass FORBID_TAGS). Bambuddy's two call sites use only
  array-form ALLOWED_TAGS/ALLOWED_ATTR, so the specific bypass was not
  reachable, but the bump still hardens the sanitizer and clears the
  audit warning.

  requirements.txt floor raised to python-multipart>=0.0.26;
  frontend/package.json caret pinned to ^3.4.0; npm audit and pip audit
  both report zero outstanding advisories after the bumps.
2026-04-16 08:47:40 +02:00
maziggy c73c23b083 fix(printers): forward speed_level in websocket status payload (#993)
The MQTT parser already tracked spd_lvl and updated state.speed_level,
  but printer_state_to_dict omitted the field, so live WebSocket pushes
  never carried it. The frontend's merge-over-old-cache update kept the
  icon stuck on its previous value; only a full page reload (which hits
  the REST /status endpoint — that field was included) would refresh it.

  Changing the print speed from the printer's own panel now updates the
  Bambuddy icon live, without a reload.
2026-04-16 08:40:28 +02:00
Sn0rrii a5c3941ef1 fix(oidc): strip trailing slash from issuer URL before building discovery URL (#985) 2026-04-15 13:50:44 +02:00
maziggy 4ed90fa35f Housekeeping 2026-04-15 12:54:53 +02:00
Thomas Rambach 360a211373 Feature: Collapsible folders for printer filters (#968) 2026-04-15 08:31:28 +02:00
maziggy 62950e37c8 fix(camera): restore new-window camera view with auth enabled
Two root causes in the "Camera View Mode = Window" path when auth is on (#979):

  1. PrintersPage opened the popup with `noopener`, which severed the opener
     link and prevented the browser from copying sessionStorage (auth token)
     into the new window. The popup booted unauthenticated, POST
     /printers/camera/stream-token returned 401, and the <img> src went out
     with no ?token=. The backend's RequireCameraStreamTokenIfAuthEnabled
     then rejected every frame with "Valid camera stream token required".
  2. CameraPage computed its stream URL from the module-level stream-token
     cache in withStreamToken(). That cache is populated by a useEffect in
     useStreamTokenSync that runs after render, so even after the token
     resolved the first post-arrival render still produced a tokenless URL
     and nothing triggered another render.

  Fix:
  - Drop `noopener` from the camera popup features (same-origin, trusted).
  - Subscribe CameraPage to the `camera-stream-token` React Query so the
    page re-renders the moment the token arrives.
  - Gate currentUrl on `waitingForStreamToken` and append the token directly
    from the reactive query value instead of the effect-synced module cache.

  Embedded overlay mode was unaffected. Added CameraPage tests covering both
  the auth-enabled (token required, src empty until it arrives, then includes
  ?token=) and auth-disabled (src rendered immediately without token) paths.
2026-04-15 08:06:27 +02:00
maziggy 1b43488016 fix(printers): recover large-3mf metadata after FTP timeout (#972)
Two-part root cause for missing photos/filament/cost on large prints
  (#972). The configured ftp_timeout was only plumbed through as the FTP
  socket timeout; the asyncio.wait_for wrapping run_in_executor stayed on
  its 60s hardcoded default, so the user's 300s setting never applied.
  Worse, asyncio.wait_for cannot cancel run_in_executor threads — after
  the 60s outer timeout fired, the executor thread kept running
  ftplib.retrbinary and frequently completed the download ~30–60s later,
  but by then the async wrapper had returned False. with_ftp_retry kept
  re-attempting the same path, each retry truncating the file the zombie
  thread had just written, and the archive was ultimately persisted as a
  fallback with no 3MF.

  download_file_async now accepts timeout at each call site (plumbed from
  ftp_timeout) and salvages post-timeout success via an explicit
  completion flag the executor thread sets only after download_to_file
  returns True. Per-attempt completion dict so a prot_p zombie can't
  flip the flag for a later prot_c attempt. A cosmetic // prefix in the
  directory-search download path is also fixed by replacing string
  concatenation with posixpath.join.
2026-04-15 07:52:28 +02:00
maziggy 899c2c6480 revert(printers): remove SD card badge entirely
Four attempts at making the printer-card SD badge stable on H2D all failed:
  the final straw was powering on an A1 causing every connected H2D to flip to
  red simultaneously. Bambu firmware SD signaling is not reliably derivable
  from MQTT — the legacy `sdcard` field is sporadic and inconsistently typed,
  and home_flag bits 8-9 are cleared on heartbeat pushes regardless of card
  state with no clean way to distinguish heartbeats from full status reports.

  Remove the badge from the Printers page card and the Printer Info modal,
  drop `sdcard` from the frontend PrinterStatus type, and strip all home_flag
  derivation and heartbeat-handling code from the MQTT parser.

  `state.sdcard` is retained on the backend and populated only from a plain
  truthy read of the `sdcard` field, because firmware_update.py uses it as a
  precondition before starting firmware installs.
2026-04-14 18:41:46 +02:00
maziggy 0d7c0d4054 fix(printers): stop H2D SD badge from flipping red on heartbeat bursts
Third follow-up on the H2D SD card badge. The prior 3-strike downgrade still
  lost the race: on idle printers, a nearby printer coming online (e.g. an A1
  reconnecting) triggered an MQTT activity burst that let idle H2Ds accumulate
  ≥3 heartbeat home_flag pushes before the next full push_status, flipping every
  H2D badge to red at once.

  Reworked the derivation:
    - the top-level `sdcard` field is authoritative when present (truthy check
      handles bool / int / "HAS_SDCARD_NORMAL" string variants)
    - home_flag bits 8-9 are only consulted on full push_status payloads
      (detected via ≥2 of gcode_state, mc_percent, nozzle_temper, print_type,
      stg_cur, ams)
    - bare heartbeat pushes carrying home_flag alone no longer affect SD state

  Removed the now-dead `_home_flag_seen` latch and 3-strike counter. Tests in
  TestSdCardParsing rewritten to cover the new semantics.
2026-04-14 18:25:36 +02:00
maziggy 665419dd22 @renovate pillow 12.1.1 → 12.2.0: patch release, security fix only (CVE-2026-40192)
@renovate pytest 9.0.2 → 9.0.3: patch release (CVE-2025-71176)
2026-04-14 12:42:46 +02:00
maziggy dd349954a6 fix(printers): stop H2D SD-card badge flipping red on heartbeat pushes
H2D sends heartbeat-style home_flag pushes where bits 8-9 are clear
  even when a card is inserted, so a single heartbeat flipped the badge
  to red until the next full push. Downgrades true->false now require
  three consecutive clear reads; upgrades apply immediately.
2026-04-14 12:32:37 +02:00
maziggy b5ccc38e4a feat(support): include all settings (redacted) + SpoolBuddy devices in support bundle
Settings dump now retains every key from the Settings table and replaces
  sensitive values with [REDACTED] instead of dropping the row. New config
  flags automatically surface in future bundles without a code change.

  Adds integrations.spoolbuddy with per-device firmware, NFC/scale hardware,
  calibration, online state and uptime — anonymized (no hostnames, IPs or
  device IDs). Both /support/bundle and the bug-report bubble benefit, since
  they share _collect_support_info().
2026-04-14 12:22:07 +02:00