# Changelog All notable changes to Bambuddy will be documented in this file. ## [0.2.3b1] - Unreleased ### New Features - **Missing Spool Assignment Notification** ([#763](https://github.com/maziggy/bambuddy/issues/763)) — When a print starts and the AMS mapping references tray slots without assigned spools, Bambuddy now shows a warning toast in the frontend and can send push notifications via any configured notification provider. The notification includes the printer name, missing slot labels (e.g. A2, Ext-L), and expected material profile. A new "Missing Spool Assignment" toggle is available under Print Events in notification provider settings (off by default). Fully integrated with i18n (all 7 locales). Contributed by @Keybored02. - **Mid-Print Spool Reassignment Tracking** ([#763](https://github.com/maziggy/bambuddy/issues/763)) — Usage tracking now correctly handles spool changes during a print. If a spool assignment is changed after a print starts, the system uses the live assignment for filament deduction; otherwise it falls back to the snapshot taken at print start. This ensures accurate filament tracking even when swapping spools mid-print. Contributed by @Keybored02. - **Auto-Link Untagged Inventory Spools on AMS Insert** ([#538](https://github.com/maziggy/bambuddy/issues/538)) — When a Bambu Lab spool is inserted into the AMS and no existing tag match is found, the system now checks if there is an untagged inventory spool with the same material, subtype, and color. If found, the RFID tag is automatically linked to that existing spool instead of creating a duplicate entry. Uses FIFO ordering (oldest spool first) so spools are consumed in purchase order. Matching is case-insensitive. Requested by @wreuel. - **External Folder Mounting for File Manager** ([#124](https://github.com/maziggy/bambuddy/issues/124)) — Host directories (NAS shares, USB drives, network storage) can now be mounted into the File Manager without copying files. Click "Link External" to point at a Docker bind-mounted path. Files are indexed into the database on scan but accessed directly from their original location — nothing is copied. Supports read-only mode (default, blocks uploads/moves/deletes), hidden file filtering, and automatic thumbnail extraction for 3MF, STL, gcode, and image files. External folders show a distinct icon and info bar with a rescan button. Deleting an external folder only removes the database index, never the actual files. Requested by @S1N4X. ### Improved - **SpoolBuddy Settings Device Tab No Longer Scrolls** — Removed the branding card, folded Device ID into the Device Info card, placed Backend/Auth config and diagnostic buttons side by side in a 2-column layout, removed the redundant online/offline status row from Device Info, and tightened spacing throughout. The Device tab now fits on the small SpoolBuddy touchscreen without scrolling. - **Spool Notes in Assign Spool Modal** ([#793](https://github.com/maziggy/bambuddy/issues/793)) — Spool cards in the Assign Spool modal now show the spool's note as a hover tooltip, making it easier to identify spools by tracking IDs or other metadata stored in notes. Works with both internal inventory and Spoolman-synced spools. Requested by @LegionCanadian. - **WiFi Safeguard for SpoolBuddy Pi** — The install script now drops an APT hook (`/etc/apt/apt.conf.d/80-preserve-wifi`) that backs up NetworkManager WiFi connections before every `apt upgrade` and restores them if they get wiped. Prevents headless SpoolBuddy Pis from losing WiFi connectivity after Raspberry Pi OS package upgrades (observed with Bookworm kernel/raspi-config updates that clear `/etc/NetworkManager/system-connections/`). - **SpoolBuddy Install Script Now Upgrades System Packages** — The install script now runs `apt-get upgrade -y` after installing required packages and the WiFi safeguard. This ensures the Pi is fully up to date before SpoolBuddy is deployed, and the WiFi safeguard protects connectivity during the upgrade. ### Fixed - **SpoolBuddy NFC Write Fails on NTAG Tags** — Multiple issues prevented writing to NTAG 213/215/216 tags. (1) Some chips report SAK `0x04` (MIFARE Ultralight family) instead of `0x00` during anticollision, and the write gate only accepted `0x00`, causing "Incompatible tag type" errors — both SAK values are now accepted. (2) The PN5180 had TX CRC disabled for both NTAG WRITE and READ commands, but the NTAG spec requires CRC on all command frames — enabled TX CRC for both. (3) The PN5180 state machine wasn't being reset between SELECT and WRITE — added proper IDLE→TRANSCEIVE transition. (4) The NTAG WRITE ACK is only 4 bits, which the PN5180 cannot capture as a complete frame (SOF detected but RX_IRQ never fires). Removed per-page ACK checking and rely on the existing full read-back verification instead. - **Database Connection Pool Exhaustion on Large Printer Farms** — Users with 100+ printers connected simultaneously experienced `QueuePool limit of size 10 overflow 20 reached, connection timed out` errors. Increased the SQLAlchemy connection pool from 30 total (10 base + 20 overflow) to 220 (20 base + 200 overflow), and raised the SQLite busy_timeout from 5 to 15 seconds to reduce write contention under heavy concurrent MQTT updates. - **SpoolBuddy Update Check Always Shows "Up to Date"** — The SpoolBuddy daemon update check compared the device's firmware version against GitHub releases instead of the running Bambuddy backend version. This meant the check could incorrectly report "up to date" even when the daemon was behind. Fixed by comparing directly against `APP_VERSION` from the backend config. - **SpoolBuddy Updates Now Use SSH** — Replaced the fragile self-update mechanism (daemon pulls its own code via git, permission errors on `.git/`, hardcoded `main` branch) with SSH-based updates driven by the Bambuddy backend. Bambuddy now SSHes into the SpoolBuddy Pi and runs git fetch/checkout, pip install, systemctl restart, and kiosk browser restart remotely. Updates automatically use the same branch as Bambuddy. SSH key pairing is fully automatic — Bambuddy generates an ED25519 keypair and includes the public key in the device registration response; the daemon deploys it to `authorized_keys` on first connect. The install script creates the `spoolbuddy` user with a bash shell and sudoers entries for daemon and kiosk restart. A "Force Update" button allows re-deploying even when versions match. The SSH public key is also shown in SpoolBuddy Settings → Updates → SSH Setup for manual pairing if needed. - **Frontend Not Updating After Deploy** — The service worker used stale-while-revalidate for JS/CSS assets, serving the old cached bundle even after a new build was deployed. Changed to network-first for JS/CSS (Vite content-hashes filenames so cache-busting is built in), bumped SW cache version, and added `Cache-Control: no-cache` to the `sw.js` endpoint so browsers always pick up new service worker versions immediately. The SpoolBuddy kiosk now skips SW registration entirely and unregisters any existing SW — a touchscreen kiosk has no use for offline caching and it was the main source of stale frontend issues after updates. - **SpoolBuddy Kiosk Starts Before Network Is Ready** — On fresh installs, the kiosk browser launched before the network was fully up, showing a connection error for 10-15 seconds until connectivity was restored. The getty@tty1 autologin override now waits for `network-online.target` so Chromium has connectivity when it starts. - **SpoolBuddy Update UI Stale After Restart** — After a SpoolBuddy update, the UI permanently showed the old version and "update available" because: (1) the SSH update set status to `"complete"` after the daemon had already re-registered, overwriting the cleared state; (2) the kiosk restart navigated away from the updates page; (3) query cache served stale data. Fixed by letting daemon re-registration clear all update status, removing the kiosk restart in favor of a frontend-driven `window.location.reload()` triggered via WebSocket when the daemon comes back online, and adding proper loading states to Check/Force Update buttons. - **Virtual Printer Proxy A1 Printing Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — BambuStudio could not send prints to A1 (and potentially P1S) virtual printers in proxy mode. The slicer connects to undocumented proprietary ports 2024-2026 on these models, which the proxy was not forwarding, causing BambuStudio to show an access code dialog instead of printing. Added transparent TCP pass-through proxying for ports 2024-2026. These ports are silently ignored on models that don't use them (X1C, H2C, P2S). Reported by @Utility9298. - **Spool Assignment on Empty AMS Slots** ([#784](https://github.com/maziggy/bambuddy/issues/784)) — Empty AMS slots (no physical spool detected) showed "Assign Spool" and "Configure" buttons in the hover popup. Assigning a spool to an empty slot created a stuck state because no "Unassign" button is available for empty slots. Truly empty slots now hide both buttons, while slots with a spool inserted but filament not loaded still show configure/assign. Also fixed stale AMS slot data on H2D and other printers that only send `{id, state}` in incremental MQTT updates — filament load/unload transitions now update in real-time without requiring a reconnect. Reported by @RosdasHH. - **Log Flood: "State is FINISH but completion NOT triggered"** ([#790](https://github.com/maziggy/bambuddy/issues/790)) — A diagnostic log message introduced in 0.2.2.1 fired on every MQTT update while a printer sat in FINISH or FAILED state, flooding logs with thousands of lines per minute in printer farms. Fixed by only logging once on the initial state transition. Reported by @user. - **H2D External Spool Print Fails With "Failed to get AMS mapping table"** ([#797](https://github.com/maziggy/bambuddy/issues/797)) — Printing from an external spool on H2D (and H2D Pro) through Bambuddy failed with `0700_8012 "Failed to get AMS mapping table"`, while the same print worked fine from BambuStudio. Bambuddy was passing raw virtual tray IDs (254/255) in the flat `ams_mapping` array, but BambuStudio converts these to -1 and relies on `ams_mapping2` for external spool routing. The H2D firmware rejects raw 254/255 in the flat array. Also fixed the `ams_mapping2` format for external trays — each virtual tray is its own AMS unit with `slot_id: 0`, not a shared unit differentiated by slot. Reported by @Lukas-ESG. - **SpoolBuddy Scale First Reading Always Wrong** — The NAU7802 ADC always returns a stale max-scale value (`0x7FFFFF`) on its first conversion after power-up, which polluted the moving average and made the initial weight report wildly inaccurate. Fixed by flushing the first reading during `init()` so all subsequent reads return valid data. Also extracted both hardware drivers out of diagnostic scripts into proper modules — the NAU7802 scale driver from `scripts/scale_diag.py` into `daemon/nau7802.py`, and the PN5180 NFC driver from `scripts/read_tag.py` into `daemon/pn5180.py`. The production daemon was importing driver classes from test scripts since the original SpoolBuddy commit. Removed the now-unnecessary `sys.path` hack from `main.py`. - **ffmpeg Process Leak Causing Memory Growth** ([#776](https://github.com/maziggy/bambuddy/issues/776)) — Camera stream ffmpeg processes accumulated over time, consuming several GB of RAM. When a user closed the camera viewer, the frontend sent a stop signal that killed the ffmpeg process, but the backend stream generator interpreted the dead process as a dropped connection and respawned ffmpeg — up to 30 reconnection attempts per stream. The orphan cleanup couldn't catch these because they were tracked as "active". Fixed by signaling the generator's disconnect event from the stop endpoint before killing the process, checking for stream removal before reconnecting, and tracking frame timestamps per-stream instead of per-printer so stale detection works correctly when multiple streams exist. Reported by @ChrisTheDBA, confirmed by @peter-k-de. ## [0.2.2.1] - 2026-03-22 ### New Features - **SpoolBuddy OTA Updates** — SpoolBuddy devices can now be updated directly from the Settings → Updates tab without SSH access. Click "Check for Updates" to see if a newer version is available, then "Apply Update" to trigger the update. The daemon picks up the command via its heartbeat, pulls the latest code from GitHub, installs dependencies, and restarts automatically via systemd. Live progress is shown in the UI with status messages from the device. The status bar at the bottom automatically checks for updates every 5 minutes and shows a prominent message when one is available. Requires the device to be online. - **Select Plates to Queue** ([#777](https://github.com/maziggy/bambuddy/issues/777)) — Multi-plate 3MF files now support selecting a subset of plates to queue, instead of only "one plate" or "all plates". In add-to-queue mode, each plate has a checkbox for multi-select, with a "Select All / Deselect All" toggle. Reprint and edit modes remain single-select. Requested by @stringham. - **Camera Image Rotation** ([#672](https://github.com/maziggy/bambuddy/issues/672)) — Added per-printer camera rotation (0°, 90°, 180°, 270°) for cameras mounted in portrait or upside-down orientations. Configurable in Settings → Camera for each printer. Rotation applies to live stream, embedded viewer, stream overlay, and notification snapshots. Requested by @wrenoud. - **Per-User Email Notifications** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — When Advanced Authentication is enabled, individual users can now receive email notifications for their own print jobs. A new "Notifications" page lets each user toggle notifications for print start, complete, failed, and stopped events. Only prints submitted by that user trigger their email — other users' prints are not affected. Requires SMTP to be configured and the "User Notifications" toggle enabled in Settings → Notifications. Administrators and Operators have access by default; Viewers do not. Contributed by @cadtoolbox. ### Fixed - **SpoolBuddy Daemon Reports Stale Version** — The SpoolBuddy daemon maintained its own hardcoded `__version__` that was never bumped to `0.2.3b1`, causing the update check to incorrectly show an update from `0.2.2b1` to the latest release. Fixed by reading the version at import time from the backend's `APP_VERSION` in `backend/app/core/config.py` — the single source of truth — so the daemon version is always in sync. - **SpoolBuddy Update Columns Missing from Database** — The OTA update feature added `update_status` and `update_message` to the device model but was missing the database migration, causing "no such column" errors on existing installations. - **Queue Print Command Not Reaching Printer** ([#778](https://github.com/maziggy/bambuddy/issues/778)) — When a queue item targeted a specific printer and the scheduler's power-on-wait loop triggered, each reconnection attempt created a new MQTT client that re-attempted subscribing to the request topic. On printers whose broker rejects this subscription (e.g. A1), this caused repeated connect/disconnect cycles for up to 170 seconds, leaving the MQTT connection in a fragile state where the print command could silently fail to reach the printer. Fixed by caching request topic support state per serial number at the class level, so new client instances skip the subscription immediately instead of rediscovering the rejection. Reported by @RubenKremer. - **AMS Slot Search Shows Unrelated Profiles** ([#681](https://github.com/maziggy/bambuddy/issues/681)) — Searching for a non-existent filament profile in the AMS slot configuration showed unrelated profiles instead of an empty result. The saved preset bypassed the search filter entirely, so stale mappings (e.g. a slot previously configured with "Bambu PLA Matte" that now holds a Silk spool) would always appear regardless of the search query. The saved preset now only bypasses the printer model filter, not the search filter. Reported by @RosdasHH. - **Virtual Printer FTP Routed to Wrong VP** ([#735](https://github.com/maziggy/bambuddy/issues/735)) — When running multiple virtual printers with different access codes on separate bind IPs, FTP connections were routed to the wrong VP. Root cause: the iptables `REDIRECT` rule rewrites the destination IP to the incoming interface's primary address, so all FTP traffic went to the first VP regardless of the intended target. Fix: FTP server now binds directly to port 990 (standard implicit FTPS), eliminating the need for iptables redirect. Requires `CAP_NET_BIND_SERVICE` (already set in the systemd service and Docker image). Also removed a global `set_exception_handler()` in the MQTT server that caused spurious error messages when running multiple VPs. See `docs/migration-vp-ftp-port.md` for migration steps. Reported by @VREmma. - **X1C Virtual Printer Not Accepting Sends** ([#735](https://github.com/maziggy/bambuddy/issues/735)) — X1C (and X1) virtual printers were advertised with legacy SSDP model codes (`3DPrinter-X1-Carbon` / `3DPrinter-X1`) that BambuStudio doesn't recognize, causing "incompatible printer preset" when sending. Fixed to use the correct codes (`BL-P001` / `BL-P002`). Also fixed proxy mode auto-inherit storing the printer's display name (e.g. `X1C`) instead of the SSDP code. Existing VPs are automatically migrated on startup. Reported by @RosdasHH. - **White Filament Color Swatches Invisible in Light Theme** ([#726](https://github.com/maziggy/bambuddy/issues/726)) — Filament color circles used a white border that was invisible against light theme backgrounds, making white spools indistinguishable. Changed to a dark border (`border-black/20`) across all views: Inventory, Archives, Assign Spool, Configure AMS Slot, Calendar, Projects, Filament Trends, Local Profiles, Link Spool, and Spoolman Settings. Reported by user. - **Camera Window Overlapping Modals** ([#738](https://github.com/maziggy/bambuddy/issues/738)) — Floating camera viewer rendered on top of modals (e.g. Assign Spool), making them unusable. Lowered camera z-index so modals always appear above it. Reported by @maziggy. - **Print Complete Notification Not Firing** ([#736](https://github.com/maziggy/bambuddy/issues/736)) — Print complete notifications could silently fail if the finish photo capture hung or timed out, because the notification was chained behind the photo task with no timeout. Added a 45-second timeout so notifications always send even if photo capture stalls. Also added diagnostic logging for MQTT state detection to trace completion triggers. Reported by @piatho. - **Webhook Notifications Missing Camera Snapshot** ([#679](https://github.com/maziggy/bambuddy/issues/679)) — Webhook notification providers did not include camera snapshots (e.g. from First Layer Complete notifications), even though providers like Telegram, Pushover, ntfy, and Discord already attached them. The webhook payload now includes a base64-encoded `image` field when a snapshot is available (generic format only, not Slack format). Reported by @Arn0uDz. - **Mobile Sidebar Not Scrollable** — On mobile devices with many navigation items, the sidebar did not scroll, making bottom items unreachable. Added overflow scrolling to the nav section while keeping the logo and footer pinned. - **User Notification Ruff/Lint Fixes** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — Fixed missing `timezone` import in email timestamp, unused lambda argument, PEP 8 blank line spacing for `mark_printer_stopped_by_user`, and SQLAlchemy forward reference in `UserEmailPreference` model. - **Carbon Rod Lubrication Maintenance Task Incorrect** ([#755](https://github.com/maziggy/bambuddy/issues/755)) — X1/P1 series printers showed a "Lubricate Carbon Rods" maintenance task, but carbon rods use plain bearings and should never be lubricated — doing so degrades print quality. Removed the lubrication task; only "Clean Carbon Rods" remains. Existing "Lubricate Carbon Rods" entries are automatically removed on next startup. Reported by @RosdasHH. - **Ntfy Notifications Fail With Non-ASCII Characters** ([#742](https://github.com/maziggy/bambuddy/issues/742)) — Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters (e.g. accented letters, CJK). The `Title` and `Message` HTTP headers were passed as Python strings, causing httpx to reject them with `UnicodeEncodeError`. Fixed by encoding header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. Reported by @user. - **Virtual Printer Proxy Mode Printing Fails on Isolated Networks** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — When the slicer and printer are on different VLANs/subnets, Bambu Studio could not send prints through the virtual printer proxy because: (1) the printer's real IP leaked through MQTT payloads (`rtsp_url`, `net.info[].ip`), causing BS to bypass the proxy; (2) the bind/detect protocol (port 3000/3002) was forwarded to the real printer, leaking its identity and name; (3) the file transfer tunnel (port 6000) used by BS for verify_job and uploads was not proxied; (4) FTP data connections for zero-byte uploads (verify_job) failed due to a TLS handshake race condition. Fixed by: rewriting IP addresses in MQTT PUBLISH payloads (both string and integer formats) with proper MQTT framing preservation, responding to bind/detect with the VP's own identity via BindServer, adding transparent TCP proxies for port 6000 (file transfer) and port 322 (RTSP camera), buffering slicer data during FTP data proxy connection setup, and advertising the configured VP name in SSDP. Also added cross-subnet SSDP support via a wildcard listener for VPN/multi-subnet setups. Reported by @Utility9298. - **Virtual Printer Proxy Mode X1C/X1 Print Upload Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — X1C and X1 printers failed to upload prints through proxy mode. After FTP verify_job succeeded (226), BambuStudio's closed-source `bambu_networking` DLL silently refused to proceed with the actual 3MF upload, showing a login modal instead. Root cause: the DLL validates the TLS connection parameters and rejects connections where the certificate doesn't match the printer's real BBL CA certificate. The TLS-terminating proxy presented Bambuddy's own "Virtual Printer CA" certificate, which the DLL rejected. Fixed by switching to transparent TCP proxying for FTP (port 990), FileTransfer (port 6000), Camera (port 322), and FTP passive data (ports 50000–50100) — raw bytes are forwarded without TLS termination, so the slicer gets end-to-end TLS directly with the printer's real certificate. Only MQTT (port 8883) remains TLS-terminated, which is required to rewrite the printer's real IP with the proxy's bind IP in MQTT payloads. Confirmed working on both H2D and X1C printers. - **UserEmailPreference Model Not Registered** — The `UserEmailPreference` SQLAlchemy model was not imported in `models/__init__.py`, causing mapper initialization failures when the `User` model's relationship resolved the string reference before the model class was registered with Base metadata. - **Native Install Missing CAP_NET_BIND_SERVICE** — The `install.sh` systemd service template was missing `AmbientCapabilities=CAP_NET_BIND_SERVICE`, causing Virtual Printer proxy mode to silently fail to bind privileged ports (322, 990) on native installations. - **Virtual Printer Proxy A1 Diagnostics** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — Added diagnostic port probing (ports 21, 80, 443) on proxy VP bind IPs to detect if BambuStudio tries to connect on ports the proxy doesn't handle. Logs a warning when an unexpected connection is detected. Helps diagnose A1/A1 Mini proxy issues where the slicer may use a different connection flow. - **File Rename Removes Extension** ([#751](https://github.com/maziggy/bambuddy/issues/751)) — Renaming a file in the File Manager included the file extension in the editable text, so users could accidentally remove it (e.g. renaming `bracket.gcode.3mf` to `bracket`), making the file unprintable. The rename modal now only lets users edit the base name, with the extension shown as a non-editable suffix. Reported by @fleishmaab, confirmed by @cadtoolbox. - **Spurious "Job Waiting for Filament" Notification** ([#753](https://github.com/maziggy/bambuddy/issues/753)) — When all printers of a model were busy and a job was queued with ASAP timing, a "Job Waiting for Filament" notification fired immediately even though no filament issue existed. The job was simply waiting for a printer to finish. The scheduler now skips the waiting notification when all matching printers are just busy, since the job will auto-start when one finishes. Also renamed the default notification title from "Job Waiting for Filament" to "Queue Job Waiting" to accurately reflect all waiting reasons. Reported by @maziggy. - **AMS Spools Removed After Printer Restart** ([#765](https://github.com/maziggy/bambuddy/issues/765)) — AMS spool assignments and slot configurations were lost after restarting the printer. When the printer shuts down, it sends a final MQTT message with `tray_exist_bits=0` and `power_on_flag=false`, which caused Bambuddy to clear all AMS slot data and auto-unlink every spool assignment. On reconnect, the assignments were gone. Fixed by skipping `tray_exist_bits` slot clearing when `power_on_flag` is `false` (shutdown message), preserving AMS data across printer restarts. Reported by @Woyteck1. ### Community Contributions - **Admin Set Default Nav-Menu Order** ([#761](https://github.com/maziggy/bambuddy/pull/761)) — Admins with authentication enabled can now set their current sidebar menu order as the default for new users. New users inherit this layout on first login and can customize it afterward. Contributed by @cadtoolbox. - **Improve Home Assistant Notifications** ([#750](https://github.com/maziggy/bambuddy/pull/750)) — Added support for Home Assistant `notify` services in addition to the existing REST-based integration. Contributed by @mrtncode. - **Add Total Cost to Projects** ([#733](https://github.com/maziggy/bambuddy/pull/733)) — The Projects page now shows a total cost that sums material, energy, and BOM costs. Contributed by @Keybored02. - **Material Mismatch & Insufficient Filament Checks** ([#720](https://github.com/maziggy/bambuddy/pull/720)) — When assigning non-Bambu Lab spools, a warning prompts if the filament type or profile doesn't match. Pre-print checks now also warn when the spool has insufficient material. Both warnings are dismissible, with a toggle in Settings. Contributed by @Keybored02. - **Send Bambu RFID Tags to Spoolman & Manual Mode Unlink** ([#719](https://github.com/maziggy/bambuddy/pull/719)) — Bambu Lab spool RFID identifiers (tray UUID) are now sent to Spoolman instead of generic placeholder tags. An "Unlink" button appears on Bambu spools when Spoolman is in manual sync mode. Fixed location clearing for generic spools during sync. Contributed by @shrunbr. - **Rework Archive Duplicates Tagging** ([#718](https://github.com/maziggy/bambuddy/pull/718)) — Duplicate detection now requires both matching filename and SHA256 hash. The tag shows reprint count instead of "Duplicate" text, links back to the parent print, and a new "Hide Duplicates" filter is available. Contributed by @Keybored02. ### Added - **Quick Print Speed Control** ([#256](https://github.com/maziggy/bambuddy/issues/256)) — Added a print speed control badge to the printer card controls row, next to the fan status badges. Click to choose between Silent (50%), Standard (100%), Sport (124%), and Ludicrous (166%) speed presets. The badge shows the current speed percentage with a gauge icon, always visible but disabled when no print is active. Includes optimistic UI updates for instant feedback. Requested by @Sllepper. - **Spool Rotation During AMS Drying** — Added a "Rotate spool during drying" checkbox to the manual drying popover for AMS 2 Pro and AMS-HT units. Rotates the spool for more even heat distribution. Off by default; resets when opening the popover for a different AMS unit. The firmware silently disables rotation if filament is currently loaded from the unit. - **Spool Name Column & Filter in Filament Inventory** ([#740](https://github.com/maziggy/bambuddy/issues/740)) — Added a "Spool" column to the filament inventory table that displays the spool catalog entry name (e.g. "Bambu Lab AMS Tray", "Sunlu 1kg"). Enable it via the column visibility menu. Sortable and hidden by default. Also added a spool name filter dropdown next to the brand filter for quick filtering by spool type. Requested by @DMoenning. ### Changed - **Redesigned Bug Report Debug Log Flow** — Replaced the fixed 30-second debug log collection with an interactive 3-step flow: start debug logging, reproduce the issue at your own pace, then stop & submit. An elapsed timer shows recording duration with auto-stop at 5 minutes. Users now have full control over when to capture logs instead of racing a countdown. The backend splits log collection into separate start/stop endpoints, and the frontend shows a step progress indicator with pulsing active state. ### Improved - **HMS Error Visibility on Printers Page** ([#772](https://github.com/maziggy/bambuddy/issues/772)) — Improved visibility of printers with HMS errors for large print farms. Added a red "Problem" counter to the status summary bar showing how many connected printers have active HMS errors. The compact-mode status pip (colored dot) now turns red for fatal/serious errors (severity ≤ 2) or amber for common warnings, instead of only showing connection status. Progress bars turn amber when a print is paused. Sorting by status now places printers with HMS errors at the top, above printing and idle printers. Requested by @jimmy-brightz. - **Print Command Response Verification** ([#737](https://github.com/maziggy/bambuddy/issues/737)) — After sending a print command, BambuBuddy now monitors whether the printer's state changes within 15 seconds. If the printer silently ignores the command (observed on some P1S firmware versions where the MQTT command handler becomes unresponsive), a warning is logged for diagnostics. This aids debugging when users report prints not starting despite BambuBuddy showing success. - **Compact Assign Spool Modal** ([#725](https://github.com/maziggy/bambuddy/issues/725)) — The "Assign Spool" modal now uses a compact 3-column grid layout instead of a vertical list, showing more spools at once without scrolling. Each card displays the spool name, color, and remaining/total weight. The modal is wider with a taller scroll area. Requested by @RosdasHH. - **Reformatted AMS Drying Presets Table** ([#732](https://github.com/maziggy/bambuddy/issues/732)) — The drying presets table in Settings now groups columns by AMS type (AMS 2 Pro, AMS-HT) with inline °C and h unit labels next to each input, replacing the previous flat column layout. Requested by @cadtoolbox. ### Security - **Bump pyOpenSSL 25.3.0 → 26.0.0** — Fixes CVE-2026-27448 (exception swallowing in TLS servername callback) and CVE-2026-27459 (buffer overflow in DTLS cookie callback). - **Bump pyasn1 0.6.2 → 0.6.3** — Fixes CVE-2026-30922 (stack overflow from deeply nested ASN.1 structures). - **Bump flatted 3.4.1 → 3.4.2** — Fixes GHSA-rf6f-7fwh-wjgh (prototype pollution via `parse()`). Dev-only dependency (eslint). ## [0.2.2] - 2026-03-16 ### New Features - **First Layer Complete Notification** ([#679](https://github.com/maziggy/bambuddy/issues/679)) — Get notified with a camera snapshot when the first layer finishes printing, so you can check adhesion remotely without watching the whole print. Enable the "First Layer Complete" toggle on any notification provider. Fires once per print when layer 2 begins (confirming layer 1 is done), with a guard against spurious triggers on printer reconnect. Requested by community. - **Remote AMS Drying** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — Start, monitor, and stop drying sessions for AMS 2 Pro and AMS-HT directly from the Printers page. A flame icon appears on supported AMS cards; clicking it opens a popover to select filament type (PLA, PETG, TPU, ABS, ASA, PA, PC, PVA) with official BambuStudio temperature/duration presets, or set temperature manually. When drying is active, a status bar shows the time remaining with a live countdown and stop button. Supported on X1/X1C (fw 01.09+), P1P/P1S (fw 01.08+), H2D (fw 01.02.30+), H2D Pro, and X1E. Not supported on P2S, A1, A1 Mini, H2S, or H2C. Requires `printers:control` permission when authentication is enabled. - **Queue Auto-Drying** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — Automatically dry filament between scheduled queue prints. When enabled in Settings → Print Queue, the scheduler starts drying on idle printers that have upcoming scheduled prints and whose AMS humidity exceeds the configured threshold. Uses conservative parameters (lowest temperature, longest duration) when mixed filament types are loaded. Drying stops automatically when humidity drops below threshold (with a 30-minute minimum to prevent oscillation), when scheduled items are removed, or when the feature is disabled. Optional "block queue" mode delays the next print until drying completes. - **Configurable Drying Presets** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — Customize temperature and duration for each filament type in Settings → Print Queue. Defaults match BambuStudio presets (PLA 55°C/8h, PETG 65°C/8h, etc.) and are used by both the manual drying popover and queue auto-drying. AMS 2 Pro and AMS-HT use separate presets reflecting their different heating capabilities. - **AMS PSU Detection** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — The drying button is disabled with a tooltip when the AMS lacks sufficient power for drying (e.g. not connected to the external PSU). Reads `dry_sf_reason` from printer firmware and surfaces HMS error codes for AMS 2 Pro and AMS-HT power issues. - **Ambient Drying** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — Automatically keep filament dry on idle printers based on humidity, even without queued prints. Enable "Ambient drying" in Settings → Print Queue to have the scheduler start drying on any idle printer whose AMS humidity exceeds the configured threshold — no scheduled prints required. Uses the same humidity threshold, drying presets, and power constraint detection as queue auto-drying. Both modes can be enabled simultaneously. Requested by community. - **Assign Spool to Empty AMS Slot** ([#717](https://github.com/maziggy/bambuddy/issues/717)) — Previously, the "Assign Spool" button only appeared on AMS slots that already had a filament profile configured, requiring users to first configure the slot manually before assigning an inventory spool — even though the assignment auto-configures the slot anyway. The "Assign Spool" option now appears on empty (unconfigured) slots as well. Selecting a spool auto-configures the slot with the correct filament profile, color, and K-profile in one step. Also fixed the AMS slot profile label showing the generic material type (e.g. "PLA") instead of the spool's actual slicer preset name (e.g. "PolyLite PLA Pro") after assignment. Requested by @RosdasHH. - **Home Assistant Notification Provider** ([#656](https://github.com/maziggy/bambuddy/issues/656)) — Added Home Assistant as a notification provider. When HA is configured in Settings → Network → Home Assistant, selecting "Home Assistant" as a notification provider sends persistent notifications to the HA dashboard — no additional configuration needed. From there, HA automations can forward notifications to mobile apps, WhatsApp, or any other service. Requested by @TravisWilder. - **Virtual Printer Queue Auto-Dispatch Toggle** ([#587](https://github.com/maziggy/bambuddy/issues/587)) — Added an "Auto-dispatch" toggle to virtual printers in Queue mode. When enabled (default), prints sent from the slicer are added to the queue and start automatically on the assigned printer — matching the current behavior. When disabled, prints are added to the queue with `manual_start` set, so they wait for manual dispatch. This allows users who want to review and manually assign prints before they start. Requested by @Percy2Live. - **Queue All Plates** ([#530](https://github.com/maziggy/bambuddy/issues/530)) — Multi-plate 3MF files can now be queued in one action. When adding a multi-plate file to the queue, a "Queue All N Plates" toggle appears in the plate selector. When activated, every plate is added as a separate queue entry (one per plate × per selected printer), each individually editable from the queue page. The toggle is only available in add-to-queue mode (not reprint or edit). Requested by @Dendrowen. - **Malaysian Ringgit Currency** ([#634](https://github.com/maziggy/bambuddy/issues/634)) — Added MYR (RM) to the list of supported currencies for filament cost tracking. Requested by @cynogen127. - **ETA Variable in Notifications** ([#638](https://github.com/maziggy/bambuddy/issues/638)) — Added `{eta}` template variable to print start, print progress, and queue job started notifications. Shows the estimated wall-clock completion time (e.g. "15:53" or "3:53 PM") based on the user's configured time format (12h/24h). Existing `{estimated_time}` still shows duration ("1h 23m"). Requested by @SebSeifert. - **Bulk Delete Spool and Color Catalog Entries** ([#646](https://github.com/maziggy/bambuddy/issues/646)) — Added checkbox selection and bulk delete to both the Spool Catalog and Color Catalog in Settings > Filament. Select individual entries with checkboxes, use the header checkbox to select/deselect all visible entries, then click "Delete Selected" to remove them in one operation. Previously, entries could only be deleted one at a time. Requested by @SebSeifert. - **Force Color Match** ([#625](https://github.com/maziggy/bambuddy/pull/625)) — Added a "Force Color Match" option for "Print to Any" queue scheduling. When enabled, the scheduler requires a strict color match when assigning prints to printers, preventing incorrect filament assignments when multiple candidates are close in color. Prints wait in the queue until a printer with the exact matching filament is available. Contributed by @cadtoolbox. - **Israeli New Shekel Currency** — Added ILS (₪) to the list of supported currencies for filament cost tracking. - **AMS Info Card & Custom Labels** ([#570](https://github.com/maziggy/bambuddy/pull/570)) — Hovering an AMS label (e.g. "AMS-A") on the Printers page now shows a popover with serial number, firmware version, and an editable friendly name. Custom labels are stored by AMS serial number so they persist when the unit is moved to a different printer. Slot numbers are now displayed inside each filament color circle with auto-inverted contrast for readability. Labels also appear in the Inventory page's location column. Contributed by @cadtoolbox. - **In-App Bug Reporting** — A floating bug report button in the bottom-right corner lets users submit bug reports directly from the Bambuddy UI. Reports include a description, optional screenshot (upload, paste, or drag & drop with automatic JPEG compression), optional contact email, and automatically collected diagnostic data. On submit, the system temporarily enables debug logging, sends push_all to all connected printers, waits 30 seconds to collect fresh logs, then submits everything to a secure relay on bambuddy.cool which creates a GitHub issue with sanitized logs uploaded as a separate file. All sensitive data (printer names, serial numbers, IPs, credentials, email addresses) is redacted from logs before submission. The expandable data privacy notice details exactly what is and isn't collected. Translated into all 7 supported languages. - **SpoolBuddy NFC Tag Writing (OpenTag3D)** — SpoolBuddy can now write NFC tags for third-party filament spools using the OpenTag3D format on NTAG213/215/216 stickers. A new "Write" page (`/spoolbuddy/write-tag`) in the kiosk UI provides three workflows: write a tag for an existing inventory spool (no tag linked yet), create a new spool and write in one flow, or replace a damaged tag (unlinks old, writes new). The left panel shows a searchable spool list or a compact creation form (material dropdown, color picker, brand, weight); the right panel shows real-time NFC status with tag detection, a spool summary, and the write button. The backend encodes spool data as a 133-byte OpenTag3D NDEF message (MIME type `application/opentag3d`, fits NTAG213's 144-byte capacity) containing material, color, brand, weight, temperature, and RGBA color data. The write command flows through the existing heartbeat polling mechanism — the frontend queues a write, the daemon picks it up on the next heartbeat, writes page-by-page with read-back verification via the PN5180's NTAG WRITE (0xA2) command, and reports success/failure via WebSocket. On success the tag UID is automatically linked to the spool with `data_origin=opentag3d`. Written tags are readable by any OpenTag3D-compatible reader including SpoolBuddy itself. Translations added for all 6 languages. - **SpoolBuddy On-Screen Keyboard** — Added a virtual QWERTY keyboard for the SpoolBuddy kiosk UI (and login page) since the Raspberry Pi has no physical keyboard and system-level virtual keyboards (squeekboard, wvkbd) don't auto-show/hide in the labwc/Chromium kiosk environment. Uses `react-simple-keyboard` with a dark theme matching the bambu-dark/bambu-green palette. Auto-shows when any text/password/email input is focused, supports shift, caps lock, backspace, and email-friendly keys (@, .). Inputs with `data-vkb="false"` are excluded (e.g. SpoolBuddySettingsPage's own numpad). A two-phase close prevents ghost-click passthrough to elements underneath the keyboard. - **SpoolBuddy Inline Spool Cards** — Placing an NFC-tagged spool on the SpoolBuddy reader now shows spool info directly in the dashboard's right panel instead of a separate modal overlay. Known spools display a SpoolIcon with color/brand/material, a large remaining-weight readout with fill bar, and a weight comparison grid, with action buttons for "Assign to AMS", "Sync Weight", and "Close". Unknown tags show the tag UID, scale weight, and offer "Add to Inventory" or "Link to Spool" actions. The card stays visible if the tag is removed (for continued interaction) and won't re-appear for the same tag after dismissal — but re-placing a tag after removal shows it again. The idle spool animation displays when no tag is detected. - **SpoolBuddy AMS Page: External Slots & Slot Configuration** — The SpoolBuddy AMS page (`/spoolbuddy/ams`) now displays external spool slots (single nozzle: "Ext", dual nozzle: "Ext-L"/"Ext-R") and AMS-HT units in a compact horizontal row below the regular AMS grid, fitting within the 1024×600 kiosk display without scrolling. Clicking any AMS, AMS-HT, or external slot opens the `ConfigureAmsSlotModal` to configure filament type and color — the same modal used on the main Printers page. Dual-nozzle printers show L/R nozzle badges on each AMS unit. Temperature and humidity are displayed with threshold-colored SVG icons (green/gold/red) matching the Bambu Lab style on the main printer cards, using the configured AMS humidity and temperature thresholds from settings. - **SpoolBuddy Dashboard Redesign** — Redesigned the SpoolBuddy dashboard with a two-column layout: left column shows device connection status (scale and NFC with state-colored icons — green when device is online, gray when offline) and printer status badges below (compact pills with green/gray dots for online/offline, wrapping to fit without scrolling); right column shows the current spool card. Cards use a dashed border style for a cleaner look. The large weight display card was removed in favor of the inline scale reading in the device card. Unknown NFC tags now offer a quick-add modal that creates a basic PLA spool entry linked to the tag — with a hint recommending users add spools via the main Bambuddy UI first for full details. The separate SpoolBuddy inventory page was removed since inventory management belongs in the main Bambuddy frontend; the bottom nav now has three tabs (Dashboard, AMS, Settings). - **SpoolBuddy Kiosk Auth Bypass via API Key** — When Bambuddy auth is enabled, the SpoolBuddy kiosk (Chromium on RPi) was redirected to the login page because the `ProtectedRoute` requires a user object from `GET /auth/me`, which only accepted JWT tokens. The `/auth/me` endpoint now also accepts API keys (via `Authorization: Bearer bb_xxx` or `X-API-Key` header) and returns a synthetic admin user with all permissions. The frontend's `AuthContext` reads an optional `?token=` URL parameter on first load, stores it in localStorage, and strips it from the URL to prevent leakage via browser history or referrer. The install script now includes the API key in the kiosk URL (`/spoolbuddy?token=${API_KEY}`), so the device authenticates automatically on boot without manual login. - **Daily Beta Builds** — Added a release script (`docker-publish-daily-beta.sh`) that reads the current `APP_VERSION` from config, builds a multi-arch Docker image, pushes to both GHCR and Docker Hub, and creates/updates a GitHub prerelease with changelog notes. Daily builds overwrite the same beta version tag (e.g., `0.2.2b1`) — users pull the latest by re-pulling the tag or using Watchtower. Beta images are never tagged as `latest`. - **Inventory Scale Weight Check Column** — Added a "Weight Check" column (hidden by default) to the inventory table that compares each spool's last scale measurement against its calculated gross weight (net remaining + core weight). Spools within a ±50g tolerance show a green checkmark; mismatched spools show a yellow warning with the difference and a sync button that trusts the scale reading and resets weight tracking. The backend stores `last_scale_weight` and `last_weighed_at` on each spool whenever weight is synced via SpoolBuddy, and the column tooltip shows scale weight, calculated weight, and difference. Edge case: when scale weight is below core weight (empty spool or not on scale), the comparison treats it as a match since sync can't correct this. ### Fixed - **Library Upload Doesn't Show New File Until Page Reload** ([#704](https://github.com/maziggy/bambuddy/issues/704)) — After uploading a file in the Library file manager, the file list didn't update until the user reloaded the browser. The upload endpoint used `db.flush()` instead of `db.commit()`, so the new row was only written to the database *after* the response was sent to the client. The frontend immediately refetched the file list upon receiving the response, but a new database session couldn't see the uncommitted row — resulting in stale data. Fixed by committing before the response is returned. Also fixed the same race condition in folder create, folder update, and file update endpoints. Reported by @shadowjig. - **Printer File Manager Doesn't Auto-Refresh** ([#704](https://github.com/maziggy/bambuddy/issues/704)) — The printer file manager (SD card browser) only fetched the file list once when opened. Files uploaded from BambuStudio/OrcaSlicer while the modal was open wouldn't appear until the user clicked the refresh button or reopened the modal. Now auto-refreshes every 30 seconds while open. Reported by @shadowjig. - **Database Connection Pool Exhaustion Under Load** ([#704](https://github.com/maziggy/bambuddy/issues/704)) — Background tasks (print scheduler FTP uploads, camera captures, notification sends, timelapse stitching) held database sessions open during slow network I/O, consuming connection pool slots for seconds at a time. With the default pool of 15 connections (size 5 + overflow 10), concurrent operations during print start/complete events could exhaust the pool, causing `QueuePool limit reached` errors and `greenlet_spawn` failures in RFID spool auto-assignment. Doubled the pool to 30 connections (size 10 + overflow 20). Reported by @shadowjig. - **Block Mode Skips Humidity Auto-Stop** ([#292](https://github.com/maziggy/bambuddy/issues/292)) — When "Wait for drying to complete" was enabled and a printer had pending queue items, the scheduler skipped the humidity auto-stop check entirely. A drying session that reached its humidity target would continue indefinitely instead of stopping after the 30-minute minimum. Now, block mode only prevents starting new drying — already-drying printers still have their humidity checked and stopped when the threshold is met. - **AMS Fill Level Shows 0% for Non-Viewer Users** ([#676](https://github.com/maziggy/bambuddy/issues/676)) — When authentication was enabled with advanced permissions, users with `inventory:view_assignments` permission saw 0% fill level on AMS slots where inventory spool data had stale `weight_used` values. The fill level fallback chain (Spoolman → Inventory → AMS remain) used nullish coalescing (`??`), which doesn't fall through on `0` — so a stale inventory fill of 0% permanently shadowed the correct real-time AMS remain value from the printer. Now, when inventory says 0% but the AMS hardware reports a positive remain, the inventory value is bypassed in favor of the live AMS data. Viewer users were unaffected because their group lacked `inventory:view_assignments`, so the inventory query never fired and the AMS remain was used directly. Reported by @cadtoolbox. - **Virtual Printer Proxy Mode Always Shows X1C Model** — Creating a virtual printer in Proxy mode always set the model to X1C regardless of the destination printer, because the frontend hides the model dropdown in proxy mode and the backend defaulted to X1C. Now auto-inherits the model from the target printer when creating or updating a proxy virtual printer (e.g. a proxy pointing at a P1S correctly presents itself as P1S to the slicer). The model also auto-updates when changing the target printer or switching to proxy mode. - **Cloud Profiles Shared Across All Users** ([#665](https://github.com/maziggy/bambuddy/issues/665)) — When authentication was enabled, Bambu Cloud credentials were stored globally — one account per Bambuddy instance. If User A logged into Cloud, every other user saw User A's account and profiles. User B logging in would overwrite User A's credentials. Cloud credentials are now stored per-user: each user logs into their own Bambu Cloud account independently. When auth is disabled (single-user mode), behavior is unchanged. Also fixed cloud data endpoints (`/cloud/settings`, `/cloud/fields`, preset CRUD) requiring `settings:read` / `settings:update` permissions instead of `cloud:auth` — users who had "Cloud Auth" enabled but "Settings" disabled couldn't load profiles after logging in. Reported by @cadtoolbox. - **Local Profiles Not Shown in AMS Slot Configuration** — Imported local filament profiles were hidden in the AMS slot configure modal when a printer model was set. The `compatible_printers` filter parsed the stored JSON array as a semicolon-delimited string, so the matching always failed and every local preset was silently skipped. Removed the filter entirely — user-imported profiles should be available on any printer. - **Interface Aliases Not Shown in Virtual Printer Interface Select** — Interface aliases (e.g. `eth0:1`) added for multi-virtual-printer setups were invisible in the bind IP dropdown. The Docker image didn't include `iproute2`, so the `ip` command wasn't available and the code fell back to ioctl-based enumeration which can only return one IP per interface. Added `iproute2` to the Docker image. - **P2S Camera Stream Disconnects After a Few Seconds** ([#661](https://github.com/maziggy/bambuddy/issues/661)) — The P2S firmware drops RTSP sessions after a few seconds with an I/O error. Root cause: ffmpeg in the Docker image uses GnuTLS for TLS, and Debian's hardened GnuTLS defaults reject TLS behaviors (renegotiation, legacy ciphers) that some printer firmwares rely on. Added a local TLS termination proxy that uses Python's ssl module (OpenSSL) to handle the TLS connection to the printer, exposing a plain RTSP port to ffmpeg. The proxy rewrites RTSP request-line URLs while preserving Digest auth headers. Also reduced RTSP reconnect delay from 1.0s to 0.2s, added ffmpeg fast-start flags for lower startup latency, and fixed external camera streams being choppy due to double rate-limiting in the proxy layer. Reported by @ddetton, confirmed by @DMoenning. - **iOS/iPadOS Cannot Reposition Floating Camera** ([#687](https://github.com/maziggy/bambuddy/issues/687)) — The floating camera viewer (embedded camera window on the dashboard) could not be dragged or resized on iOS/iPadOS because it only handled mouse events. Touch input scrolled the page underneath instead of moving the camera window. Added touch event support (`touchstart`/`touchmove`/`touchend`) to both the header drag handle and the resize handle, with `preventDefault` to stop page scrolling during drag. Reported by @dsmitty166. - **PA-CF / PA12-CF / PAHT-CF Not Treated as Compatible** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable, but the print scheduler and filament override UI used exact string matching. If a 3MF required PA-CF but the AMS had PA12-CF loaded, the scheduler wouldn't assign the job and the filament override dropdown was empty/disabled. Added a filament type equivalence system so these PA variants are treated as compatible in scheduler assignment, AMS slot matching, force color match validation, and the filament override dropdown. Reported by @aneopsy. - **Force Color Match Toggle Click Target Too Large** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — In the Schedule Print modal, clicking anywhere on the "Force color match" row toggled the checkbox, not just the checkbox and its label. The click target now covers only the checkbox, icon, and label text. Reported by @aneopsy. - **HA Switch Badge Always Sends Turn On Instead of Toggle** — Clicking a non-script Home Assistant entity (switch, light, input_boolean) on the printer card always sent `turn_on`, which is a no-op when the switch is already on. Now sends `toggle` for non-script entities so the badge click actually toggles the switch state. Script entities still use `turn_on` (stateless trigger). - **Multiple Plugs Per Printer Crashes Auto-On/Off** — When multiple smart plugs were assigned to the same printer (e.g., a Tasmota plug + an HA switch), the auto-on/auto-off handler called `scalar_one_or_none()` which raises `MultipleResultsFound`. Now fetches all plugs and returns the main (non-script) power plug, matching the API route behavior. - **Multiple HA Switches Per Printer UNIQUE Constraint** — The migration that removes the UNIQUE constraint on `smart_plugs.printer_id` (to allow multiple HA switches per printer) used an exact string match to detect the constraint in the SQLite schema. Databases created with older SQLAlchemy versions expressed the constraint differently (e.g. quoted column names, table-level `UNIQUE(printer_id)`, or separate indexes), so the migration silently skipped them. Users hit `IntegrityError: UNIQUE constraint failed` when assigning a second HA switch to a printer. Now uses regex pattern matching and also checks for standalone UNIQUE indexes. - **HMS Notifications for Unknown/Phantom Error Codes** — Printers send many undocumented or phantom HMS error codes that don't correspond to real errors (e.g. calibration status codes after firmware updates). These triggered email/push notifications even though the printer card correctly filtered them out. Flipped the notification logic from "notify all, suppress specific codes" to "only notify for errors with known descriptions", matching the frontend behavior. Also fixed the log message reporting incorrect notification counts. - **Ethernet Badge Shown on WiFi Printers / MQTT Disconnecting** ([#585](https://github.com/maziggy/bambuddy/issues/585)) — Three bugs in the ethernet badge feature: (1) `home_flag` bit 18 is set on all printers regardless of connection type, so every ethernet-capable model showed the ethernet badge even when connected via WiFi. Replaced bit 18 detection with wifi_signal-based heuristic: printers on ethernet with WiFi disabled report a hardcoded `-90 dBm` sentinel, while real WiFi signals vary. (2) The lazy import used `from app.utils.printer_models` which crashes with `ModuleNotFoundError` in paho-mqtt's background thread (correct path is `backend.app.utils.printer_models`). This killed the MQTT thread entirely, causing all printers to go stale after 60s and repeatedly disconnect/reconnect. (3) WiFi-only models (A1, P1P, etc.) that don't have an ethernet port are excluded via model-based gating. Reported by @cadtoolbox. - **Inventory Usage Tracker Missing External Spool Mapping** ([#677](https://github.com/maziggy/bambuddy/issues/677)) — When all higher-priority slot-to-tray mapping methods failed (MQTT mapping, print command mapping, queue mapping, color matching), the internal inventory usage tracker fell back to `slot_id - 1` which can never reach external spool IDs (254/255) or AMS-HT IDs (128+). Added position-based resolution using sorted available tray IDs from the printer's AMS state, matching the fix applied to Spoolman tracking in #686. Contributed by @shrunbr. - **Spool Assignment Applies Wrong Filament Profile** ([#681](https://github.com/maziggy/bambuddy/issues/681)) — Assigning a spool with a specific filament variant (e.g. "Generic PLA Silk") to an AMS slot applied the base profile instead (e.g. "Generic PLA"). The Bambu Cloud API returns only the base `filament_id` for versioned setting IDs (`GFSL99` → `GFL99`), ignoring variant suffixes (`GFSL99_01`). Added a cross-check that compares the resolved filament name against the spool's stored preset name and corrects the filament ID via reverse lookup when they don't match (e.g. `GFL99` → `GFL96` for "Generic PLA Silk"). Also fixed the UI showing a stale preset name (e.g. "Bambu PLA Matte" instead of "Bambu PLA Silk") after assignment — the slot preset mapping was only saved when assigning via SpoolBuddy, not via the PrintersPage hover card. The backend now saves the slot preset mapping using the spool's authoritative `slicer_filament_name` after every successful MQTT configuration, regardless of which UI path triggered the assignment. Reported by @peter-k-de, @RosdasHH. - **Debug Logging Endpoint 500 Error** — The `GET /api/v1/support/debug-logging` endpoint returned a 500 Internal Server Error when the database contained a timezone-aware timestamp written by a previous version. The duration calculation subtracted a timezone-aware datetime from a naive `datetime.now()`, raising `TypeError`. Now strips timezone info when reading the stored timestamp. - **Bed Cooled Notification Never Fires** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — The bed cooldown monitor always timed out after 30 minutes without sending a notification. After print completion, P1S (and likely other models) sends partial MQTT status updates that don't include `bed_temper`, so the cached bed temperature stayed frozen at the end-of-print value and never dropped below the threshold. The monitor now sends periodic `pushall` commands to the printer to force fresh temperature data. Also added debug logging to the polling loop for future diagnostics. - **Notification Provider Missing Event Toggles on Create** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — When creating a new notification provider, the `on_bed_cooled` toggle and all 7 queue event toggles (`on_queue_job_added`, `on_queue_job_assigned`, `on_queue_job_started`, `on_queue_job_waiting`, `on_queue_job_skipped`, `on_queue_job_failed`, `on_queue_completed`) were silently discarded. The create endpoint manually listed each field but omitted these 8 toggles, so they always defaulted to `false` regardless of user selection. Editing an existing provider worked correctly. - **Clear Plate Prompt Shown for Staged Queue Items** — The "Clear Plate & Start Next" button on the printer card appeared when all pending queue items were staged (`manual_start`/Queue Only), even though the scheduler won't auto-start them. The clear plate prompt now only appears when there are auto-dispatchable items that the scheduler will actually start after the plate is cleared. - **Ethernet Badge Shown on WiFi-Only Printers** ([#585](https://github.com/maziggy/bambuddy/issues/585)) — The printer card network badge always showed "Ethernet" even on printers without an ethernet port. WiFi-only models (A1, P1P, etc.) are now excluded via model-based gating. Reported by @cadtoolbox. - **GitHub Backup Required Cloud Login** ([#655](https://github.com/maziggy/bambuddy/issues/655)) — The GitHub backup settings card was completely blocked behind Bambu Cloud authentication, showing "Bambu Cloud login required" even though the backup feature works without it (K-profiles and app settings don't need cloud). Removed the cloud auth gate so GitHub backup can be configured and used without Bambu Cloud. The "Cloud Profiles" checkbox is disabled with a hint when not logged in. Reported by @TravisWilder. - **GitHub Backup Log Timestamps Off by 1 Hour** — Backup log timestamps in the history table were displayed in UTC instead of the user's local timezone. The local `formatDateTime` function didn't use `parseUTCDate`, so timezone-less timestamps from SQLite were interpreted as local time. Now uses the shared `parseUTCDate` utility for correct UTC-to-local conversion. - **H2D AMS Units Shown on Wrong Nozzle** ([#659](https://github.com/maziggy/bambuddy/issues/659)) — On the H2D dual-nozzle printer, AMS units were displayed on the wrong nozzle (e.g. both AMS-HT and AMS2 Pro shown on the left nozzle instead of their correct assignments). Three interrelated bugs in the AMS `info` field parsing: (1) the field was parsed as decimal instead of hexadecimal (BambuStudio uses `std::stoull(str, nullptr, 16)`), (2) the extruder ID was extracted as a single bit instead of a 4-bit field, and (3) partial MQTT updates overwrote the full extruder map instead of merging. Now correctly hex-parses the `info` field, extracts the 4-bit extruder ID from bits 8-11, skips uninitialized AMS units (`0xE`), and merges partial updates into the existing map. Reported by @cadtoolbox. - **SD Card Error After FTP Upload** ([#645](https://github.com/maziggy/bambuddy/issues/645)) — After printing one file, subsequent prints could fail with `0500-C010 "MicroSD Card read/write exception"` until Bambuddy was restarted. The FTP upload used `transfercmd()` for A1 compatibility but skipped reading the server's 226 "Transfer complete" response, leaving the SD card file write unconfirmed. The print command was sent via MQTT before the printer's FTP server had finished flushing the file to disk. Now waits for the 226 confirmation after each upload (with a 60-second timeout for slower models like H2D). Reported by @lanfi89, confirmed by @Bademeister89. - **P2S Shows Carbon Rod Maintenance Tasks** ([#640](https://github.com/maziggy/bambuddy/issues/640)) — The P2S was incorrectly classified as a carbon rod printer, showing "Lubricate Carbon Rods" and "Clean Carbon Rods" maintenance tasks. The P2S uses hardened steel linear shafts, not carbon fiber rods. Added a new `steel_rod` motion system category and "Lubricate Steel Rods" / "Clean Steel Rods" maintenance tasks specific to the P2S. X1/P1 series continue to show carbon rod tasks; A1/H2 series continue to show linear rail tasks. Reported by @maziggy. - **Dispatch Toast Stuck After Second Print** — The print dispatch progress toast ("Starting prints…") stayed visible forever after the second print dispatch in a session. The dedup guard (`lastDispatchSummaryRef`) that prevents duplicate completion toasts was never reset between batches, so every single-printer dispatch produced the same summary key (`"first-complete:1:0"`). The first print completed normally, but subsequent completions matched the stale ref and skipped creating the done toast — leaving the progress toast stuck in "Processing" state with no way to dismiss except a page reload. Now resets the dedup guard whenever the dispatch toast is dismissed (auto-dismiss timeout, cleanup events) and when a new batch starts. - **Archive Card Buttons Overlapping at Narrow Widths** ([#641](https://github.com/maziggy/bambuddy/issues/641)) — The "Reprint" and "Schedule" buttons at the bottom of archive cards overlapped when the browser window was narrower than the card grid expected (e.g. snapped to half-screen on a 2K monitor). The button text labels used a viewport-based `sm:` breakpoint that didn't account for actual card width. Added `overflow-hidden` to the flex buttons and `truncate` to the text spans so labels clip cleanly with ellipsis instead of bleeding into adjacent buttons. Reported by rsocko@outlook.com, confirmed by @dsmitty166. - **Debug Logging Banner Timer Shows Negative Time** — When enabling debug logging, the banner showed a negative duration (e.g. "-60m -59s") equal to the server's UTC offset. The `enabled_at` timestamp was stored using `datetime.now()` (local time, no timezone indicator), but the frontend interpreted it as UTC. Now stores and compares all debug logging timestamps in UTC. - **Non-Bambu Lab Spools Can't Link/Unlink to Spoolman** ([#653](https://github.com/maziggy/bambuddy/pull/653)) — The "Link to Spoolman" button was not shown for non-Bambu Lab spools (which lack RFID tag UIDs). Now generates a fallback tag from the printer ID, AMS ID, and tray ID for spools without RFID identifiers. Also added an "Unlink from Spoolman" button for non-Bambu spools that are already linked. Contributed by @shrunbr. - **Spoolman Location Not Updated on Link/Unlink** ([#669](https://github.com/maziggy/bambuddy/pull/669)) — Linking a spool to Spoolman did not set the spool's location field. Now sets the Spoolman location to the printer name, AMS name, and slot number (e.g. "P2S-1 - AMS-A 3") when linking, and clears it when unlinking. Contributed by @shrunbr. - **Print Dispatch Toast Disappears Instantly on Fast Uploads** ([#615](https://github.com/maziggy/bambuddy/issues/615)) — When sending a print job, the notification popup disappeared instantly for small files or closed immediately when the progress bar reached 100% for larger files, giving no confirmation that the job was submitted. The dispatch toast now stays visible for 3 seconds after completion, showing a success message (e.g. "1 print started successfully") before auto-dismissing. For very fast uploads where the progress toast was never shown, a fresh confirmation toast is created instead. Reported by @aneopsy. - **Print Modal Shows Busy Printers as Selectable** ([#622](https://github.com/maziggy/bambuddy/issues/622)) — When printing a file from the file manager, the print modal listed all printers including busy ones. Selecting a busy printer resulted in a failed send notification. The printer selector now fetches each printer's live status and shows a state badge (Idle, Printing, Paused, Preparing, Finished, Failed, Offline). In reprint mode, busy printers are grayed out and not selectable. "Select all" also skips busy printers. In queue mode, busy printers remain selectable since the job will wait. Reported by contact@aito3d.fr. - **PWA Install Not Available in Chrome** ([#629](https://github.com/maziggy/bambuddy/issues/629)) — Chrome did not show the PWA install prompt because the manifest icons had incorrect dimensions (e.g. 190px wide declared as 192px) and the manifest was missing the `screenshots` entries required for Chrome's richer install UI. Resized all three icons (`android-chrome-192x192.png`, `android-chrome-512x512.png`, `apple-touch-icon.png`) to their declared sizes, split the discouraged `"any maskable"` purpose into a dedicated `"maskable"` entry, and added mobile and desktop screenshots to the manifest. Reported by @SebSeifert. - **Project Statistics Count Archived Files as Printed** ([#630](https://github.com/maziggy/bambuddy/issues/630)) — Files added to a project from the archive were counted in project statistics (completed prints, parts progress) as if they had already been printed. Only files with `status="completed"` (actually printed via a printer) now count toward completion stats. Files with `status="archived"` (stored but not yet printed) are no longer included. Reported by @SebSeifert. - **Python 3.10 Compatibility** — Bambuddy failed to start on Python 3.10 with `ImportError: cannot import name 'StrEnum' from 'enum'` because `enum.StrEnum` was added in Python 3.11. Added a compatibility shim that falls back to `(str, Enum)` on Python < 3.11, matching the documented requirement of Python 3.10+. - **Bug Report Bubble Overlapping Toasts** — Moved toast notifications and upload progress up so they stack above the bug report bubble instead of overlapping on top of each other. - **Virtual Printer: Bind-TLS Proxy Handshake Failure on OpenSSL 3.x** — The TLS proxy connecting to the printer's bind port (3002) failed with `SSLV3_ALERT_HANDSHAKE_FAILURE` on systems with OpenSSL 3.x (e.g. Python 3.12+) because the default cipher set excludes plain RSA key exchange, which is the only mode Bambu printers support. Added `AES256-GCM-SHA384` and `AES128-GCM-SHA256` to the client SSL context's cipher list. - **Windows: Server Shuts Down After 60 Seconds** ([#605](https://github.com/maziggy/bambuddy/issues/605)) — On Windows, terminating orphaned ffmpeg camera processes broadcast `CTRL_C_EVENT` to the entire process group, causing uvicorn to interpret it as a user-initiated shutdown. ffmpeg is now spawned in its own process group (`CREATE_NEW_PROCESS_GROUP`) so cleanup no longer affects the server. Reported by @Reactantvr. - **Multi-Printer Filament Mapping Shows Wrong Nozzle Filaments on Dual-Nozzle Printers** ([#624](https://github.com/maziggy/bambuddy/issues/624)) — When selecting multiple printers for a print job on dual-nozzle printers (H2D), the per-printer filament mapping override dropdown showed filaments from both nozzles instead of only the correct nozzle for each slot. The single-printer filament mapping (FilamentMapping.tsx) was fixed in v0.2.1 to filter by `nozzle_id`, but the multi-printer path (InlineMappingEditor in PrinterSelector.tsx) was missed. Both the auto-match logic and the dropdown options now filter by `nozzle_id`, matching the single-printer behavior. Reported by @cadtoolbox. - **Filament Mapping Dropdowns Missing Subtypes** ([#624](https://github.com/maziggy/bambuddy/issues/624)) — All filament mapping dropdowns (single-printer, multi-printer, and "Print to Any" model-based assignment) showed only the base material type (e.g., "PLA") without the subtype (e.g., "PLA Basic", "PLA Matte"). This made it impossible to distinguish between different filament variants of the same color. Now shows `tray_sub_brands` (e.g., "PLA Basic", "PLA Matte", "PETG HF") in all filament dropdowns, falling back to the base type when no subtype is set. The backend's available-filaments endpoint also includes `tray_sub_brands` in the dedup key, so "PLA Basic Black" and "PLA Matte Black" appear as separate entries instead of collapsing into duplicate "PLA (Black)" rows. Reported by @cadtoolbox. - **Archive Card Shows "Source" Badge for Sliced .3mf Files** — Archive cards created from prints showed a "SOURCE" badge instead of "GCODE" when the filename was a plain `.3mf` (without `.gcode` in the name). The `isSlicedFile()` check only matched `.gcode` or `.gcode.3mf` extensions, but `.3mf` files can be either sliced (contains gcode) or raw source models. Now checks the archive's `total_layers` and `print_time_seconds` metadata — if either is present, the file is sliced. Also passes the original human-readable filename when creating archives from the file manager print flow (previously stored the UUID library filename). - **AMS Slot Shows Wrong Material for "Support for" Profiles** — Configuring an AMS slot with a filament profile like "PLA Support for PETG PETG Basic @Bambu Lab H2D 0.4 nozzle" set the slot material to PLA instead of PETG. The name parser iterated material types in order and returned the first match ("PLA"), ignoring that "PLA Support for PETG" means the filament type is PETG. Both the frontend `parsePresetName()` and backend `_parse_material_from_name()` now detect the "X Support for Y" naming pattern and extract the material after "Support for". The frontend also prefers the corrected parsed material over the stored `filament_type` (which may have been saved with the old parser during import). - **Firmware Check Shows Wrong Version for H2D Pro** ([#584](https://github.com/maziggy/bambuddy/issues/584)) — H2D Pro printers showed firmware as out of date because the firmware check matched against the H2D firmware track instead of the H2D Pro track. The firmware check's model-to-API-key mapping only had display names (e.g., "H2D", "H2D Pro") but not SSDP device codes (e.g., "O1E", "O2D"). Added all known SSDP model codes to the firmware check mapping so raw device codes resolve to the correct firmware track. - **Spurious Error Notifications During Normal Printing (0300_0002)** — Some firmware versions send non-zero `print_error` values in MQTT during normal printing (e.g., `0x03000002` → short code `0300_0002`). The `print_error` parser treated any non-zero value as a real error, appending it to `hms_errors` and triggering notifications — even though the printer was printing fine. All known real HMS error codes have their low 16 bits >= `0x4000` (`0x4xxx` = fatal, `0x8xxx` = warning/pause, `0xCxxx` = prompt). Values below `0x4000` are status/phase indicators, not faults. Now skips values where the error portion is below `0x4000` in both the `print_error` and `hms` array parsers. - **Spool Auto-Assign Fails With Greenlet Error** ([#612](https://github.com/maziggy/bambuddy/issues/612)) — RFID spool auto-assignment logged `WARNING greenlet_spawn has not been called; can't call await_only() here` and silently failed. The `Spool.assignments` relationship was never eagerly loaded: when `auto_assign_spool()` created a new `SpoolAssignment` and called `db.add()`, SQLAlchemy resolved the FK back-populates synchronously (outside the async greenlet), triggering a lazy load on the uninitialized `spool.assignments` collection. The previous fix only covered `spool.k_profiles`. Now also initializes `spool.assignments = []` on newly created spools in `create_spool_from_tray()`, and adds `selectinload(Spool.assignments)` to both queries in `get_spool_by_tag()` for existing spools. Added `exc_info=True` to the error handlers for full tracebacks in future logs. - **SpoolBuddy Link Tag Missing tag_type** — Linking an NFC tag to a spool via the SpoolBuddy dashboard's "Link to Spool" action only set `tag_uid` but left `tag_type` and `data_origin` empty, because it called the generic `updateSpool` API instead of the dedicated `linkTagToSpool` endpoint. The printer card's `LinkSpoolModal` already used `linkTagToSpool` correctly. Now uses `linkTagToSpool` with `tag_type: 'generic'` and `data_origin: 'nfc_link'`, which also handles conflict checks and archived tag recycling. - **SpoolBuddy AMS Page Missing Fill Levels for Non-BL Spools** — AMS slots with non-Bambu Lab spools assigned to inventory didn't show fill level bars on the SpoolBuddy AMS page, even though the main printer card displayed them correctly. The SpoolBuddy AMS page only used the MQTT `remain` field (which is -1/unknown for non-BL spools), while the printer card had a fallback chain: Spoolman → inventory → AMS remain. Now fetches inventory spool assignments and computes fill levels from `(label_weight - weight_used) / label_weight`, falling back to AMS remain when no inventory assignment exists. - **SpoolBuddy AMS Page Ext-R Slot Falsely Shown as Active When Idle** — On dual-nozzle printers (H2D), the Ext-R slot was incorrectly highlighted as active when the printer was idle. The ext-R tray has `id=255`, and the idle sentinel `tray_now=255` matched it via `trayNow === extTrayId`. The main printer card avoided this by clearing `effectiveTrayNow` to `undefined` when `tray_now=255`. Now guards against `tray_now=255` before any ext slot active check. - **Printer Card Loses Info When Print Is Paused** ([#562](https://github.com/maziggy/bambuddy/issues/562)) — When a print was paused (via G-code pause command or user action), the printer card showed the print as finished — the progress bar, print name, ETA, layer count, and cover image all disappeared, replaced by the idle "Ready to Print" placeholder. The display conditions only checked for `state === 'RUNNING'` but not `'PAUSE'`, even though other parts of the same page (Skip Objects button, Stop/Resume controls) already handled both states correctly. Now shows print progress info for both `RUNNING` and `PAUSE` states, and the status label correctly reads "Paused" instead of the hardcoded "Printing" fallback. - **SpoolBuddy "Assign to AMS" Slot Shows Empty Fields in Slicer** — After assigning a spool to an AMS slot via SpoolBuddy's "Assign to AMS" button, the slicer's slot overview showed the correct filament, but opening the slot detail card showed all fields empty/unselected. Two bugs: (1) the `assign_spool` backend called the cloud API with the raw `slicer_filament` value including its version suffix (e.g., `PFUS9ac902733670a9_07`), which returned a 404; the silent fallback sent the `setting_id` as `tray_info_idx` instead of the real `filament_id` (e.g., `PFUS9ac902733670a9` instead of `P4d64437`), and the slicer couldn't resolve the preset; (2) no `SlotPresetMapping` was saved, so Bambuddy's own ConfigureAmsSlotModal couldn't identify the active preset when reopened. Now strips version suffixes before the cloud lookup, resolves the real `filament_id` via the cloud API (with local preset and generic ID fallbacks), includes the brand name in `tray_sub_brands`, and saves the slot preset mapping from the frontend after assignment. - **Virtual Printer Bind Server Fails With TLS-Enabled Slicers** ([#559](https://github.com/maziggy/bambuddy/issues/559)) — BambuStudio uses TLS on port 3002 for certain printer models (e.g. A1 Mini / N1), but the bind server only spoke plain TCP on both ports 3000 and 3002. The slicer's TLS ClientHello was rejected as an "invalid frame", preventing discovery and connection entirely. Port 3002 now uses TLS (using the VP's existing certificate), while port 3000 remains plain TCP for backwards compatibility. The proxy-mode bind proxy was also updated to use TLS termination on port 3002. - **Queue Returns 500 When Cancelled Print Exists** ([#558](https://github.com/maziggy/bambuddy/issues/558)) — When a print was cancelled mid-print, the MQTT completion handler stored status `"aborted"` on the queue item, but the response schema only accepts `"pending"`, `"printing"`, `"completed"`, `"failed"`, `"skipped"`, or `"cancelled"`. Listing all queue items hit a Pydantic validation error on the invalid status, returning a 500 error. Filtering by a specific status (e.g. "pending") excluded the bad row and worked fine. Now normalises `"aborted"` to `"cancelled"` before storing. A startup fixup also converts any existing `"aborted"` rows. - **Tests Send Real Maintenance Notifications** — Tests that call `on_print_complete(status="completed")` created background `asyncio` tasks (maintenance check, smart plug, notifications) that outlived the test's mock context. When the event loop processed these orphaned tasks, `async_session` was no longer patched and they queried the real production database — finding real printers with maintenance due and real notification providers, then sending real notifications. Tests now cancel spawned background tasks before the mock context exits. - **Virtual Printer Config Changes Ignored Until Toggle Off/On** — Changing a virtual printer's mode (e.g. proxy → archive), model, access code, bind IP, remote interface IP, or target printer via the UI updated the database but the running VP instance was never restarted. `sync_from_db()` skipped any VP whose ID was already in the running instances dict without checking if config had changed. Now compares critical fields between the running instance and DB record and restarts the VP when a difference is detected. - **Sidebar Navigation Ignores User Permissions** — All sidebar navigation items (Archives, Queue, Stats, Profiles, Maintenance, Projects, Inventory, Files) were visible to every user regardless of their role's permissions. Only the Settings item was permission-gated. Now each nav item is hidden when the user lacks the corresponding read permission (e.g., `archives:read`, `queue:read`, `library:read`). The Printers item remains always visible as the home page. Also added the missing `inventory:read|create|update|delete` permissions to the frontend Permission type (they existed in the backend but were absent from the frontend type definition). - **Camera Button Clickable Without Permission & ffmpeg Process Leak** ([#550](https://github.com/maziggy/bambuddy/issues/550)) — Two camera issues in multi-user environments (e.g., classrooms with multiple printers). First, the camera button on the printer card was clickable even when the user's role lacked `camera:view` permission. Now disabled with a permission tooltip, matching the existing pattern for `printers:control` on the chamber light button. Second, ffmpeg processes (~240MB each) were never cleaned up after closing a camera stream. The `stop_camera_stream` endpoint called `terminate()` but never `wait()`ed or `kill()`ed, and HTTP disconnect detection in the streaming response only checked between frames — if the generator was blocked reading from ffmpeg stdout, disconnect was never detected (due to TCP send buffer masking the closed connection). Three fixes: (1) the stop endpoint now uses `terminate()` → `wait(2s)` → `kill()` → `wait()`; (2) each stream gets a background disconnect monitor task that polls `request.is_disconnected()` every 2 seconds independently of the frame loop, directly killing the ffmpeg process on disconnect; (3) a periodic cleanup (every 60s) scans `/proc` for any ffmpeg process with a Bambu RTSP URL (`rtsps://bblp:`) that isn't in an active stream and `SIGKILL`s it — catching orphans that survive app restarts or generator abandonment. - **Windows Install Fails With "Syntax of the Command Is Incorrect"** ([#544](https://github.com/maziggy/bambuddy/issues/544)) — The `start_bambuddy.bat` Python hash verification used a multi-line `for /f "usebackq"` with a backtick-delimited command split across lines. Windows CMD cannot parse line breaks inside backtick-delimited `for /f` commands, causing "The syntax of the command is incorrect" immediately after downloading Python. The entire block was also redundant — it downloaded a separate checksum file from python.org and re-verified the hash, but `verify_sha256` had already checked the archive against the pinned hash on the previous line. Removed the duplicate verification block. Also had a secondary bug: always downloaded the `amd64` checksum even on `arm64` systems. - **Queue Badge Shows on Incompatible Printers** ([#486](https://github.com/maziggy/bambuddy/issues/486)) — The purple queue counter badge in the printer card header showed on all printers of the same model when a job was scheduled for "any [model]", even if the printer didn't have the matching filament color loaded. The `PrinterQueueWidget` (which shows "Clear Plate & Start") already filtered by filament type and color, but the badge count used the raw unfiltered queue length. Now applies the same filament compatibility filter to the badge count. - **SpoolBuddy Daemon Can't Find Hardware Drivers** — The daemon's `nfc_reader.py` and `scale_reader.py` import `read_tag` and `scale_diag` as bare modules, but these files live in `spoolbuddy/scripts/` which isn't on Python's module search path. The systemd service sets `WorkingDirectory` to `spoolbuddy/` and runs `python -m daemon.main`, so only the `spoolbuddy/` and `daemon/` directories are on `sys.path`. Added `scripts/` to `sys.path` at daemon startup, resolved relative to the module file so it works regardless of install path. Also moved the `read_tag` import inside `NFCReader.__init__`'s try/except block — it was previously outside, so a missing module crashed the entire daemon instead of gracefully skipping NFC polling. Demoted hardware-not-available log messages from ERROR to INFO since missing modules are expected when hardware isn't connected. - **SpoolBuddy Scale Tare & Calibration Not Applied** — The SpoolBuddy scale tare and calibrate buttons on the Settings page queued commands but never executed them. Five bugs in the chain: (1) the daemon received the `tare` command via heartbeat but never called `scale.tare()` — a comment said "need cross-task communication" but the ScaleReader was already available in the shared dict; (2) no API endpoint existed for the daemon to report the new tare offset back to the backend database, so tare results were lost; (3) when calibration values changed in heartbeat responses, the daemon updated its config object but never called `scale.update_calibration()`, so the ScaleReader kept using its initial values forever; (4) the heartbeat response that delivered the tare command still contained pre-tare calibration values, which immediately overwrote the new tare offset back to zero; (5) the `set-factor` endpoint computed `calibration_factor` using the DB `tare_offset`, which could be stale or zero if the tare hadn't persisted yet — producing a wildly wrong factor (e.g., 5000g displayed with empty scale). Added a `POST /devices/{device_id}/calibration/set-tare` endpoint and `update_tare()` API client method. The heartbeat loop now executes `scale.tare()` when the tare command is received, persists the result via the new endpoint, propagates calibration changes to the ScaleReader instance, and skips calibration sync on the heartbeat cycle that delivers a tare command. The calibration flow now captures the raw ADC at tare time and sends it alongside the loaded-weight ADC in step 2, so the factor is computed from the actual tare reference rather than the DB value — making calibration self-contained and independent of the tare persistence round-trip. The calibration weight input uses a compact touch-friendly numpad since the RPi kiosk has no physical keyboard. - **A1 Mini Shows "Unknown" Status After MQTT Payload Decode Failure** ([#549](https://github.com/maziggy/bambuddy/issues/549)) — Some printer firmware versions (observed on A1 Mini 01.07.02.00) occasionally send MQTT payloads containing non-UTF-8 bytes. The `_on_message` handler called `msg.payload.decode()` (strict UTF-8), and the resulting `UnicodeDecodeError` was not caught — only `json.JSONDecodeError` was handled. The entire message was silently dropped, causing printer status to show "unknown", temperatures to read 0°C, and AMS data to disappear. Now catches `UnicodeDecodeError` and falls back to `decode(errors="replace")`, which substitutes invalid bytes with U+FFFD while keeping the JSON structure intact. Logs a warning for diagnostics. - **H2C Dual Nozzle Variant (O1C2) Not Recognized** ([#489](https://github.com/maziggy/bambuddy/issues/489)) — The H2C dual nozzle variant reports model code `O1C2` via MQTT, but only `O1C` was in the recognized model maps. This caused the camera to use the wrong protocol (chamber image on port 6000 instead of RTSP on port 322) — the printer immediately closed the connection, producing a reconnect loop. Also affected model display names, chamber temperature support detection, linear rail classification, and virtual printer model mapping. Added `O1C2` to all model ID maps across backend and frontend. - **Support Package Leaks Full Subnet IPs and Misdetects Docker Network Mode** — Three support package fixes. First, the network section included full subnet addresses (e.g., `192.168.192.0/24`); now masks the first two octets (`x.x.192.0/24`). Second, `network_mode_hint` used `len(interfaces) > 2` which always reported "bridge" on single-NIC hosts even with `network_mode: host`, because `get_network_interfaces()` excludes Docker infrastructure interfaces. Now checks for the presence of Docker interfaces (`docker0`, `br-*`, `veth*`) via `socket.if_nameindex()` — these are only visible when the container shares the host network namespace. Third, `developer_mode` was still null for most users because the MQTT `fun` field was only parsed inside the `print` key; some firmware versions send it at the top level of the payload. Now also checks top-level `fun`. Also added a `virtual_printers` section with mode, model, enabled/running status, and pending file count for each configured virtual printer. - **SpoolBuddy Scale Calibration Lost After Reboot** — The SpoolBuddy daemon generated its device ID from the MAC address of whichever network interface `Path.iterdir()` returned first, but filesystem iteration order is non-deterministic. On different boots, the daemon could pick `eth0` (MAC ending `3100`) or `wlan0` (MAC ending `3102`), producing a different `device_id` each time. Since calibration values (`tare_offset`, `calibration_factor`) are stored per device ID in the backend database, a new ID meant registering as a brand-new uncalibrated device. Fixed by sorting network interfaces alphabetically before selection, ensuring the same interface (and thus the same device ID) is always chosen. - **SpoolBuddy NFC Reader Fails to Detect Tags** — The PN5180 NFC reader had two polling issues. First, each `activate_type_a()` call that returned `None` (no tag) corrupted the PN5180 transceive state — subsequent calls silently failed even when a tag was physically present, making it impossible to detect tags placed after startup (only tags already on the reader during init were detected). Fixed by performing a full hardware reset (RST pin toggle + RF re-init, ~240ms) before every idle poll, giving a ~1.8 Hz effective poll rate. Second, after a successful SELECT the card stayed in ACTIVE state and ignored subsequent WUPA/REQA, causing false "tag removed" events after ~1 second. Fixed with a light RF off/on cycle (13ms) before each poll when a tag is present, resetting the card to IDLE for re-selection. Also added error-based auto-recovery (full hardware reset after 10 consecutive poll exceptions), periodic status logging every 60 seconds, and accurate heartbeat reporting of NFC/scale health. ### Changed - **CI: Node.js 20 → 22** — Updated GitHub Actions workflows (`ci.yml`, `security.yml`) from Node.js 20 to Node.js 22 LTS ahead of [GitHub's Node 20 deprecation](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/). - **Daily Builds Falsely Trigger Update Notification** — The version parser misclassified daily build tags (e.g. `0.2.2b4-daily.20260313`) as full releases instead of betas, because the `-daily.YYYYMMDD` suffix pushed the last dot-segment to a pure number (`20260313`), bypassing the prerelease detection. Users running the same beta version saw a spurious "update available" notification after each daily build. Now strips the daily suffix before parsing. - **License changed from MIT to AGPL-3.0** — To prevent unauthorized redistribution of Bambuddy as a closed-source product. All existing contributions were made under MIT, which is forward-compatible with AGPL-3.0. Community contributions and usage are unaffected. - **License changed from MIT to AGPL-3.0** — To prevent unauthorized redistribution of Bambuddy as a closed-source product. All existing contributions were made under MIT, which is forward-compatible with AGPL-3.0. Community contributions and usage are unaffected. ### Improved - **Shorter Inventory Location Labels** — The location column in the Inventory table now shows compact labels like "H2D-1 B3" instead of "H2D-1 AMS-B Slot 3". External spool holders show "Ext" instead of "External". AMS-HT labels remain unchanged ("HT-A"). - **Higher FTP Timeout Options for Large Files** ([#660](https://github.com/maziggy/bambuddy/issues/660)) — Added 180s and 300s FTP timeout options in Settings. The previous maximum of 120s was insufficient for large 3MF files (e.g. 28 MB Hueforge models) which can't be downloaded from the printer's FTP server within 2 minutes, especially during active printing. Reported by @PasDoe. - **Separate Permission for AMS Spool Assignments** ([#635](https://github.com/maziggy/bambuddy/issues/635)) — Added a new `inventory:view_assignments` permission that controls whether spool-to-AMS-slot assignment data is visible on the Printers page. Previously, viewing spool assignments on printer cards required `inventory:read`, which also exposed the full Inventory page in the sidebar. Admins can now grant `inventory:view_assignments` without `inventory:read` so users can see what's loaded in the AMS without accessing the full spool inventory. All default groups (Administrators, Operators, Viewers) include the new permission automatically. Also fixed multi-word permission labels in the group editor (e.g. "Update_Own" → "Update Own"). Reported by @Minebuddy. - **Prometheus Build Info Metric** ([#633](https://github.com/maziggy/bambuddy/pull/633)) — Added a `bambuddy_build_info` gauge metric to the Prometheus metrics endpoint, exposing the application version, Python version, platform, and architecture as labels. Follows the standard Prometheus `_build_info` convention for dashboards and version-change alerting. Contributed by @sw1nn. - **i18n: Settings, Smart Plugs, Notifications, Backup/Restore** — Replaced all hardcoded English strings with translation keys (`t()` calls) across the Settings page, Smart Plug components (SmartPlugCard, AddSmartPlugModal, SwitchbarPopover), Notification components (NotificationProviderCard, AddNotificationModal, NotificationTemplateEditor, NotificationLogViewer), and Backup/Restore components (GitHubBackupSettings, RestoreModal). Added ~600 new translation keys to all 7 supported locales (en, de, ja, fr, it, pt-BR, zh-CN). Removed hardcoded label maps (`PROVIDER_LABELS`, `EVENT_LABELS`, `CATEGORY_LABELS`) in favor of dynamic translation key lookups with fallbacks. - **Install Script: Branch Selection** — The native install script (`install.sh`) now supports a `--branch` option and an interactive branch prompt (defaults to `main`). Previously the script hardcoded `origin/main`, so beta testers told to install from a beta branch would silently get the stable release instead. Fresh installs use `git clone --branch`, existing installs checkout and reset to the selected branch. The install summary highlights non-main branches in yellow with a "(beta)" label. Invalid branch names are caught early with an error message listing available branches. - **Print Queue Scheduler Diagnostics** ([#616](https://github.com/maziggy/bambuddy/issues/616)) — Added diagnostic logging to the print queue scheduler to help diagnose why queued prints aren't starting. After each queue check, the scheduler now logs a skip summary (how many items were skipped due to manual_start, scheduled_time, etc.) and for each busy printer, logs the exact state preventing it from being considered idle (connected status, printer state, plate_cleared flag). Previously the scheduler only logged "found N pending items" with no visibility into why items were skipped. - **SpoolBuddy Settings Page Redesign** — Redesigned the SpoolBuddy settings page with a tabbed layout (Device, Display, Scale, Updates). The Device tab shows an About section, NFC reader info (type, connection, status), device info (host, IP, uptime, online status), and device ID. The Display tab has a brightness slider (CSS software filter for HDMI displays) and screen blank timeout selector (Off, 1m, 2m, 5m, 10m, 30m) — the screen blanks after user inactivity (no touch) and wakes on tap. The Scale tab shows live weight with a step-indicator calibration wizard (tare → place known weight → calibrate). The Updates tab shows the daemon version and checks for updates against GitHub releases with optional beta inclusion. Display settings (brightness + blank timeout) are stored per-device in the backend and applied instantly in the frontend layout via outlet context. - **SpoolBuddy Language & Time Format Support** — The SpoolBuddy kiosk now respects Bambuddy's configured UI language and time format. Added a `language` field to backend app settings so the UI language is persisted server-side (previously only stored in browser localStorage, inaccessible to the kiosk's separate Chromium instance). The SpoolBuddy layout fetches settings on load and syncs `i18n.changeLanguage()`. The top bar clock uses `formatTimeOnly()` with the user's time format setting (system/12h/24h). Added full SpoolBuddy settings translations for all 6 supported languages (English, German, French, Japanese, Italian, Portuguese). - **SpoolBuddy Kiosk Stability** — Disabled Chromium's swipe-to-navigate gesture (`--overscroll-history-navigation=0`) in the install script to prevent accidental back-navigation on the touchscreen. Added the `video` group to the SpoolBuddy system user for DSI backlight access. - **SpoolBuddy Touch-Friendly UI** — Enlarged all interactive elements across the SpoolBuddy kiosk UI for comfortable finger use on the 1024×600 RPi touchscreen. Bottom nav icons and labels increased (20→24px icons, 10→12px labels, 48→56px bar height). Top bar printer selector and clock enlarged. Dashboard stats bar compacted, printers card removed (printer selection via top bar is sufficient), section headers and device status text bumped up. AMS page single-slot cards, spool visualizations, and fill bars enlarged. AMS unit cards get larger spool previews (56→64px), bigger material/slot text, and larger humidity/temperature indicators. Inventory spool cards, settings page headers, and calibration inputs all sized up to meet 44px minimum tap targets. The AMS slot configuration modal now renders in a two-column full-screen layout on the kiosk display (filament list on left, K-profile and color picker on right) instead of the standard centered dialog, eliminating scrolling. - **Ethernet Connection Indicator** ([#585](https://github.com/maziggy/bambuddy/issues/585)) — Printers connected via ethernet now show a green "Ethernet" badge with a cable icon instead of the WiFi signal strength indicator. Detected via `home_flag` bit 18 from the printer's MQTT data. The printer info modal also shows "Ethernet" instead of WiFi signal details. - **SpoolBuddy AMS Page Single-Slot Card Layout** — AMS-HT and external spool cards on the SpoolBuddy AMS page now use a responsive grid (2 cards per AMS card width) instead of auto-sized flex items, so they align with the regular AMS card columns above. Regular AMS cards no longer stretch vertically to fill available space on printers with fewer AMS units. - **SpoolBuddy Scale Value Stabilization** — The SpoolBuddy daemon now suppresses redundant scale weight reports: only sends updates when the weight changes by ≥2g. Previously every 1-second report interval sent a reading regardless of change, and stability state flips (stable ↔ unstable) also triggered reports — when ADC noise kept the spread hovering around the 2g stability threshold, the flag toggled every cycle, forcing a report with a slightly different weight each time. Removed stability flipping as a report trigger (the stable flag is still included in each report for consumers). Also increased the NAU7802 moving average window from 5 to 20 samples (500ms → 2s) to smooth ADC noise. The frontend also applies a 3g display threshold as defense-in-depth. - **SpoolBuddy TopBar: Online Printer Selection** — The printer selector in the SpoolBuddy top bar now only shows online printers and auto-selects the first online printer. If the currently selected printer goes offline, it automatically switches to the next available online printer. Also replaced the placeholder icon with the SpoolBuddy logo. Renamed the connection status label from "Online" to "Backend" for clarity. - **SpoolBuddy Assign to AMS Redesign** — The "Assign to AMS" sub-modal (opened from the spool card) is now a full-screen overlay that reuses the `AmsUnitCard` component from the AMS page. Regular AMS units display in a 2-column grid with the same spool visualization, fill bars, and material labels. AMS-HT and external slots (Ext / Ext-L / Ext-R on dual-nozzle printers) appear in a compact horizontal row below. Clicking any slot auto-configures the filament via a single `assignSpool` API call — the backend handles both the DB assignment and MQTT configuration. The printer selector was removed from the modal since the top bar already provides printer selection. Dual-nozzle printers show L/R nozzle badges on each AMS unit. - **Filament ID Conversion Utility** — Extracted filament_id ↔ setting_id conversion logic into a shared utility (`backend/app/utils/filament_ids.py`). The `assign_spool` endpoint now normalizes `slicer_filament` (which can be stored in either filament_id format like "GFL05" or setting_id format like "GFSL05_07") into the correct `tray_info_idx` and `setting_id` for the MQTT command. Previously `setting_id` was always sent as empty string, which could cause BambuStudio to not resolve the filament preset for the AMS slot. - **Updates Card Separates Firmware and Software Settings** — The Updates card on the Settings page mixed printer firmware and Bambuddy software update toggles with no visual grouping. Now splits the card into two labeled sections ("Printer Firmware" and "Bambuddy Software") separated by a divider, making it clear which toggles control what. - **SpoolBuddy Test Coverage** — Added integration tests for all 12 SpoolBuddy API endpoints (21 backend tests covering device registration/re-registration, heartbeat status and pending commands, NFC tag scan/match/removal, scale reading broadcast, spool weight calculation, and scale calibration including tare, set-factor, and zero-delta error handling) and component tests for the three main SpoolBuddy frontend components (20 frontend tests covering WeightDisplay weight formatting and status indicators, SpoolInfoCard spool info rendering and action callbacks, UnknownTagCard tag display, and TagDetectedModal open/close/escape behavior with known and unknown spool views). - **Cleanup Obsolete Settings** — The startup migration now deletes orphaned settings keys from the database that are no longer used by the application (e.g., `slicer_binary_path` from earlier slicer integration research). - **Added HUF Currency** ([#579](https://github.com/maziggy/bambuddy/issues/579)) — Added Hungarian Forint (HUF, Ft) to the supported currencies list for filament cost tracking. - **FTP Upload Progress & Speed** — Reduced FTP upload chunk size from 1MB to 64KB for smoother progress reporting — at typical printer FTP speeds (~50-100KB/s) the progress bar now updates roughly every second instead of appearing stuck for 20+ seconds between jumps. Removed the post-upload `voidresp()` wait for all printer models (previously only skipped for A1); H2D printers delay the FTP 226 acknowledgment by 30+ seconds after data transfer completes, causing a long hang at 100%. The data is already on the SD card once the transfer finishes. Also added transfer speed logging (KB/s) and PASV+TLS handshake timing to help diagnose slow connections. - **Wider Print & Schedule Modals** — Increased the Print and Schedule Print modal width from 512px to 672px to better accommodate long filament profile names (e.g., "PLA Support for PETG PETG Basic @Bambu Lab H2D 0.4 nozzle"). ### Security - **Stored XSS via Project Notes** — Project notes were rendered with `dangerouslySetInnerHTML` without sanitization, allowing injected `