All notable changes to Bambuddy will be documented in this file.
/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/).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.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./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.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.url field (ftp://file name.3mf) contained unencoded spaces that the firmware couldn't parse. Fixed by replacing spaces with underscores in the remote filename before upload. Reported by @benjamdev.formatSlotLabel to display the full slot label (e.g. "Low Filament: PLA (B2) - 4% remaining").read_tag.py diagnostic script had five issues preventing NTAG reads: (1) SAK 0x04 (MIFARE Ultralight family) was rejected as "unsupported tag type" — now accepts both 0x00 and 0x04. (2) ntag_read_pages had TX CRC off (should be on per NTAG spec), no Crypto1 clear, and no IDLE→TRANSCEIVE state reset. (3) The PN5180 enters an unrecoverable state after an NTAG READ command — added full GPIO hardware reset between each 4-page batch. (4) Reading past the end of smaller tags (MIFARE Ultralight has 16 pages vs NTAG's 44+) caused a hard failure — now returns partial data gracefully. (5) ntag_write_page/ntag_write_pages had the same stale CRC/state issues plus unreliable ACK checking and post-write verification — synced with daemon.tag_uid but left tray_uuid, tag_type, and data_origin intact. All tag-related fields are now cleared together.0x04 (MIFARE Ultralight family) instead of 0x00 during anticollision — both 0x00 and 0x04 are now accepted. (2) TX CRC was disabled for NTAG commands but the spec requires it — enabled for both WRITE and READ. (3) The PN5180 state machine needed IDLE→TRANSCEIVE resets (not just set_transceive_mode()) and Crypto1 cleared before NTAG operations. (4) The 4-bit WRITE ACK cannot be captured by the PN5180 (SOF detected but no RX_IRQ) — removed per-page ACK checking. (5) Post-write read-back verification also failed (second READ command gets no response from the PN5180) — removed verification since the tag reliably ACKs each write.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.APP_VERSION from the backend config..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.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.network-online.target so Chromium has connectivity when it starts."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.{id, state} in incremental MQTT updates — filament load/unload transitions now update in real-time without requiring a reconnect. Reported by @RosdasHH.<spoolman_url>/spool._completion_triggered = True when a terminal state is first seen without a prior RUNNING state so the flag is clean for the next print cycle. Reported by @user.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.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.__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.update_status and update_message to the device model but was missing the database migration, causing "no such column" errors on existing installations.rc.is_failure) are never suppressed by the spurious-disconnect filter. The disconnect event used by disconnect() is fired unconditionally at the top of the callback so that no early-return filter can prevent it from unblocking callers. Reported by @inkdawgz.stg_cur=0 when idle, which maps to the "Printing" stage name and overrides the correct "Idle" gcode_state on the printer card. The System Info page was unaffected because it displays the raw gcode_state. Extended the existing A1/A1 Mini workaround for this firmware bug to also cover P1S and P1P models. Reported by @inkdawgz.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.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.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.image field when a snapshot is available (generic format only, not Slack format). Reported by @Arn0uDz.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.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.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.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 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.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.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.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.notify services in addition to the existing REST-based integration. Contributed by @mrtncode.parse()). Dev-only dependency (eslint).printers:control permission when authentication is enabled.dry_sf_reason from printer firmware and surfaces HMS error codes for AMS 2 Pro and AMS-HT power issues.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.{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./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.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/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.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.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. Fixed auto-generated "Contributors" section appearing in GitHub release notes by stripping @mentions from changelog text before creating the release.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.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.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.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./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.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.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.touchstart/touchmove/touchend) to both the header drag handle and the resize handle, with preventDefault to stop page scrolling during drag. Reported by @dsmitty166.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).scalar_one_or_none() which raises MultipleResultsFound. Now fetches all plugs and returns the main (non-script) power plug, matching the API route behavior.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.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.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.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.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_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.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.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.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.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.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.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.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.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.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.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.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.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+.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.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.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.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..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).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).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.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.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.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.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.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.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."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.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.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.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: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 SIGKILLs it — catching orphans that survive app restarts or generator abandonment.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.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.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.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._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.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.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.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.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.ci.yml, security.yml) from Node.js 20 to Node.js 22 LTS ahead of GitHub's Node 20 deprecation.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.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.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.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.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.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).--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.home_flag bit 18 from the printer's MQTT data. The printer info modal also shows "Ethernet" instead of WiFi signal details.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.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.slicer_binary_path from earlier slicer integration research).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.dangerouslySetInnerHTML without sanitization, allowing injected <script> or event handler payloads to execute in any viewer's browser and steal JWT tokens from localStorage. Now sanitized with DOMPurify before rendering.<a> tags by interpolating the href attribute without escaping embedded quotes. A crafted 3MF file with a single-quoted href containing a double-quote break-out could inject onmouseover event handlers through the sanitizer. Replaced the custom sanitizer with DOMPurify./api/v1/auth/setup endpoint could be called without authentication even when auth was already enabled, allowing any network client to disable authentication entirely. Now returns 403 when auth is already enabled; use the authenticated admin panel to modify auth settings.rtsps:// URLs and added access codes to the sensitive string collection for exact-match redaction.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.{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.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.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.v0.2.3b1-daily.20260316) were offered as updates even with "Include beta versions" toggled off. The version parser only checked the last dot-separated segment for prerelease markers, but daily build tags put the beta indicator (b1) earlier with a numeric date suffix as the last segment. Now checks the entire version string. Reported by @Teolhyn.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_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.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.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.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.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.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.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.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.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.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.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.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.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.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.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+.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.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.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.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.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).--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.home_flag bit 18 from the printer's MQTT data. The printer info modal also shows "Ethernet" instead of WiFi signal details./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.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/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.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.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.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..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).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).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.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.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.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.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.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.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."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.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.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.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: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 SIGKILLs it — catching orphans that survive app restarts or generator abandonment.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.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.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.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._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.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.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.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.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.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.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.slicer_binary_path from earlier slicer integration research).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.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.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: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 SIGKILLs it — catching orphans that survive app restarts or generator abandonment.start_bambuddy.bat launcher had Unix (LF) line endings instead of Windows (CRLF). When a user's git config has core.autocrlf=false or input, the file is checked out with LF endings and cmd.exe cannot parse it. Added a .gitattributes file that forces CRLF for all .bat files regardless of git config.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._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.datetime.now(timezone.utc)) produced aware datetimes, but SQLAlchemy's SQLite DateTime columns return naive datetimes on read. Any Python-side comparison between the two raised TypeError: can't subtract offset-naive and offset-aware datetimes, crashing the maintenance overview endpoint and potentially 7 other code paths (API key expiration, smart plug auto-off, power alert cooldown, runtime tracking, print scheduling, and timelapse matching). Added tzinfo is None guards before all database datetime comparisons.cap_add: NET_BIND_SERVICE in docker-compose.yml didn't reliably propagate to the Python process when running as a non-root user (user: directive), depending on the container runtime's ambient capability support. Now sets the file capability directly on the Python binary in the Dockerfile via setcap, which the kernel honors regardless of runtime configuration.dataMin/dataMax), not the selected time window. When the printer was offline for part of the period, shorter views (e.g., 6h) appeared compressed to only the portion with data (e.g., 1.5h). Now pins the X axis domain to the full requested time range (e.g., now−6h to now), pads the data edges so the line extends across the full window, and connects through null values so the chart always shows a continuous line.PrinterQueueWidget only checked required_filament_types (type only) and ignored filament_overrides (type + color). Now passes loaded filament type+color pairs from AMS/vt_tray status to the widget and filters queue items against override colors, mirroring the backend's _count_override_color_matches() logic.-wal file, but the shutdown handler never checkpointed the WAL back into the main database or disposed of engine connections. If the container was stopped or crashed, the WAL could contain partial schema migrations or uncommitted data, causing inconsistent query results on restart. Deleting the -wal and -shm files was the only workaround. Now runs PRAGMA wal_checkpoint(TRUNCATE) and disposes the engine on shutdown, ensuring all data is flushed to the main database file before exit.plate_id was always 1, generating the wrong MQTT gcode path for multi-plate 3MF files (HMS error 0500_4003). Now extracts the plate index from the 3MF's slice_info.config. Second, ams_mapping was never computed for printer-specific queue items (VP assigned to a particular printer), so the printer always used the first AMS slot regardless of which filament the 3MF required. The scheduler now computes AMS mapping for all queue items that lack one, not just model-based assignments.PrinterQueueWidget now filters queue items by filament compatibility — it checks the printer's loaded filament types (from AMS and external spools) against the queue item's required_filament_types and only shows items the printer can actually print. If no compatible items exist, the widget is hidden.weight_locked flag that is automatically set when weight_used is explicitly updated via the API. Locked spools are skipped by both the automatic AMS remain% sync and the manual force-sync endpoint. The usage tracker (3MF/gcode delta tracking) is unaffected. Users can re-enable AMS sync by setting weight_locked: false.archive.cost with conflicting strategies: the usage tracker summed ALL historical SpoolUsageHistory rows for the archive (including rows from previous reprints), and a separate add_reprint_cost method added yet another full print's cost on top. Removed the redundant add_reprint_cost path entirely and changed the usage tracker to compute cost only from the current print session's results instead of querying all historical rows. archive.cost now always reflects the cost of a single print.datetime.now() (server local time) or the deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes timestamps without timezone indicators are UTC and appends 'Z', so when the container's timezone wasn't UTC, every stored timestamp was off by the timezone offset. Replaced all database and comparison timestamps with datetime.now(timezone.utc) across 16 backend files (~80 call sites). On the frontend, replaced 13 new Date(backendTimestamp) calls with parseUTCDate() across 8 files to correctly interpret UTC timestamps. Cosmetic timestamps (filenames, user-facing local time formatting) are intentionally left as local time.printers:control permission can now no longer enable auto power off — the checkbox and tri-state toggle are disabled and visually dimmed.role === 'user' check instead of the actual settings:read permission, so newly created admin users who had the permission still couldn't see the button. Also, after login the auth state was set directly from the login response instead of re-fetching the full auth status, which could miss permission data. Now uses hasPermission('settings:read') for the sidebar check and calls checkAuthStatus() after login to load the complete user state including permissions./, \, ?, or # (e.g., Abzweigdose/Verteilerdose 70mm) caused the slicer protocol handler to fail. The filename is placed in the download URL path and encodeURIComponent-encoded, but BambuStudio and OrcaSlicer call url_decode() on the entire protocol handler URL before downloading. This decoded %2F back to /, creating extra path segments that resulted in a 404. The URL filename is purely cosmetic (the backend resolves files by archive ID, not filename), so now sanitizes /, \, ?, and # to _ in slicer download URLs._find_idle_printer_for_model() validated only filament type (via _get_missing_filament_types()), while color matching (_count_override_color_matches()) was used only for ranking candidates, not filtering them. A printer with 0 color matches was still selected if it had the right types. Now requires at least 1 color match when filament overrides specify colors — printers with 0 matches are skipped and added to the "waiting for filament" reason instead of being treated as valid candidates._add_to_print_queue() method always created queue items with printer_id=None and no target_model. Now assigns the virtual printer's target_printer_id if configured, or falls back to the VP's model (e.g., P1S, X1C) as target_model for "Any Printer" scheduling.onSuccess handler overwrote localSettings with the server response, discarding characters typed during the save request. Removed the stale state overwrite so in-progress user input is preserved.GET /api/v1/queue and GET /api/v1/queue/{id} endpoints now include filament_type, filament_color, layer_height, nozzle_diameter, and sliced_for_model from the archive or library file. Previously these fields were only available via the archive endpoints, requiring an extra API call.max-w-lg to max-w-xl to give profile names more room.on_print_complete callback, after an early return that exits when no archive is found for the print. Prints started from BambuStudio or the printer's touchscreen typically have no archive in Bambuddy, so the function returned before the bed cooldown task was ever created. Moved the bed cooldown monitor to before the archive lookup early-return so it fires for all completed prints regardless of archive state. Also hardened the temperature dict check from truthiness (if status.temperatures:) to type check (isinstance(status.temperatures, dict)) to avoid false negatives on empty dicts._sanitize_log_content() function redacted emails, serials, and credentials but left raw IPv4 addresses in log output. Now adds known printer IPs to the sensitive string list for exact matching, and applies an IPv4 regex that replaces addresses with [IP] while preserving firmware version strings (which use leading-zero octets like 01.09.01.00). Updated the system info page privacy disclaimer to list IP addresses as redacted.stg_cur=74 during print preparation, but this stage was not in the stage name lookup table (which went up to 66, sourced from BambuStudio). Now maps stage 74 to "Preparing". Also added stage 77 ("Preparing AMS") which was present in BambuStudio but missing from the lookup.tray_now=254 generically for both external spools, so the frontend's direct ID comparison (effectiveTrayNow === extTrayId) always matched Ext-L (id=254). Now uses active_extruder on dual-nozzle printers to determine which external spool is active: extruder 1 (left) → Ext-L, extruder 0 (right) → Ext-R.on_ams_change stale-assignment cleanup searched only AMS unit data for matching trays, but external spools live in vt_tray (a separate MQTT field). Since _find_tray_in_ams_data never found them, external assignments were always marked as stale and removed. Now looks up external spool assignments (ams_id=255) in the printer's vt_tray data instead, and keeps the assignment if vt_tray data hasn't arrived yet.fun field is an integer in the JSON payload, but the parser used int(value, 16) which requires a string argument. This raised TypeError on every message, silently caught by the exception handler, so developer_mode was never set. Now handles both integer and hex string formats.remain fallback entirely — extEffectiveFill only checked Spoolman and inventory, falling through to null even when the printer reported a valid fill percentage. Now includes the same AMS remain fallback as regular and AMS-HT slots. Second, when fill level was unknown (null), the AMS slot visual showed a full-width gray bar (appearing "full") while the hover card showed "—" (appearing "empty") — confusing users into thinking the printer card and hover card disagreed. Removed the misleading gray fallback bar from all three slot types; the empty fill bar track now consistently indicates "unknown" in both views. Third, the fill level priority chain always preferred AMS remain over Spoolman and inventory data, even when those sources were more accurate (e.g., spools migrated from Spoolman to internal inventory, or spools with accurate usage tracking). Reversed the priority to Spoolman → Inventory → AMS remain, and fixed fillSource to correctly reflect the actual data source used (was always reporting 'ams' even when Spoolman or inventory provided the value via the fallback chain when remain was -1).filename field but not file_metadata.print_name, which the UI uses as the primary display name. Since print_name is extracted from inside the 3MF at upload time, it always took precedence over the renamed filename. The rename endpoint now also updates print_name in the file metadata when present.file_path was null. The finish photo capture silently skipped because it derived the save directory from file_path. Now falls back to archive/{id}/ so the photo is captured regardless.GET /printers/available-filaments endpoint aggregates loaded filaments across all active printers of a given model. Backend stores overrides as a JSON column on the queue item and applies them at scheduling time by merging into filament requirements before AMS mapping. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).tray_now, not global tray IDs — contrary to the previous assumption that all single-nozzle printers report global IDs. Filament usage tracking was unaffected because it uses the MQTT mapping field (snow-encoded with correct AMS hardware IDs). The display now cross-references tray_now with the MQTT mapping field to resolve the correct AMS unit when multiple AMS units are detected via ams_exist_bits. Falls back to the raw value when no mapping is available (e.g., manual filament load outside of a print) or when the mapping is ambiguous.indexOf), so "PCTG" matched "PC" first. The AMS slot configuration and local profiles views were also missing PCTG from their known material types. Additionally, the temperature range logic used includes('PC') which matched PCTG and assigned PC temperatures (260-300°C) instead of PETG-range temperatures (220-260°C). Fixed by reordering PCTG before PC in the spool form parser, adding PCTG to all material type arrays, and adding an exact-match temperature case for PCTG.delete_file_async catches errors internally and returns False instead of raising — the except retry branch never executed. Fixed by only breaking on successful delete and retrying with a 2-second delay on failure. Second, when start_print() failed after uploading a file (in both the background dispatcher and print scheduler), the uploaded file was never cleaned up since on_print_complete never fires for a print that never started. Now deletes the uploaded file on a best-effort basis when start_print() returns False. Third, cleanup failure logging was at DEBUG level, making failures invisible in normal operation — escalated to WARNING.0500_0007 "MQTT command verification failed") were triggering printer error notifications even though they don't indicate actual print problems. For example, a device with incorrect bind settings sending unauthorized MQTT commands caused repeated false-alarm nozzle/extruder error notifications with camera snapshots of perfectly fine prints. Now suppresses notifications for known non-actionable error codes: 0500_0007 (MQTT auth failure), 0500_4001 (Bambu Cloud connection failure), and 0500_400E (print cancelled by user).http://user:pass@host) were logged verbatim by httpx; now uses httpx's auth parameter for HTTP Basic auth so credentials never appear in the URL. Added username and path to the settings key filter to redact smtp_username and slicer_binary_path from the support info JSON. A URL credentials regex provides defense-in-depth for any remaining user:pass@ patterns in logs. IP addresses are no longer redacted from the bundle as they are needed for connectivity debugging. Updated the frontend privacy disclaimer and wiki documentation to reflect the new behavior.on_ams_change handler eagerly deleted the empty spool's SpoolAssignment record (fingerprint mismatch), so on_print_complete found nothing and silently dropped usage — fixed by snapshotting all spool assignments at print start into the PrintSession. Second, even with the snapshot fix, the entire print's filament weight was attributed to the original spool (100%/0% split) because _track_from_3mf() only knew about the tray loaded at print start. Now tracks tray changes during the print via tray_change_log on PrinterState, recording each tray switch with its layer number. At print completion, the usage tracker splits the 3MF weight across trays using per-layer gcode data for precise segment boundaries, with a linear layer-ratio fallback when gcode data isn't available. The last segment always receives the remainder to prevent rounding drift.AttributeError: 'NoneType' object has no attribute 'set'. The MQTT callback thread checked self._pending_kprofile_response (not None) at line 2698, but between that check and the .set() call, the asyncio thread's finally block in get_kprofiles() could clear the attribute to None after a timeout — a classic TOCTOU race. Fixed by capturing the event reference in a local variable before the check.printer_id=NULL and target_model="P1S". After the assigned printer finished, the queue widget queried only for items matching printer_id=X, missing the next pending model-based item (printer_id IS NULL). With no next item found, the "Clear Plate & Start Next" button never appeared, leaving the scheduler stuck reporting "Busy". The queue API now accepts an optional target_model parameter; when combined with printer_id, it uses OR logic to also return unassigned items whose target_model matches the printer's model. The frontend passes the printer's model through to this query. Additionally, the backend now resolves the printer's model server-side from the database when the frontend doesn't provide target_model (e.g., when the printer was added without selecting a model), ensuring the OR logic works regardless of whether the client knows the printer's model.'>=' not supported between instances of 'str' and 'int' when computing AMS filament mapping. MQTT raw data returns AMS unit and tray IDs as strings, but _build_loaded_filaments() compared them to integers without casting. The crash prevented the assignment from committing, so the scheduler retried every 30 seconds in an infinite loop. Cast ams_id and tray_id to int() to match the pattern already used for external spool IDs.printer_manager.get_printer(), which returns a PrinterInfo with only name and serial_number. Accessing .ip_address, .access_code, and .model raised AttributeError, silently caught by the outer exception handler. Replaced with a DB query for the Printer model, matching the pattern used everywhere else in on_print_complete().file_path. The finish photo was saved correctly to data/photos/, but the photo serving endpoint resolved the path as (base_dir / "").parent / "photos/" which evaluates to base_dir.parent/photos/ — one directory level too high. The photo existed on disk but the API returned 404. Fixed the path resolution in get_photo, upload_photo, and delete_photo to use base_dir / Path(file_path).parent (same pattern as the save code), which correctly resolves to base_dir/photos/ when file_path is empty.file_path="". The archive endpoints used Path.exists() to check if the 3MF file was available, but settings.base_dir / "" resolves to the base directory itself — which exists() reports as True. Subsequent ZipFile() calls then failed with [Errno 21] Is a directory. Replaced all .exists() checks on archive file paths with .is_file() across 15 locations in the archive routes and 1 in the main module. Also added a file_path truthiness guard for finish photo capture to prevent saving photos under the base directory when the archive has no file path.slicer_filament if set (including PFUS/P custom presets), (2) reuse slot's existing preset only if it's a specific non-generic ID for the same material, (3) generic Bambu filament ID as last resort. Both assign_spool and configure_ams_slot code paths are fixed.Message header. Multi-line messages (e.g., printer name + remaining time) contain newline characters, which are illegal in HTTP headers. Test notifications worked because they are single-line with no image. Now escapes newlines to literal \n in the header, which ntfy interprets and renders as actual line breaks. Additionally, ntfy servers with attachments disabled rejected thumbnail uploads with "attachments not allowed" (HTTP 400 / code 40014), causing the entire notification to fail. Now automatically retries without the image when the server doesn't support attachments.formatDate() that hardcoded the en-GB locale, always displaying dates in a fixed format regardless of the date format setting. Now fetches the date_format setting and uses the shared formatDateInput() utility which formats as MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, or browser locale based on the user's choice.String.fromCharCode(65 + ams_id), which produced accented characters (e.g., Á) for AMS-HT units (ams_id ≥ 128). Now uses the shared formatSlotLabel() utility which correctly handles AMS-HT and external spool slots.POST /inventory/spools/bulk. Stock spools are computed (no database migration) — any spool without a slicer_filament is displayed with an amber "Stock" badge. A new filter (All / Stock / Configured) on the inventory page lets you filter by stock status. Group similar spools: a "Group" toggle in the inventory toolbar visually collapses identical unused/unassigned spools into a single expandable row or card with a count badge (e.g., "5 identical spools"). Grouping key uses material, subtype, brand, color, and label weight. Used or AMS-assigned spools always appear individually. Group state persists to localStorage. The Stock column is available but hidden by default in column settings. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).cost_per_kg value; when a print completes, the usage tracker calculates the cost from actual filament consumption and stores it in the usage history. Archive costs are automatically aggregated from spool usage records. A global default_filament_cost setting (Settings → Filament) provides a fallback when spools don't have individual costs set. The print modal shows a real-time cost preview based on loaded filaments. Archive cards display the total cost. The inventory table includes a sortable cost/kg column. The recalculate-costs endpoint can retroactively update all archive costs when filament prices change. Contributed by @Keybored02./releases instead of /releases/latest and filters by parse_version() prerelease detection (not GitHub's prerelease flag, which may not be set correctly). Users on the Docker latest tag will no longer see notifications for beta releases they can't install.fun field (bit 0x20000000). When any connected printer lacks developer mode, a persistent orange warning banner appears at the top of the UI with the affected printer name(s) and a link to Bambu Lab's documentation on how to enable it. Without developer mode, MQTT write operations (start/stop/pause prints, AMS control, light/speed/gcode commands) are silently rejected by newer firmware. The developer_mode state is included in the support bundle for diagnostics. New /printers/developer-mode-warnings endpoint provides a lightweight polling summary. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR)./inventory route and sidebar position._count_override_color_matches (no status, exact match, no match, partial match, color normalization, external spool) and 5 for override application in filament matching (color override, tray_info_idx clearing, type change, partial override, nozzle filtering with override). Added 12 frontend tests for the FilamentOverride component: 5 rendering tests (null guards, slot display, dropdown count), 2 type filtering tests (same-type only, all colors), 3 nozzle filtering tests (extruder_id matching, single-nozzle passthrough, null extruder_id inclusion), and 2 interaction tests (select override, reset to original)._resolve_local_slot_from_mapping (snow decoding, unmapped entry filtering, ambiguity detection, AMS-HT slot matching). All 66 tray_now-related tests pass.SpoolBulkCreate schema validation (quantity bounds, field preservation, stock vs configured distinction) and bulk endpoint logic (correct spool count, single quantity, identical fields). Added 29 frontend tests: 13 for SpoolFormModal covering validateForm with quickAdd flag (6 tests), quick-add toggle visibility, PA Profile tab hiding, quantity field gating (hidden by default, visible only in quick-add, hidden in edit mode), and brand/subtype optional asterisk removal in quick-add; 16 for inventory grouping logic covering spoolGroupKey identity/differentiation (7 tests) and computeDisplayItems grouping rules (9 tests for identical/different/used/assigned/single/order/mixed/empty scenarios).archive_id database migration, SQLAlchemy is None → .is_(None) in where clauses, duplicate archive cost write, and unconditional zero-cost overwrite.tray_change_log lifecycle (default empty, seed on print start, clear on new print, record during RUNNING/PAUSE, ignore during IDLE, deduplicate, multi-change history). Added 6 usage tracker unit tests for weight splitting (per-layer gcode split, linear fallback, no-change normal path, empty log recovery, missing spool skip, triple segment split).fun field parsing (bit clear/set detection, exact bit check, invalid hex handling, state persistence across messages). Added 4 frontend tests for the warning banner (single/multiple printer names, hidden when empty, "How to enable" link).frontend-typecheck (tsc --noEmit) and frontend-lint (eslint .) hooks to the pre-commit config. Both hooks only trigger when frontend/src/**/*.{ts,tsx} files are staged.PAUSED checks across frontend and backend. The printer only sends PAUSE via MQTT gcode_state, so PAUSED comparisons were unreachable code.extract_nozzle_mapping_from_3mf() function used filament_nozzle_map (user preference) as the primary source for nozzle assignments. BambuStudio's "Auto For Flush" mode overrides user preferences at slice time, so the actual assignment lives in the group_id attribute on <filament> elements in slice_info.config. Now uses group_id as the primary source and falls back to filament_nozzle_map only when group_id is not present./printers/{id}/status endpoint read ams_extruder_map from the MQTT state without checking if the AMS data had been received yet. On fresh connections before the first AMS push-all, this returned an empty map — causing the frontend nozzle filter to show all trays as unfiltered. Now returns an empty object gracefully and the frontend disables nozzle filtering until the map is populated.useFilamentMapping hook always set extruder_id: 0 for external spool matches. Now uses the nozzle mapping from the 3MF file to determine the correct extruder.ams_id * 4 + slot (giving 512+), but AMS-HT units use their raw ams_id (128-135) as the global tray ID. Now uses ams_id directly for AMS-HT units.extruder_id using strict equality, but extruder_id could be undefined for printers that hadn't reported their AMS extruder map yet. This caused all trays to be hidden. Now skips nozzle filtering when extruder_id is undefined.mc_percent and layer_num from the printer's MQTT state — but by the time the on_print_complete callback ran, the printer had already reset these to 0. Now captures the last valid progress and layer values during printing, and the usage tracker reads these captured values on cancellation for accurate partial usage.tray_now <= 3 check for H2D dual-nozzle disambiguation matched any printer loading from AMS 0 (trays 0-3). On P2S, X1C, and X1E with multiple AMS units, this caused warning log spam every second. Now uses a persistent _is_dual_nozzle flag detected from device.extruder.info (>= 2 entries), which only dual-nozzle printers (H2D, H2D Pro) report.snow_slot = -1 for AMS-HT trays (IDs 128-135), causing a "slot mismatch" debug log on every MQTT update even though the result was correct. Now correctly computes snow_slot = 0 for AMS-HT single-slot units.ams_extruder_map fallback computed ams_id * 4 + slot for all AMS types — including AMS-HT units (IDs 128-135) which have a single slot and use their unit ID as the global tray ID. This produced bogus values like 512+ that briefly appeared in the UI and could pollute last_loaded_tray. Now correctly returns the AMS-HT unit ID for single-slot units, handles AMS-HT in multi-AMS matching, filters AMS-HT candidates when slot > 0, and tightens last_loaded_tray to only accept physically valid tray IDs (0-15, 128-135, 254).hover:z-20 and tooltip z-20 classes.Print Queue Shows UUID Hash Instead of Filename (#438) — When printing a library file, the Print Queue and archive displayed the UUID-hex disk filename (e.g., c65887535303404eba1525176a0f78dc) instead of the original human-readable name. Library files are stored on disk with UUID filenames for uniqueness, but archive_print() used the disk path as the display name. Now passes the original LibraryFile.filename through to archive_print() from both the print scheduler and the direct-print-from-library flow, so the archive's filename, print_name, and directory name all use the human-readable name.
Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers (#364) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT mapping field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to slot_id - 1 as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (ams_hw_id * 256 + local_slot) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on tray_now disambiguation. For printers that don't provide the MQTT mapping field (A1, A1 Mini, P1S, P2S), a color-matching fallback compares 3MF filament slot colors against AMS tray colors to resolve the correct slot-to-tray mapping. Gracefully returns no match when colors are ambiguous (duplicate tray colors) or unavailable.
AMS Slot Config: PFUS Preset IDs Cause Slicer to Reset Slots — When assigning a spool with a user-local PFUS* preset ID (from BambuStudio's custom filament profiles), the slicer didn't recognize the ID and actively reset the AMS slot configuration. Now replaces PFUS* IDs with generic Bambu filament IDs (e.g., GFL99 for PLA). When the slot already has a recognized cloud-synced preset for the same material (e.g., P4d64437), it is reused to preserve K-profile calibration associations. Applies to both the slot configure endpoint and the inventory spool assignment flow.
Fill Level Bar Missing for Brand New Spools — Spools with weight_used = 0 (brand new, never printed) showed no fill level bar on the printer card. The condition checked weight_used > 0 instead of weight_used != null, excluding zero-usage spools. Now correctly shows 100% fill for new spools while still hiding the bar when weight data is unavailable (null).
npm audit: suppress moderate ajv ReDoS finding — Added audit-level=high to frontend/.npmrc so npm audit exits cleanly. The ajv@6 ReDoS (GHSA-2g4f-4pwh-qvx6) is a transitive dependency of eslint@9 with no patched v6 release; ajv@8 override breaks eslint. The vulnerability requires crafted $data schema input — not an attack vector in a linting config.
npm audit: fix minimatch ReDoS finding — Added an npm override for minimatch@^10.2.1 in package.json to resolve the high-severity ReDoS (GHSA-3ppc-4f35-3m26) affecting minimatch@3.x/9.x pulled in transitively by eslint@9, typescript-eslint, and @vitest/coverage-v8. Eslint@9 pins minimatch@3.x with no patched release; eslint@10 upgrades to minimatch@10 but is not yet available. The override forces the patched version across the tree. Verified lint, build, and all tests pass.
Spool Form Allows Empty Brand & Subtype (#417) — The spool add/edit modal did not require Brand or Subtype fields, allowing spools to be saved without them. When such a spool was assigned to an AMS slot, the tray_sub_brands sent to the printer was incomplete (e.g., just "PETG" instead of "PETG Basic"), causing BambuStudio to not recognize the filament profile. Brand and Subtype are now mandatory fields with validation errors shown on submit.
Open in Slicer Fails When Authentication Enabled (#421) — The "Open in Slicer" buttons for BambuStudio and OrcaSlicer failed with "importing failed" when authentication was enabled. Slicer protocol handlers (bambustudio://, orcaslicer://) launch the slicer app which fetches the file via HTTP — but cannot send authentication headers, so the global auth middleware returned 401. Additionally, the URL format was wrong on Linux (used the macOS-only bambustudioopen:// scheme instead of bambustudio://open?file=). Fixed with short-lived, single-use download tokens: the frontend fetches a token via an authenticated POST endpoint, then builds a /dl/{token}/{filename} URL that the slicer can access without auth headers. The token is validated server-side (5-minute expiry, single-use). Platform-specific URL formats now match the actual slicer source code: macOS uses bambustudioopen:// with URL encoding, Windows/Linux use bambustudio://open?file=, and OrcaSlicer uses orcaslicer://open?file=.
/api/virtual-printers) and React UI for creating, editing, and deleting virtual printers. Each instance supports all four modes (Immediate, Review, Print Queue, Proxy), any of the 11 supported printer models, per-instance TLS certificates (shared CA), and individual network interface override. Database-backed with auto-incremented serial suffixes.printers:clear_plate permission allows admins to grant users the ability to confirm a plate is cleared for the next queued print without granting full printers:control (which also allows stopping prints, configuring AMS, toggling lights, etc.). Existing groups with printers:control automatically receive the new permission on startup. The Operators default group includes it by default./groups/:id/edit. Features a responsive 2-column grid of always-expanded category cards, permission search/filtering, Select All / Clear All bulk actions, category-level checkboxes with partial state, and a fixed bottom action bar. The old GroupsPage.tsx dead code has been removed./api/v1/filaments/ to /api/v1/filament-catalog/ to avoid confusion with the inventory spools page (labeled "Filament" in the UI). The old endpoint managed material type definitions (cost, temperature, density), not physical spools — the shared name caused users to expect the API to return their spool inventory.useFilamentMapping hook (nozzle-aware matching, AMS-HT handling, external spool extruder logic).tray_now disambiguation paths: single-nozzle passthrough (X1E/P2S), H2D dual-nozzle snow field, pending target, ams_extruder_map fallback, active extruder switching, and full multi-color print lifecycles.weight_used internally.used_g data from the archived 3MF file provides precise per-spool consumption. For failed or aborted prints, per-layer G-code analysis provides accurate partial usage up to the exact failure layer, with linear progress scaling as fallback. AMS remain% delta is the final fallback for G-code-only prints without an archived 3MF. Slot-to-tray mapping uses queue ams_mapping for queue-initiated prints and the printer's tray_now state for single-filament non-queue prints, ensuring the correct physical spool is always tracked.print_complete, print_failed, and print_stopped notification events now expose {filament_grams} (total grams, scaled by progress for partial prints), {filament_details} (per-filament breakdown with AMS slot info, e.g. "AMS-A T1 PLA: 12.4g | AMS-A T3 PETG: 2.8g"), and {progress} (completion percentage for failed/stopped prints). The {filament_details} variable includes the AMS unit and tray position for each filament used, with "Ext" shown for external spool holders. Falls back to type-only format (e.g. "PLA: 10.0g") when usage tracking data is unavailable. Webhook payloads include filament_used, filament_details, and progress fields. Per-slot filament data is stored in archive extra_data for downstream use.filament_nozzle_map + physical_extruder_map in project_settings.config) and constrains filament matching to only AMS trays connected to the correct nozzle via ams_extruder_map. Applies to the print scheduler, reprint modal, queue modal, and multi-printer selection. Falls back gracefully to unfiltered matching when no trays exist on the target nozzle. The filament mapping UI shows L/R nozzle badges for dual-nozzle prints. Translated in all 4 locales (en, de, ja, it).vt_tray field is now an array across the entire stack (MQTT, API, WebSocket, frontend).compatible_printers field. When re-configuring an already-configured slot, the modal pre-selects the saved preset, pre-populates the color, and auto-selects the active K-profile. The preset list auto-scrolls to the selected item. All modal strings are now fully translated in 5 locales (en, de, fr, it, ja)./cloud/filament-id-map endpoint) instead of showing raw IDs like "GFU99" or "P4d64437". Falls back to extracting names from the profile name field.print_log_entries database table.sendPhoto API with the image as caption attachment. ntfy sends the image as a binary PUT with Filename and Message headers. No configuration needed — images are sent automatically when available.clean_print_error MQTT command to dismiss stale print_error values that persist after print cancellation or transient events. Locally clears the error list for immediate UI feedback. Permission-gated to printers:control. The button only appears when there are active errors.X1C_01_09_00_10.bin) instead of the original filename from Bambu Lab's CDN. On the first download the correct filename was uploaded to the SD card, but on subsequent attempts the cached file with the wrong name was used — causing the printer to not recognize the firmware file. Now caches using the original filename so the SD card always receives the correct file./updates/check endpoint also ignored the setting entirely. Now the backend returns early without making GitHub API calls when the setting is disabled, the Settings page respects the check_updates flag before auto-fetching, and the printer card firmware badge shows a neutral version-only display instead of disappearing when firmware update checks are off.SpoolAssignment records when enabling Spoolman, invalidates the frontend cache so printer cards update immediately, and hides the inventory assign/unassign UI on printer cards while in Spoolman mode.on_ams_change callback unconditionally unlinked Bambu Lab spool assignments on each MQTT push-all response, then re-assigned them by sending ams_filament_setting without a setting_id, which cleared the printer's filament preset. Now compares spool RFID identifiers (tray_uuid / tag_uid) before unlinking — if the same spool is still in the slot, the assignment is preserved and no ams_filament_setting command is sent.is_bambu_lab_spool() function (backend) and isBambuLabSpool() (frontend) incorrectly identified third-party spools as Bambu Lab spools when they used Bambu generic filament presets (e.g., "Generic PLA"). The tray_info_idx field (e.g., "GFA00") identifies the filament type, not the spool manufacturer — third-party spools using Bambu presets also have GF-prefixed values. Removed tray_info_idx from detection logic; now uses only hardware RFID identifiers (tray_uuid and tag_uid) which are physically embedded in genuine Bambu Lab spools.BambuFTPClient.disconnect() only caught OSError and ftplib.Error, but quit() raises EOFError when the server has closed the connection mid-session. EOFError is not a subclass of either, so it propagated to callers. Now caught alongside the other exception types for clean best-effort disconnect.tag_uid and tray_uuid fields because they were included in the "always update" list. These fields are now preserved during updates and only cleared when a spool is physically removed (slot clearing detected by empty tray_type). This fixes the AMS "eye" icon disappearing for RFID spools after startup.ams_set_filament_setting, which replaced the firmware's RFID-managed filament config with a manual one — causing the slicer's "eye" icon to change to a "pen" icon. Now detects RFID spools and skips the filament setting command, only sending K-profile selection.extrusion_cali_sel command included a setting_id field that BambuStudio never sends, causing firmware to mislink calibration data. The extrusion_cali_set command was sent unconditionally, overwriting existing profile metadata. Now setting_id is removed from selection commands, and extrusion_cali_set is only sent when no existing profile is selected (cali_idx < 0).000000 (black) as a guard against empty slots, but empty slots already skip color data entirely. Removed the unnecessary check so black is now pre-populated like any other color.weight_used (e.g., +1.6g), but periodic AMS status updates recalculated weight_used from the AMS remain% sensor and overwrote the precise value. For small prints on large spools (e.g., 1.6g on 1000g), the AMS remain% stays at 100% (integer resolution = 10g steps), resetting weight_used back to 0. The AMS weight sync now only increases weight_used, never decreases it, preserving precise values from the usage tracker.remain=0 for all trays while tray_type is still populated. The weight sync treated 0% remain as "100% consumed," computing weight_used = label_weight (e.g., 1000g). The "only increase" guard passed because label_weight > current_used + 1, marking every assigned spool as fully consumed. The AMS weight sync now skips remain=0 entirely — a physically empty spool is tracked by the usage tracker during the print, not by a transient AMS sensor reading.weight_used. If the frontend cache was stale (e.g., loaded before the last print completed), saving the form would silently reset weight_used to the pre-print value, reverting the remaining weight to full. The form now only includes weight_used in the update request when the user explicitly changes the weight field.'SpoolKProfile' object has no attribute 'extruder_id'. The K-profile model uses extruder (not extruder_id). Fixed the attribute name so K-profile matching correctly filters by nozzle on dual-extruder printers.on_print_start callback used ilike('%{name}%') to find existing "printing" archives, which meant a print named "Clip" could incorrectly match "Cable Clip" or "Clip Stand". This could cause a new print to reuse the wrong archive or skip creating one. Tightened to exact print_name match or exact filename variants (.3mf, .gcode.3mf)..3mf files to the printer's SD card root (/) but never deleted them after the print finished. Some printers (e.g. P1S) auto-start files found in the root directory on power cycle, causing ghost prints on every reboot. Now deletes the uploaded file from the SD card after print completion (best-effort, non-blocking). The cleanup also tries .gcode files and retries up to 3 times with a 2-second delay to handle printers that briefly lock the filesystem after a print ends. Runs before the archive lookup so it works even when auto-archiving is disabled.printing to completed/failed) was placed after an early return that exits when the archive record cannot be found. If the archive lookup failed (e.g. app restart mid-print, manual archive deletion), the function returned early and the queue item stayed in printing forever. Over multiple print cycles, stale items accumulated — causing the "Printing" count to show double the actual printers and completed prints to remain in the "Currently Printing" section. Moved the queue item status update (including MQTT relay notification, queue-completed notification, and auto-power-off) to before the archive lookup early return so it always runs.overflow-y: auto, which on Windows Edge (where scrollbars take layout space) caused the scrollbar to appear and disappear on hover — making the color picker unusable at certain zoom levels. Added scrollbar-gutter: stable to reserve scrollbar space and prevent layout thrashing.on_print_start callback had to re-download the 3MF from the printer via FTP, and if that failed, a fallback archive was created without the 3MF file — making 3MF-based filament usage tracking impossible. The queue item's archive_id also remained NULL, so the usage tracker could not find the queue's AMS slot mapping for correct spool resolution. The scheduler now creates an archive from the library file before uploading, links it to the queue item, and registers it as an expected print — matching the behavior of the direct library print route.archive_name and archive_id when displaying the queued item name. Queue items from the file manager have library_file_name and library_file_id instead, so the widget displayed "Archive #null". Now falls back to library_file_name and library_file_id, matching the Queue page display logic.ams_mapping from reprint, library print, and queue print commands is now stored and used as the highest-priority mapping source for usage tracking.tray_now field is always 255 in MQTT data. The actual tray is resolved via the snow field ~44 seconds after print start, but reverts to "unloaded" when the AMS retracts filament at completion. The usage tracker now tracks last_loaded_tray — the last valid tray seen during printing — as a fallback when both tray_now at start and at completion are invalid. Also captures tray_now at print start for printers that report a valid value before the RUNNING state.ams_mapping the slicer sent, because it only subscribed to the printer's report topic. The usage tracker fell back to tray_now which could resolve to the wrong AMS tray (e.g., Black PLA at A2 instead of Green PLA at A4 on H2D Pro). Now subscribes to the MQTT request topic to intercept print commands from any source, capturing the ams_mapping universally — regardless of who starts the print. The request topic subscription is fail-safe: if the printer's MQTT broker rejects it (e.g., P1S), Bambuddy detects the rejection via SUBACK or disconnect timing and gracefully disables the subscription for that printer, falling back to the existing tray_now-based tracking without breaking the MQTT connection..avi (MJPEG), but the timelapse scanner only looked for .mp4 files — so P1S timelapses were never found or attached to archives. Now discovers both .mp4 and .avi timelapse files across all FTP directories (/timelapse, /timelapse/video, /record, /recording). AVI files are saved immediately and converted to MP4 in a non-blocking background task using FFmpeg with -threads 1 and nice -n 19 to minimize CPU impact on Raspberry Pi. If FFmpeg is unavailable, the AVI is served as-is with the correct MIME type. The manual "Scan for Timelapse" route also searches the additional directories used by P1-series printers..mp4, .avi, and .mkv files (non-MP4 auto-converted in background). Remove deletes the file and clears the database reference. Both actions are permission-gated and available in grid and list views.7CC4D5FF vs 56B7E6FF for the same spool, Euclidean distance ~43.6). Now uses a color similarity function with a tolerance threshold of 50, preventing false unlinks from minor RFID/firmware color variations while still detecting genuinely different spools.-p 3000:3000 -p 3002:3002).mapping field, tray_now, last_loaded_tray, all mapping-related raw data keys, and per-AMS-tray summaries (type, color, tray_now, tray_tar). Enables investigating the slot-to-tray mapping behavior across different printer models (X1E, H2D Pro, P1S, etc.) without requiring DEBUG mode.sqlalchemy.engine (changed from INFO to WARNING) and aiosqlite (new WARNING suppression) noise that previously filled 2.5MB in 16 minutes. Every start_print() call now logs a PRINT COMMAND trace with the caller's file, line, and function name. The print scheduler logs pending queue items when found. on_print_complete warns when multiple queue items are in "printing" status for the same printer, which signals a state inconsistency./camera/snapshot) always used the internal printer camera even when an external camera was configured. Now checks for external camera first, matching the existing stream endpoint behavior. Also fixed external MJPEG and RTSP streams silently dropping every ~60 seconds due to missing reconnect logic — the underlying stream generators exit on read timeout, and the caller now retries up to 3 times with a 2-second delay instead of ending the stream.file_1, file_2 instead of the original filename. The Content-Disposition header parser now handles RFC 5987 percent-encoded filenames (filename*=utf-8''...) used by FastAPI for non-ASCII characters. Fix applied to all download endpoints (library files, archives, source files, F3D files, project exports, support bundles, printer files).<img> URL was always the same (/printers/{id}/cover) regardless of which print was active, so the browser served its cached image. Now appends the print name as a cache-busting query parameter so the browser fetches the new cover when a different print starts.*Title* asterisks instead of bold text when the message body contained underscores (e.g. job name A1_plate_8, error code 0300_0001). The code was disabling Markdown parsing entirely when underscores were detected. Now escapes underscores in the body with \_ so Markdown rendering stays enabled.X-Frame-Options: SAMEORIGIN or CSP frame-ancestors headers block iframe embedding, causing "refused to connect" errors. A new "Open in new tab" toggle in the add/edit link modal lets users choose per-link. Keyboard shortcuts (number keys) also respect the setting. Defaults to iframe (existing behavior) for backward compatibility.printers:control permission and is available in all supported languages (en/de/ja).plate_cleared flag is now included in the printer status API response, so the widget correctly shows the passive queue link instead of the Clear Plate button after acknowledgment — even after a page refresh.?tab=email URLs are handled automatically.confirm() dialog and archive had no confirmation at all. Delete shows a danger-styled modal, archive shows a warning-styled modal. Translated in all 5 locales (en, de, fr, it, ja).orcaslicer://open?file= protocol. Default remains Bambu Studio for backward compatibility..orca_filament, .bbscfg, .bbsflmt, .zip, and .json exports. Resolves OrcaSlicer inheritance chains by fetching base Bambu profiles from GitHub (cached locally with 7-day TTL). Stores presets in the database with extracted core fields (material type, vendor, nozzle temps, pressure advance, compatible printers). New "Local Profiles" tab on the Profiles page with drag-and-drop import, 3-column layout (Filament/Process/Printer), search, and expandable preset details. Local filament presets appear in AMS slot configuration alongside cloud presets. Includes smart profile type detection (explicit type field, ZIP path hints, settings ID keys, content heuristics, and name-based patterns) and material/vendor extraction from preset names as fallback.printer.local, my-printer.home.lan) in addition to IPv4 addresses. Updated backend validation, frontend forms, and all locale labels.SkipObjectsModal component shared across PrintersPage and both camera views.tray_uuid over tag_uid for spool identification.HA_URL and HA_TOKEN environment variables for zero-configuration add-on deployments. Auto-enables when both variables are set. UI fields become read-only with lock icons when env-managed. Database values preserved as fallback.active_extruder, replacing the misleading "Docked" label.firmware:read and firmware:update permissions. Translations added in all 4 locales.ja.ts from a divergent format (different key structure, 12 structural conflicts, 1,366 missing translations) to match the English/German locale structure exactly. Translated all 2,083 keys into Japanese, achieving full parity with EN/DE. Zero structural divergences, zero missing keys.h2d), causing firmware checks to offer H2D firmware instead of H2C firmware. H2C has its own firmware track (01.01.x.x vs H2D's 01.02.x.x). Added separate h2c API key mapping. Also added missing H2C/H2S entries to printer model ID and 3MF model maps.invert() filter. The filter was intended for monochrome preset icons but was incorrectly applied to user-uploaded images (e.g., full-color logos). Removed the invert filter from custom icon rendering in the sidebar and the add/edit link modal.[Errno 104] Connection reset by peer while the small verify_job always succeeded. The _handle_data_connection callback returned immediately, allowing the asyncio server-handler task to complete while the data connection was still in active use. The passive port listener also stayed open during transfers, risking duplicate data connections. Fixed by keeping the callback alive until the transfer completes (_transfer_done event), closing the passive listener after accepting the connection, and rejecting duplicate data connections. Also added a 5-second drain timeout to MQTT status pushes to prevent blocking when the slicer is busy uploading.remote_interface_ip setting (network interface override) was only used in proxy mode, but users with multiple network interfaces (LAN + Tailscale, Docker bridges) also needed it in server modes (immediate/review/print_queue). Auto-detected IP from _get_local_ip() followed the OS default route, causing wrong IP in TLS certificate SAN (handshake failures) and SSDP broadcasts (slicer can't discover printer). Now the interface override applies to all modes: included in certificate SAN, passed to SSDP server as advertise IP, and triggers service restart on change. UI dropdown shown for all modes when enabled (not just proxy).subtask_name and never invalidated between prints, so a cache hit returned the stale first-print thumbnail. Now the cover cache is cleared on every print start./usr/etc/print/auto_cali_for_user.gcode) and other internal printer files under /usr/ are now detected and skipped during print start.sendBeacon) failed with 401 Unauthorized when authentication was enabled because sendBeacon cannot send auth headers. Replaced with fetch + keepalive: true which supports Authorization headers while remaining reliable during page unload.filament_used_grams by print quantity, even though the value already represents the total for the entire job. A 26-object print using 126g was counted as 3,276g. Removed the erroneous multiplier from three aggregations in FilamentTrends.tsx.homeassistant_service was not configured with HA URL/token before querying plug energy data, causing it to silently return nothing.use_ams: 1 (integer) as a nozzle index, routing filament to the deputy nozzle instead of the main nozzle. Bambu Studio sends use_ams: true (boolean) while using integers for other fields. Fixed by keeping use_ams as boolean for all printers including H2D series.ams_unit_count: 0 because it expected raw_data["ams"] to be a nested dict ({"ams": [...]}) but the MQTT handler stores it as a flat list. Now handles both formats.latest_version, since there is nothing to compare against.ams_id * 4 + tray_id (= 512), but AMS-HT uses the raw ams_id (128) since it has a single tray. The backend then misidentified 512 as an external spool. Fixed in frontend tray ID calculation, backend ams_mapping2 builder, print scheduler, and Spoolman tracking.getPrinterImage() to return it for H2C models.id - 16). Filament colors and materials were missing because the H2C uses different MQTT field names (color_m, fila_id, sn, tm) than the H2D (filament_colour, filament_id, serial_number, max_temp). Added fallback field name resolution. Also fixed nozzle rack layout breaking on medium card size by allowing the temperature row to wrap.mock_ftp_server.py) implements implicit TLS, custom AVBL command, and per-command failure injectiontransfercmd(), progress callbacks, 553/550/552 error handlingerror_perm hierarchy, diagnose_storage CWD propagation, injection count decrementpyOpenSSL to requirements-dev.txt for Docker test image compatibilitydiagnose_storage() was running before every upload, and its CWD failures (ftplib.error_perm) were not caught because error_perm is not a subclass of error_replydiagnose_storage() from the upload hot pathexcept (OSError, ftplib.error_reply) to except (OSError, ftplib.Error) to catch all FTP error types/api/v1/archives/{id}/reprint and /api/v1/library/files/{id}/print caused by the FTP failure above/api/v1/printers/{id}/cover when FTP download returned 0 bytes but reported success; now retries and falls back to 4040.1.8.1 for hotfixes without incrementing the minor versionxml.etree.ElementTree with defusedxml across all 3MF parsing code../ sequences.codeql/python-bambuddy.qls, .codeql/javascript-bambuddy.qls) with documented accepted-risk exclusions%s style across all backend filesexcept Exception blocks to specific types (OSError, KeyError, ValueError, zipfile.BadZipFile, sqlalchemy.exc.OperationalError, etc.)str(e) with generic error messages in HTTP responses (updates.py)homeassistant.py)tasmota.py)usedforsecurity=False to non-security hash calls (MD5 for AMS fingerprinting, SHA1 for git blob format)test_security.sh uses --threads=0 for all CodeQL commands (auto-detects CPU cores).trivyignore to suppress accepted Dockerfile USER directive findingAmbientCapabilities=CAP_NET_BIND_SERVICE capabilityfilament_used_grams field already contains the total for the entire print job* quantity multiplication from archive stats, Prometheus metrics, and FilamentTrends charttray_info_idx (filament type identifier)tray_info_idx (e.g., "GFA00" for generic PLA) identifies filament TYPE, not unique spoolsfind() which always returned the first match regardless of colorstorbinary() with manual chunked transfer using transfercmd()storbinary() waiting for completion responsePUID=$(id -u) PGID=$(id -g) docker compose up -dstart_bambuddy.bat for Windows users - double-click to run, no installation required.portable\ folder for easy cleanupstart_bambuddy.bat (launch), start_bambuddy.bat update (update deps), start_bambuddy.bat reset (clean start)set PORT=9000 & start_bambuddy.batRequirePermissionIfAuthEnabled() for permission checks<img> tags which cannot send Authorization headers/api/v1/spoolman/spools/linked endpoint returning map of linked spool tags to IDsscript.* entities; now shows all HA entities with toggle enabledfilament_used_grams by quantity/archives/stats) and Prometheus metrics also fixedfetch() without Authorization headerformatDateInput, parseDateInput, getDatePlaceholderformatTimeInput, parseTimeInput, getTimePlaceholderproject relationship not eagerly loaded in get_archive() service methodSecurity Release: This release addresses critical security vulnerabilities. Users running authentication-enabled instances should upgrade immediately.
JWT_SECRET_KEY environment variable (recommended for production).jwt_secret file in data directory with secure permissions (0600)/api/ routes when auth is enabled*_own and *_all variants:queue:update_own / queue:update_allqueue:delete_own / queue:delete_allarchives:update_own / archives:update_allarchives:delete_own / archives:delete_allarchives:reprint_own / archives:reprint_alllibrary:update_own / library:update_alllibrary:delete_own / library:delete_all*_all permissions (can modify any items)*_own permissions (can only modify their own items)*_all permissioncreated_by_id columns to print_archives, library_files, and print_queue tablesprinters:ams_rfid permission for re-reading AMS RFID tagsqueue:create permission for users with restricted access?fps=30 parameter to control camera frame rate (1-30, default 15)?camera=false parameter to hide camera and show only status overlay on black backgroundlibrary:read permission for File Manager endpoints:
library:read permission check to all list/view endpoints (files, folders, stats)library:upload permission check to upload and folder creation endpointsqueue:create permission check to add-to-queue endpointprinters:control permission check to direct print endpointlibrary:read permission can no longer view files in the File Manager*_all permissions<img> don't send Authorization headersams_mapping2 slot_id handling that caused AMS mapping failuresskip_session_reuse to ImplicitFTP_TLSsliced_for_model column that was missing in some upgrade paths/overlay/:printerId combining camera feed with status overlay?size=small|medium|large and ?show=progress,layers,eta,filename,status,printerpower_l1, data.power)GET /api/v1/printers/usb-cameras)GET /api/v1/metrics (Prometheus text format)/api/settings for Home Assistant rest_command compatibility (Issue #152)/ to search paths when looking for 3MF files/cachetray_exist_bits bitmask to detect and clear empty slotsGET /api/v1/smart-plugs/ha/sensors to list available energy sensors{finish_photo_url} template variable for print_complete, print_failed, print_stopped eventscompleted_at - started_at) instead of slicer estimates; cancelled prints only count time actually printed (Issue #137){"text": "..."} instead of custom fields (Issue #133)library_file_id directlyselectedFolderId from useEffect dependency array that was causing a reset looptray_info_idx from the preset's base_id when filament_id is null