# Changelog
All notable changes to Bambuddy will be documented in this file.
## [0.2.3b3] - Unreleased
### Improved
- **AMS Drying Support for P2S** — Remote AMS drying and queue auto-drying now work on P2S printers with firmware 01.02.00.00 or later. Previously P2S was hard-blocked from the drying feature.
### New Features
- **LDAP Default Fallback Group** — Settings → Authentication → LDAP → Advanced now has a "Default group" selector. When an LDAP user authenticates but is not listed in any mapped LDAP group, they are automatically assigned to this fallback group instead of being left without permissions. Previously such users could log in successfully but landed on empty pages because every permission check failed. Leave the setting empty to preserve the old behavior. A warning is logged each time the fallback is applied so administrators can spot missing group assignments.
### Fixed
- **External Sidebar Link Icon Not Showing** ([#878](https://github.com/maziggy/bambuddy/issues/878)) — Custom icons uploaded for external sidebar links rendered correctly in the edit dialog but were missing from the sidebar itself, and opening the icon URL directly returned `{"detail":"Valid camera stream token required..."}`. The sidebar `
` tag in `Layout.tsx` used a raw `/api/v1/external-links/{id}/icon` URL, but that endpoint is protected by a query-string stream token (the same mechanism used for camera streams and archive thumbnails, because `
` tags cannot send Authorization headers). The edit dialog already routed through `api.getExternalLinkIconUrl()`, which wraps the URL via `withStreamToken()`; the sidebar now does the same, so icons appear when auth is enabled.
- **Shortest Job First Toggle Disappears After Clicking** ([#879](https://github.com/maziggy/bambuddy/issues/879)) — The SJF toggle badge on the queue page was rendered inside the Pending Queue section header, which is only shown when there is at least one pending item and the list view is active. Clicking the toggle often coincided with the scheduler starting the only pending print, at which point the Pending section unmounted and the toggle vanished along with it — making it look like the button had disappeared after clicking. The toggle has been moved to the top of the queue page, next to the list/timeline view switcher, so it stays reachable regardless of pending-item count, active filters, or the selected view mode.
- **SpoolBuddy Update Fails in Docker with "no user exists for uid 1000"** — The SpoolBuddy remote-update flow shelled out to the OpenSSH `ssh-keygen` and `ssh` binaries for keypair creation and command execution. Both of those binaries call `getpwuid(getuid())` during startup and abort with `No user exists for uid ` when the container runs under an arbitrary PUID that is not listed in `/etc/passwd` (the default `python:3.13-slim` image only has an entry for root, so running with `user: "1000:1000"` — or any non-root user — tripped the same error). The entire SpoolBuddy update path is now subprocess-free: keypairs are generated in-process via the `cryptography` library (already a dependency), SSH commands run through the pure-Python `asyncssh` client, and git-branch detection reads `.git/HEAD` directly instead of shelling out to `git`. Native installs behave identically — they already worked because the running user was always in `/etc/passwd`. A regression test asserts that neither keypair creation nor command execution spawns any subprocess.
- **Camera Stream "6 of 5" Reconnect Counter + ffmpeg Log Flood** ([#925](https://github.com/maziggy/bambuddy/issues/925)) — Two bugs surfaced while investigating camera reconnect behaviour. First, the camera page briefly displayed "Reconnecting attempt 6 of 5" before giving up, because the attempt counter could be incremented to the maximum while the reconnect banner was still rendering. The displayed value is now clamped to the configured maximum. Second, every failed ffmpeg spawn logged the full ~20-line ffmpeg version/configuration banner, producing hundreds of lines of noise per failed camera click (one reported click produced 555 log lines across 30 retries). A new stderr summarizer strips the ffmpeg banner before logging so only the actual error lines remain. The underlying "camera service stops accepting new connections after prolonged uptime" behaviour in the X1C firmware is still under investigation.
- **LDAP POSIX Primary Group Ignored** — LDAP authentication only looked at groups that listed the user explicitly via `memberUid` (supplementary group membership). A user's POSIX primary group — referenced by the `gidNumber` attribute on the user object and matching the `gidNumber` on a `posixGroup` — was ignored entirely, so users whose role came from their primary group landed without the expected permissions. The authenticator now also searches for `posixGroup` entries whose `gidNumber` matches the user's primary `gidNumber`, and dedupes DNs case-insensitively before resolving the group mapping (LDAP DNs are case-insensitive by spec).
- **Support Bundle Leaks Virtual Printer IP Address** — The debug support bundle included the `virtual_printer_remote_interface_ip` setting value unmasked in `support-info.json`. The setting key didn't match any of the existing sensitive-key filters, so the raw IP address was included in the bundle. Added `_ip` to the sensitive key filter so IP address settings are excluded from support bundles. Log file content was already covered by the existing IPv4 regex redaction.
- **"Build Plate Cleared" Button Unclickable After Second Print** ([#912](https://github.com/maziggy/bambuddy/issues/912)) — After completing the first queued print and confirming the plate was cleared, the "Build plate cleared — ready for next print" button became unresponsive after the second print finished. The React Query mutation's `isSuccess` state persisted from the first plate-clear confirmation, causing the component to render the static "Plate Ready" confirmation instead of the clickable button. The mutation state is now reset when the printer leaves the FINISH/FAILED state, so the button works correctly on every print cycle.
- **Spoolman Location Not Cleared When Spool Removed from AMS** ([#921](https://github.com/maziggy/bambuddy/issues/921)) — When Spoolman auto-sync was enabled and a spool was removed from an AMS slot, its location in Spoolman was never cleared, causing "double-booked" slots where multiple spools shared the same location. The auto-sync callback set locations for newly inserted spools but skipped the cleanup step that clears stale locations. The location clearing logic now runs after every auto-sync cycle. Also fixed the single-printer manual sync endpoint which didn't track synced spool IDs, risking incorrect location clearing for location-matched (non-RFID) spools.
## [0.2.3b2] - 2026-04-08
### New Features
- **Optional PostgreSQL Database Support** — Bambuddy can now use an external PostgreSQL database instead of the built-in SQLite. Set the `DATABASE_URL` environment variable (e.g., `postgresql+asyncpg://user:pass@host:5432/bambuddy`) to connect to Postgres. SQLite remains the default when no `DATABASE_URL` is set. All features work with both backends including full-text archive search (FTS5 on SQLite, tsvector+GIN on PostgreSQL), backup/restore (file copy vs pg_dump/pg_restore), health diagnostics, and cross-database restore (import a SQLite backup into PostgreSQL with automatic type conversion and FK handling).
- **Shortest Job First Queue Scheduling** ([#879](https://github.com/maziggy/bambuddy/issues/879)) — New SJF toggle badge on the queue page header. When enabled, the scheduler starts shorter print jobs before longer ones instead of FIFO order. A starvation guard ensures long jobs that get skipped once are protected from being skipped again — they move to the front of the queue on the next cycle. The queue display automatically reorders to show the scheduler's actual execution order. Print duration is cached on queue items at creation time from the 3MF metadata.
- **Auto-Print G-code Injection** ([#422](https://github.com/maziggy/bambuddy/issues/422)) — Configure custom start and end G-code snippets per printer model in Settings (Workflow tab) for bed-clearing systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. When adding a print to the queue, enable "Inject G-code" to have the scheduler inject the configured snippets into the 3MF before uploading to the printer. The original file is never modified — injection creates a temporary copy for upload only.
- **External Folder Subfolder Preservation** ([#890](https://github.com/maziggy/bambuddy/issues/890)) — Scanning an external folder now mirrors the real directory structure into the file manager folder tree instead of flattening all files into the root. Subdirectories are created as child LibraryFolders with correct parent/child hierarchy, and files are assigned to their matching subfolder. Hidden directories are skipped when "Show hidden files" is disabled. Subfolders that are deleted from disk are automatically cleaned up on the next scan. Created subfolders inherit the parent's read-only and show-hidden settings.
- **LDAP Authentication** ([#794](https://github.com/maziggy/bambuddy/issues/794)) — Users can now authenticate against an LDAP/Active Directory server. Configure the LDAP server URL, bind DN, search base, and user filter in Settings > Authentication > LDAP. Supports StartTLS, LDAPS (SSL), and plaintext connections. LDAP groups can be mapped to BamBuddy groups (Administrators, Operators, Viewers) for automatic role assignment. Auto-provisioning creates BamBuddy accounts on first LDAP login when enabled. Local admin accounts remain as fallback when the LDAP server is unreachable. Password management features (change password, forgot password, admin reset) are automatically disabled for LDAP users.
- **SpoolBuddy Quick Menu** ([#893](https://github.com/maziggy/bambuddy/issues/893)) — Swipe down from the top of the SpoolBuddy display to open a quick-access control panel. Toggle printer power via smart plugs directly from the display, and manage the SpoolBuddy system with restart daemon, restart browser, reboot, and shutdown controls. All destructive actions require confirmation. The menu shows real-time smart plug state (ON/OFF) for each printer that has a linked power plug.
### Improved
- **Database Engine Info on System Page** — The System Information page now shows the active database engine (SQLite or PostgreSQL) and its version in the Database section, making it easy to verify which backend is in use.
- **Plate Number in Printer View** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — Printer cards and the stream overlay now show the plate number alongside the filename when printing plate 2+ of a multi-plate 3MF file (e.g. "MyModel — Plate 3"). Single-plate prints are unchanged.
- **Printer Name in Queue for Model-Based Jobs** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — Queue items assigned to a printer type ("Any P1S") now show the actual printer name once the scheduler assigns a specific printer, instead of continuing to display the generic model target while printing or in history.
- **AMS Drying Support for H2S** ([#886](https://github.com/maziggy/bambuddy/issues/886)) — Remote AMS drying and queue auto-drying now work on H2S printers with firmware 01.02.00.00 or later.
- **REST Smart Plug: Separate Power/Energy URLs and Unit Multipliers** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — REST/Webhook smart plugs can now use individual URLs for power and energy data instead of requiring all values in a single status response. Each value falls back to the shared Status URL when no separate URL is configured, so existing setups work without changes. Added power and energy multipliers for unit conversion (e.g., set energy multiplier to `0.001` to convert Wh to kWh). Useful for platforms like ioBroker that expose each data point as a separate API endpoint.
### Security
- **Path Traversal in File Upload Endpoints** — Archive upload endpoints (`/upload`, `/upload-bulk`, `/{id}/source`, `/source-by-name`, `/{id}/f3d`, `/{id}/timelapse`) used the client-supplied filename directly in file paths without stripping directory components. An authenticated attacker could write files outside the intended directory via directory traversal (e.g. `../../evil.3mf`). All upload endpoints now sanitize filenames by extracting only the basename before constructing paths. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Unauthenticated Bug Report Endpoints** — The bug report endpoints (`/start-logging`, `/stop-logging`, `/submit`) had no authentication, allowing anyone on the network to enable debug logging, retrieve system logs, and trigger bug report submissions with system diagnostics when authentication was enabled. All three endpoints now require authentication — `start-logging` requires `settings:update` permission, `stop-logging` and `submit` require `settings:read`. Endpoints remain open when authentication is disabled (the default). Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **API Key Empty Printer List Grants Full Access** — An API key with an empty `printer_ids` list (`[]`) was treated identically to `null` (global access to all printers), granting full printer access instead of no access. Now `null` means global access (admin key) and `[]` means no printer access. Existing API keys with empty lists are automatically migrated to `null` on startup. Also fixed the webhook queue endpoint which used a falsy check that would bypass the filter for empty lists. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Missing HTTP Security Headers** — API responses did not include standard security headers. Added a middleware that sets `X-Content-Type-Options: nosniff` (prevents MIME-sniffing), `X-Frame-Options: DENY` (prevents clickjacking via iframe embedding), and `Referrer-Policy: strict-origin-when-cross-origin` (limits URL leakage to external services) on every response. `Content-Security-Policy` was omitted because the React SPA uses inline styles extensively and a permissive CSP would provide no meaningful protection. `Strict-Transport-Security` was omitted because Bambuddy is a LAN application commonly accessed over HTTP — HSTS would lock users out. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Camera Snapshot Temp Files World-Readable** — Camera snapshot and plate detection endpoints created temporary JPEG files in `/tmp` with default 0644 permissions, making them readable by any local user. Switched from `NamedTemporaryFile(delete=False)` to `mkstemp` with explicit 0600 permissions so only the application user can read them. Cleanup was already handled via `finally` blocks. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
### Fixed
- **Spool Weight Not Updated After Print** ([#839](https://github.com/maziggy/bambuddy/issues/839)) — Filament usage tracking failed silently in several scenarios: (1) when FTP download failed and a fallback archive was created without a 3MF file, the primary tracking path was skipped entirely — now falls back to matching the 3MF from the library or a previous archive of the same file; (2) external/VT tray spools were never tracked by the AMS remain% fallback because it only iterated AMS unit trays — now captures and tracks VT tray remain% deltas; (3) notifications showed "Unknown" for time and filament on fallback archives — now enriches notifications with usage tracker results and captures estimated print time from MQTT at archive creation; (4) when auto-archive was disabled, `archive_id` was None at print completion so the entire 3MF tracking path was skipped — now searches library files and previous archives by filename to find the 3MF even without an archive, and captures the AMS slot-to-tray mapping at print start so it's available at completion regardless of archive state; (5) when auto-archive was disabled but the print was dispatched by BamBuddy (queue/reprint), the on_print_start callback discarded the expected print entry and returned early — the archive was never promoted to `_active_prints`, so at completion `archive_id` and `ams_mapping` were both None, making all tracking paths fail. Now detects expected prints before the auto-archive early-return and falls through to the normal promotion path, also injecting the stored `ams_mapping` into the usage tracker session.
- **File Manager Stale UI After Deleting Folders/Files** — Deleting a folder, file, or bulk-deleting items in the file manager appeared to succeed (toast shown) but the UI didn't update until a page reload. The delete endpoints (`delete_folder`, `delete_file`, `bulk_delete`) relied on FastAPI's dependency cleanup auto-commit which runs after the response is sent — the frontend received the success response, refetched the folder/file list, but the delete hadn't been committed yet. Added explicit `db.commit()` before returning in all three endpoints.
- **Spool Manager Deducts Double the Filament Used** ([#880](https://github.com/maziggy/bambuddy/issues/880)) — After a print completed, the built-in spool manager subtracted twice the actual filament consumption. The printer's MQTT status message contains both updated AMS remain percentages and the `FINISH` state, which triggered two independent deduction paths in the same event loop cycle: the AMS weight sync (absolute SET from remain%) and the usage tracker (additive delta from 3MF data). The AMS weight sync now skips updates while a print session is active, letting the usage tracker handle deductions precisely via 3MF slicer data.
- **Thumbnails Broken After Backend Restart** — Archive and library thumbnails returned 401 Unauthorized after a backend restart because stream tokens are stored in memory and lost on restart. The frontend now detects failed token-protected image loads and automatically refreshes the stream token, so thumbnails recover without a page reload.
- **SpoolBuddy Kiosk Screen Blanks on Boot** — The touchscreen display would blank immediately after the RPi booted, requiring a touch to wake. Added `consoleblank=0` to the kernel cmdline to disable Linux console blanking during the Plymouth-to-labwc transition, and changed the `wlr-randr` anti-blank loop to fire immediately instead of sleeping 60 seconds first.
- **Queue Widget Ignores Plate-Clear Setting** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — The "Clear Plate & Start Next" button on printer cards appeared even when "Require plate-clear confirmation" was disabled in Settings → Queue. The backend correctly auto-dispatched without waiting, but the frontend widget always showed the prompt. The widget now respects the setting and shows a passive queue link instead when plate-clear confirmation is disabled.
- **Ghost Jobs From SQLite Lock on Print Completion** ([#897](https://github.com/maziggy/bambuddy/issues/897)) — When a print finished, the queue status update (`printing` → `completed`) could fail silently if the SQLite database was locked by another writer (e.g. the runtime tracker). The failed commit left the job permanently stuck in `printing` status — a "ghost job" that caused the UI to show false double-assignments when the next job started. The critical queue status commit now retries up to 3 times with backoff on SQLite lock errors (PostgreSQL is unaffected — it uses row-level locking). Additionally, the runtime tracker was holding a single long transaction across all printers; it now commits per-printer to minimize lock hold time.
- **Multi-Plug Automation Only Works for First Plug** ([#903](https://github.com/maziggy/bambuddy/issues/903)) — When multiple smart plugs were assigned to the same printer (e.g. a TUYA printer plug and a particle filter plug via Home Assistant), only the first plug's automation worked. The auto-on at print start, auto-off at print completion, and queue auto-off all queried for a single plug instead of iterating all plugs linked to the printer. All automation paths now control every assigned plug. Also fixed the queue auto-off path which was hardcoded to Tasmota instead of using the correct service for the plug type (HA, MQTT, REST).
- **SpoolBuddy Inventory Not Updating on Spool Changes** — Adding, editing, deleting, archiving, or restoring a spool in the internal inventory did not update SpoolBuddy's frontend views until the next manual refresh or 30-second poll. The spool CRUD endpoints did not emit websocket events, and the SpoolBuddy Dashboard had no polling fallback. All inventory mutation endpoints now broadcast an `inventory_changed` websocket event, and the frontend invalidates the spool cache on receipt — so SpoolBuddy (and all other tabs) reflect changes instantly.
- **AMS Slot Changes Fail Until Reconnect** ([#887](https://github.com/maziggy/bambuddy/issues/887)) — After a keep-alive timeout, paho-mqtt auto-reconnects but the new session can be half-broken: the printer continues sending status updates but silently ignores commands. The developer mode probe detected this (no response, leaving `developer_mode` as `null`), but had no timeout or recovery — one unanswered probe permanently blocked retries. Added a 10-second probe timeout with one retry; after two consecutive unanswered probes, Bambuddy force-closes the socket to trigger a clean reconnect with a fresh session. Additionally, the developer mode probe was firing on every auto-reconnect, which destabilized some firmware MQTT brokers (A1/P1 series) — causing a reconnect → probe → disconnect feedback loop. The probe result is now cached across reconnects and only runs once on the first connection, with a 5-second delay after connect to let the session stabilize.
- **WebSocket Crash on Printers Without `fun` Field** ([#873](https://github.com/maziggy/bambuddy/issues/873)) — Connecting to printers that don't send the MQTT `fun` field (A1, P1 series, X1Plus firmware) caused a repeating `'str' object has no attribute 'get'` crash in the WebSocket handler, showing the printer as offline with missing AMS and SD card info. The developer mode probe introduced in 0.2.3b1 published an MQTT message inside `_update_state()` between overwriting `raw_data` with the full MQTT dict (where `vt_tray` is a raw dict) and restoring the previously normalized list — the `publish()` call released the GIL, letting the event loop read the un-normalized dict and iterate over string keys instead of spool dicts. Fixed by normalizing `vt_tray` dict→list in the MQTT data before assignment, and moving preserved field restoration before the probe. Added defensive normalization in `printer_state_to_dict` as a belt-and-suspenders guard.
## [0.2.3b1] - 2026-04-02
### New Features
- **Queue Timeline View** ([#823](https://github.com/maziggy/bambuddy/issues/823)) — The queue page now has a production schedule view showing when each print is estimated to finish. Events are sorted chronologically and grouped by hour, with cards showing the file name, printer, estimated completion time, and time remaining. Active prints show a live progress bar. Filter by "Show All", "Printing", or "Queued", and navigate between days. Click any event to edit or stop it. Toggle between List and Timeline views with the button group above the queue.
- **Staggered Batch Start for Multi-Printer Jobs** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — When sending a print to multiple printers via the queue, you can now stagger the starts to avoid power spikes from simultaneous bed heating. Enable "Stagger printer starts" in the schedule options to define a group size (how many printers start at once) and interval (minutes between groups). For example, 10 printers with group size 2 and interval 5 min will start in 5 waves over 25 minutes. Default group size and interval are configurable in Settings → Queue. Works with both ASAP and Scheduled timing — ASAP starts the first group immediately, subsequent groups get computed scheduled times. The stagger option is also available in the direct Print dialog when multiple printers are selected — prints are automatically queued with staggered start times, so you can close the browser and walk away.
- **Plate-Clear Confirmation Setting** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — New "Require plate-clear confirmation" toggle in Settings → Queue. When disabled, the scheduler starts queued prints automatically on printers with finished jobs without waiting for per-printer plate confirmation. Useful for farm workflows where plates are verified physically before starting a batch. Default is enabled (existing behavior preserved).
- **Settings Queue Tab** — New dedicated Queue tab in Settings consolidates queue-related settings: staggered start defaults and auto-drying configuration (moved from the Filament tab).
- **Per-User Statistics Filtering** ([#730](https://github.com/maziggy/bambuddy/issues/730)) — Admins can now filter the Statistics page by user. A user dropdown appears in the stats header for users with the new `stats:filter_by_user` permission (Administrators only by default). Filter by a specific user to see their prints, filament usage, and costs, or select "No User (System)" to view prints without user attribution (e.g. slicer-initiated or pre-auth prints). The filter applies to all stats widgets and exports.
- **Bulk Printer Actions** ([#825](https://github.com/maziggy/bambuddy/issues/825)) — Select multiple printer cards and apply bulk actions from a floating toolbar. Toggle selection mode from the header, then click cards to select. Use "Select All", "Select by State" (printing, paused, finished, idle, error, offline), or "Select by Location" to quickly pick printers. Available actions: Stop, Pause, Resume, Clear Notifications, and Clear Bed — each button is smart-enabled based on the selected printers' current states. Confirmation modals for destructive actions (Stop, Pause, Clear Bed). The status summary bar now shows all printer states (printing, paused, finished, idle, error, offline).
- **Prefer Lowest Remaining Filament** ([#805](https://github.com/maziggy/bambuddy/issues/805)) — New optional setting in Settings → Filament that prefers AMS spools with the lowest remaining filament during auto-matching. When multiple spools match the same type and color, the one with the least filament remaining is selected first. Helps consume partial spools before starting new ones. Applies to queue scheduling, print modal, and multi-printer mapping. Unknown remain values (e.g. external spools without sensors) are treated as full. Disabled by default.
- **REST/Webhook Smart Plug Type** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — New "REST" smart plug type for controlling power via generic HTTP APIs. Works with any home automation platform that has an HTTP endpoint (openHAB, ioBroker, FHEM, Node-RED, etc.). Configure separate ON/OFF URLs with custom HTTP methods (GET/POST/PUT/PATCH), request bodies, and headers. Optional status polling via a GET endpoint with JSON path extraction for state, power, and energy monitoring. Fully controllable — supports auto on/off with prints, daily scheduling, sidebar quick-toggle, and power alerts.
- **Configurable Default Print Options** ([#858](https://github.com/maziggy/bambuddy/issues/858)) — Print options (bed levelling, flow calibration, vibration calibration, first layer inspection, timelapse) now have configurable defaults in Settings → Workflow. Set your preferred defaults once and every new print dialog starts with those values. Still overridable per print.
- **Batch Print Quantity** ([#342](https://github.com/maziggy/bambuddy/issues/342)) — Print multiple copies of a file in one step. The print and schedule dialogs now have a quantity field — set it to any number and the system creates that many queue items automatically. When quantity is greater than one, items are grouped into a batch for tracking. In the direct print dialog, the first copy prints immediately while the remaining copies are queued. The queue page shows a batch badge on grouped items. Batch progress and cancellation are available via the API.
- **GitHub Backup: Spool Inventory & Print Archives** ([#870](https://github.com/maziggy/bambuddy/issues/870)) — GitHub backup can now include spool inventory and print archive history as optional toggles alongside the existing K-profiles, cloud profiles, and settings. Spool backup exports all spools with their material, brand, color, weight, cost tracking, RFID tags, and full usage history. Archive backup exports print history metadata (filament, temperatures, times, costs, energy) — no gcode/3MF binary files. Both are off by default and can be enabled independently in Settings → Backup & Restore.
### Improved
- **Standardized Webhook Notification Payloads** ([#871](https://github.com/maziggy/bambuddy/issues/871)) — Custom webhook notifications now include structured event data fields (`event`, `printer`, `filename`, `duration`, etc.) alongside the existing `title`, `message`, `timestamp`, and `source` fields. Previously, only `title` and `message` were sent, requiring automation tools to parse the message text for event details. All event-specific template variables are now included as top-level JSON fields, making it easy for n8n, Node-RED, Home Assistant, and other automation platforms to route and process notifications based on structured data. Slack/Mattermost format is unchanged.
- **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance.
- **Developer Mode Detection for A1/P1 Printers** — Printers that don't send the `fun` field in MQTT status (A1, P1 series) now have developer mode detected via a probe command. After receiving the first full status update, Bambuddy sends a no-op external slot configure and checks whether the printer accepts or rejects it (`mqtt message verify failed`). Printers that do send the `fun` field (X1C, H2D, etc.) continue to use the existing bit-based detection. Developer mode state is re-checked on every reconnect.
### Fixed
- **Bed Cooled Notification Never Firing** ([#872](https://github.com/maziggy/bambuddy/issues/872)) — Replaced the polling-based bed cooldown monitor with an event-driven approach. The old implementation polled cached bed temperature every 15 seconds for up to 30 minutes after print completion, but some printer firmware (e.g. P2S 01.00.05.00) stops including `bed_temper` in MQTT updates after a print finishes — even in response to pushall requests — causing the cached value to stay frozen at the end-of-print temperature until the monitor timed out. The new approach registers a waiter at print completion and reacts instantly when `bed_temper` data arrives via MQTT, whenever that may be. No timeout, no polling, no stale data — the notification fires as soon as the printer reports the bed is at or below the configured threshold.
- **Filament Color and Subtype Inconsistencies** ([#857](https://github.com/maziggy/bambuddy/issues/857)) — Fixed several filament identification issues: (1) AMS slot popup showed generic color names like "Dark Gray" instead of Bambu-specific names like "Titan Gray" because the fallback skipped the Bambu hex color database. (2) "Silk+" subtype was missing from the known variants list, so the Edit Spool dropdown showed "Silk" instead. Also added "Tough+". (3) Gradient and Dual Color filaments were misclassified — PLA Basic Gradient was detected as "Basic" and PLA Silk Dual Color as "Silk" because the firmware only sends the base material in `tray_sub_brands`. Now detects gradient/multi-color/tri-color variants from the `tray_id_name` color code pattern (M\*/T\* suffixes).
- **External Spool Print Fails on Printers With AMS** ([#854](https://github.com/maziggy/bambuddy/issues/854), [#859](https://github.com/maziggy/bambuddy/issues/859)) — Two related issues with external spool printing: (1) Sending a print to a printer with no AMS units and only an external spool caused "Failed to get AMS mapping table" because the command was sent with `use_ams: true`. Now automatically sets `use_ams: false` when all filament slots map to external spools. (2) Printers with an AMS connected but empty (e.g. X1C with `ams_exist_bits=1, tray_exist_bits=0`) got stuck at heatbed heating or hit the same 07FF_8012 error because the print command used `ams_id: 254` in `ams_mapping2` instead of `255`. The firmware interpreted 254 as a physical AMS tray target instead of external spool. BambuStudio uses `ams_id: 255` (VIRTUAL_TRAY_MAIN_ID) for single-nozzle external spool. Fixed by mapping external spool to `ams_id: 255` on all non-H2D printers. H2D dual-nozzle printers retain 254 (deputy) / 255 (main) distinction.
- **External Folder Scan 500 Error on 3MF Files** ([#846](https://github.com/maziggy/bambuddy/issues/846)) — Scanning an external folder containing .3mf files crashed with "Object of type bytes is not JSON serializable". The parsed 3MF metadata contained raw thumbnail bytes (`_thumbnail_data`) that were stored directly in the database JSON column without cleaning. Also removed a call to the non-existent `parser.extract_thumbnail()` method — thumbnail data is already available in the parsed metadata. Now uses the same `clean_metadata()` pattern as upload and zip extraction.
- **Archives Capped at 50 Items** ([#843](https://github.com/maziggy/bambuddy/issues/843)) — The archives page only showed the 50 most recent prints due to a hardcoded API limit. Users with more than 50 archives could not see or access older entries. Fixed by fetching all archives and adding client-side pagination with configurable page sizes (25, 50, 100, 200, or All). Page size preference is persisted.
- **Filament Usage Not Recorded When Auto-Archive Disabled** — When a printer had "Auto-archive completed prints" turned off, filament consumption was silently lost. The `on_print_complete` callback returned early before reaching the usage tracking code, so neither the internal inventory (AMS remain% deltas) nor Spoolman received usage data. Moved filament tracking to run before the archive check so usage is always recorded regardless of the auto-archive setting.
- **H2D External Spool Uses Wrong Nozzle** ([#836](https://github.com/maziggy/bambuddy/issues/836)) — Prints sent from Bambuddy to dual-nozzle printers (H2D, H2D Pro) with external spools always routed to the wrong nozzle. The old `ams_mapping2` format used a shared `ams_id: 255` with `slot_id: 0/1` to differentiate external slots, but the firmware interpreted slot_id as the nozzle index (0=main/right, 1=deputy/left), routing filament to the opposite nozzle. Already fixed by the #797 `ams_mapping2` format change (per-tray `ams_id` instead of shared unit), but users on older builds still experience this. Printing the same file directly from the slicer worked correctly.
- **SpoolBuddy "Add to Inventory" Failed Silently** — The quick-add button on the SpoolBuddy kiosk did nothing when tapped. The scale weight was sent as a float but the backend requires an integer, causing a Pydantic validation error. The error was silently caught with no user feedback, leaving the confirmation modal stuck open. Fixed by rounding the weight before sending, moving the modal close to a `finally` block, and adding an error toast with the actual API message.
- **SpoolBuddy Dashboard Crash on Null Spool Fields** — Viewing a spool with null `subtype`, `brand`, `rgba`, or `color_name` on the SpoolBuddy dashboard crashed the UI (black screen). The spool prop construction used `displayedSpool?.subtype ?? sbState.matchedSpool!.subtype` — when the field was `null`, the `??` operator fell through to `sbState.matchedSpool` which could also be null, causing a TypeError. Fixed by picking one source object instead of mixing per-field fallbacks. Added a global React error boundary so future crashes show the error instead of a black screen.
- **Plate Thumbnails 401 in Print Modal** — Multi-plate 3MF plate thumbnails in the print modal returned 401 Unauthorized when authentication was enabled. The backend returns bare URL paths for plate thumbnails, but the `PlateSelector` component used them directly in `
` without appending the stream token. Fixed by passing the URL through `withStreamToken()`.
- **Schedule Calendar Picker Opens Off-Screen** — Clicking the calendar icon in the print modal's scheduled mode opened the native date picker at the bottom of the viewport instead of near the date field. The hidden `datetime-local` input used `sr-only` positioning which anchored the picker off-screen. Fixed by positioning the hidden input inside the date field's container.
- **SpoolBuddy Kiosk Display Blanking and Crashes** — The kiosk Chromium flags added in 0.2.2.2 caused display instability: `--js-flags=--max-old-space-size=128` crashed the V8 renderer when heap exceeded 128 MB, `--enable-low-end-device-mode` aggressively killed GPU rendering surfaces, and resetting `CHROMIUM_FLAGS` discarded the Pi's GPU defaults (`--enable-gpu-rasterization`, ANGLE/GLES) creating an unstable mixed CPU/GPU rendering path. Fixed by removing both flags, appending kiosk flags to Pi defaults instead of replacing them, adding a `wlr-randr` keep-alive loop to prevent display blanking, and adding `0` to the labwc config.
- **Sidebar Bottom Icons Cut Off With Smart Plugs** ([#862](https://github.com/maziggy/bambuddy/issues/862)) — Adding smart plug buttons to the sidebar caused the bottom icon row to overflow and get partially cut off. The footer section could be compressed by the flexbox layout when the navigation area grew. Fixed by preventing the footer from shrinking, allowing the expanded icon row to wrap, and adding scroll overflow to the collapsed sidebar icon stack.
- **AMS History Cleanup Crash Every ~24 Hours** — The periodic cleanup of old AMS sensor history entries failed with "can't compare offset-naive and offset-aware datetimes". The cleanup cutoff used `datetime.now(timezone.utc)` (timezone-aware) but the `recorded_at` column stores naive datetimes via SQLite's `func.now()`. The mismatch caused a TypeError when SQLAlchemy processed the comparison. Fixed by using a naive UTC datetime for the cutoff. The error only appeared once per ~24h because the cleanup runs every 288 recording cycles (288 × 5 min = 24h).
- **SpoolBuddy Status Bar Not Updating on Printer Switch** — The bottom status bar on SpoolBuddy kiosk pages showed stale warnings (e.g. low filament) from the previously selected printer after switching to a different printer via the dropdown or swipe gesture. Two issues: (1) the AMS data cache was a single ref shared across all printers, so switching to a printer whose status hadn't loaded yet fell back to the previous printer's cached AMS data; (2) the Layout's alert useEffect unconditionally cleared alerts to null when the device was online, which could overwrite printer-specific alerts set by child pages. Fixed by keying the AMS cache per printer ID and tracking Layout-owned alerts separately so child page alerts aren't clobbered.
## [0.2.2.2] - 2026-03-27
### New Features
- **Persistent Auto-Off for Smart Plugs** ([#826](https://github.com/maziggy/bambuddy/issues/826)) — Smart plugs now have a "Keep Enabled" toggle under Auto Off settings. When enabled, auto-off stays active between prints instead of requiring manual re-enablement after each print (one-shot). Useful for accessories like BentoBox filters on Home Assistant switches that should always power off when a print completes. Default behavior (one-shot) is unchanged. Requested by @AeroMaestro.
- **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 Kiosk Performance Optimizations** — Reduced idle CPU load on Raspberry Pi from ~3.3 to ~0.9. Frontend: replaced expensive CSS animations on the idle dashboard (`animate-ping` with scale transforms, `blur-2xl` glow, continuous `animate-pulse` on status dots) with static elements and a slow color-cycling spool (5s interval). Chromium: added `--disable-extensions`, `--disable-background-timer-throttling`, `--disable-renderer-backgrounding`, and `--disable-crash-reporter` to `/etc/chromium.d/spoolbuddy-kiosk`. WebSocket: SpoolBuddy Dashboard and Layout pages now use React Query `select` to extract only `connected` status from printer queries, so temperature/fan/progress updates no longer trigger re-renders on every MQTT tick. Services: stripped services are now masked (not just disabled) to prevent socket/dbus reactivation; user-level services (xdg-desktop-portal, mpris-proxy, pipewire, etc.) are masked globally via `/etc/systemd/user/` overrides instead of unreliable `su -l systemctl --user`. Removed chromium and upower from `strip_packages` since the kiosk needs them — they were being uninstalled then immediately reinstalled on every run.
- **SpoolBuddy AMS Slot Action Picker** — Clicking an AMS slot on the SpoolBuddy AMS page now shows a picker with contextual actions: Configure AMS Slot (set filament preset, K-profile, color), and either Assign Spool / Link to Spoolman (when no spool is mapped) or Unassign / Unlink (when one is). Works with both internal inventory and Spoolman. Previously the slot click went straight to the configure modal with no way to manage spool assignments.
- **Unassign Button in Edit Spool Modal** — The edit spool modal now has an "Unassign" button next to "Delete Tag" that removes the spool's AMS slot assignment, clearing the location column in the inventory table.
- **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.
- **SpoolBuddy Assign-to-AMS Material Mismatch Warnings** — The SpoolBuddy "Assign to AMS" modal now warns when the spool's material or slicer profile doesn't match the target slot's current filament. Shows a confirmation dialog with five warning levels: exact material mismatch, partial material match, profile-only mismatch, and combined material+profile mismatches. Respects the global `disable_filament_warnings` setting. Previously, assigning a spool to an occupied slot proceeded without any validation, matching the behavior already present in the main Assign Spool modal.
- **Spool Assignment Changes Sync Across Tabs** — Assigning or unassigning a spool now broadcasts a WebSocket event to all connected clients. Other open browser tabs and the SpoolBuddy frontend update automatically without requiring a page reload.
- **SpoolBuddy Inventory Page** — Added a new Inventory page to the SpoolBuddy kiosk UI, accessible from the bottom navigation bar between Write and Settings. Shows a responsive catalog grid of spools with colored spool circles (matching AMS page style), material/subtype labels, color dots, fill level bars, remaining weight with percentage, and green AMS location badges (A1, B2, etc.) for assigned spools. Includes a search bar (filters by material, subtype, brand, color, notes) and touch-friendly inline filter pills ("All", "In AMS", per-material). Tapping a spool opens a full-screen detail view with spool icon, remaining bar, AMS assignment, weight breakdown, slicer filament, PA K-profiles (name and value), temperature range, cost, tag ID, and notes. Detail view updates live from query data. Assigned spools sort first. When Spoolman is enabled, the page shows the Spoolman UI instead.
- **SpoolBuddy Auto-Navigate on Tag Scan** — When an NFC tag is detected while the SpoolBuddy UI is on a non-dashboard page (Settings, AMS, Write Tag, etc.), the frontend automatically navigates back to the main dashboard to show the scanned spool. Also wakes the screen if the display was blanked.
- **SpoolBuddy Swipe to Switch Printers** — Swiping left/right on the SpoolBuddy touchscreen now cycles through online printers instead of triggering browser back/forward navigation. The selected printer updates in the top bar dropdown. Requires at least two online printers; single-printer setups are unaffected.
- **SpoolBuddy Virtual Keyboard Layout Fix** — The virtual keyboard now participates in the flex layout instead of overlaying as a fixed element. When the keyboard opens, the bottom nav and status bar are hidden and the content area shrinks to fit, eliminating the dead space gap between content and keyboard on the Inventory page. Number inputs (e.g. Weight field on Write Tag) now accept virtual keyboard input.
- **Removed Diagnostic Buttons from Write Tag Page** — Removed the "NFC Diag" and "Scale Diag" buttons from the NFC status panel on the Write Tag page. These diagnostics are accessible from the Settings page and don't belong on the tag writing flow.
- **SpoolBuddy Assign Spool Modal No Longer Clips Display** — The shared Assign Spool modal overflowed off-screen on the small SpoolBuddy touchscreen, hiding the footer buttons. Added scoped CSS in the SpoolBuddy AMS page that caps the modal at 90vh with a scrollable spool list, without affecting the main Bambuddy frontend.
- **SpoolBuddy System Tab** — Added a "System" tab to SpoolBuddy Settings showing live OS stats from the Raspberry Pi: CPU temperature, core count, load average, memory usage, disk usage, OS distro/kernel/architecture, Python version, and system uptime. Stats are collected by the daemon every heartbeat (10s) using stdlib-only reads from `/proc` and `/sys` — no additional dependencies required. Usage bars turn amber at 70% and red at 90%; CPU temperature is color-coded green/amber/red.
- **SpoolBuddy Boot Splash Polished** — New splash image displays only the SpoolBuddy logo (removed Bambuddy branding) with green glow bloom, radial gradient background, light rays, and vignette. A generator script (`generate_splash.py`) is included for easy customization. Also reduced redundant initramfs rebuilds during install by deferring the rebuild until after the Plymouth theme is configured.
### Security
- **Token-Based Auth for Media Endpoints** — Camera streams, snapshots, thumbnails, timelapse videos, photos, QR codes, and cover images served via `
`/`