The daemon now collects CPU temp, core count, load average, memory/disk
usage, OS info, and system uptime every heartbeat using stdlib-only reads
from /proc and /sys. Stats are sent as a JSON blob in the heartbeat
payload, stored in a new system_stats TEXT column, and displayed in a
new "System" tab in SpoolBuddy Settings with color-coded usage bars.
Assigning or unassigning a spool now broadcasts a spool_assignment_changed
event to all connected WebSocket clients. The frontend handles this event
by invalidating the spool-assignments and slotPresets caches, so other
open tabs update automatically without a page reload.
Some P1S/P1P firmware reports stg_cur=0 when idle, which maps to
the "Printing" stage name and overrides the correct IDLE gcode_state
on the printer card. Extend the existing A1/A1 Mini stg_cur idle bug
workaround to also cover P1S and P1P models.
Two issues prevented recovery from stale printer MQTT connections:
1. The spurious-disconnect filter (30s window) could suppress real
keepalive-timeout disconnects (which fire at ~22.5s with keepalive=15s).
Now checks rc.is_failure so error disconnects are never suppressed,
and tightens the window from 30s to 10s.
2. Stale detection (60s no messages) set state.connected=False but left
the TCP socket open, so paho kept publishing into the void. Now
force-closes the socket so paho's loop thread detects the break and
auto-reconnects.
When a printer enters FINISH/FAILED without Bambuddy ever seeing
RUNNING (e.g. after a restart), mark _completion_triggered = True
so the flag is clean for the next print cycle.
BambuStudio converts virtual tray IDs (254/255) to -1 in the flat
ams_mapping and relies on ams_mapping2 for external spool routing.
Bambuddy was passing raw 254/255 in the flat array, which H2D firmware
rejects with "Failed to get AMS mapping table".
- Convert external tray IDs to -1 in flat ams_mapping (match BambuStudio)
- Fix ams_mapping2 for external trays: each virtual tray is its own AMS
unit with slot_id 0, not a shared unit differentiated by slot
- Fix main/deputy nozzle ID comment (255=main, 254=deputy per BambuStudio)
- Add 8 unit tests for start_print() mapping construction
External folder scan generated thumbnails for 3mf/stl/gcode files but
skipped image files. Added IMAGE_EXTENSIONS check with
create_image_thumbnail() to the scan loop.
Some printers (e.g. H2D) only send {id, state} in incremental MQTT
tray updates — no tray_type, tray_color, or other fields. When
filament is unloaded (state changes from 11 to 10), the old tray
data persisted indefinitely because the merge logic only updates
fields present in the incoming message.
Backend:
- Detect tray state != 11 without tray_type in incremental updates
and clear stale tray data (bambu_mqtt.py)
- Expose tray `state` field via REST API and WebSocket broadcasts
(printer.py schema, printers.py route, printer_manager.py)
- Include AMS tray content in WebSocket dedup key so load/unload
transitions trigger broadcasts (main.py)
Frontend:
- Add `state` to AMSTray interface (client.ts)
- Show configure/assign buttons for state=10 (spool present, not
loaded) but hide for state=9 (truly empty) on AMS/HT slots
- Hide fill level bar on empty slots
Tests:
- 5 new tests covering state-based clearing, preservation, reload,
and idempotency
When a Bambu Lab spool is detected in the AMS but no tag match exists,
check for an untagged inventory spool with matching material/subtype/color
before creating a new entry. Links the RFID tag to the existing spool
(data_origin="rfid_linked") to prevent duplicate inventory entries.
Host directories (NAS, USB, network shares) can now be mounted
into the File Manager without copying files. Files are indexed
into the database on scan but read directly from their original
location. Supports read-only mode, hidden file filtering, and
automatic thumbnail extraction for 3MF/STL/gcode.
- POST /library/folders/external — create with path validation
- POST /library/folders/{id}/scan — discover/sync files
- Block uploads, moves, and deletes for read-only external folders
- Never delete actual files from external paths (DB-only removal)
- Purple folder icon + info bar with rescan button in UI
- i18n for all 7 languages
- 19 backend + 11 frontend tests
When a user closed the camera viewer, the stop endpoint killed the
ffmpeg process but never signaled the stream generator's disconnect
event. The generator saw "process died" as a dropped RTSP session
and respawned ffmpeg — up to 30 times per stream. The orphan cleanup
couldn't catch these because they were still tracked as active.
- Add per-stream disconnect events dict so stop endpoint can signal
generators to stop reconnecting before killing the process
- Check if stream_id was removed from _active_streams before
reconnecting (belt-and-suspenders with the event)
- Track frame timestamps per stream_id instead of per printer_id
so stale detection isn't fooled by newer streams for the same
printer
- Reduce stale thresholds from 120s+60s to 60s+30s
- Signal disconnect events from cleanup when killing stale streams
Diagnostic log added in 0.2.2.1 fired on every MQTT update while a
printer was in FINISH/FAILED state, not just on the state transition.
For farms with multiple idle printers, this produced thousands of log
lines per minute. Guard the log to only fire once when the state first
changes to FINISH/FAILED.
15 backend unit tests covering keypair management, branch detection,
SSH command execution (success/failure/timeout), and full update flow
with error handling. 4 new frontend tests for the Updates tab (version
display, buttons, SSH setup, update available state).
The kiosk touchscreen has no way to hard-refresh, and the service worker
served stale cached JS after updates. SpoolBuddy pages now unregister
any existing SW and skip registration entirely. Regular desktop/mobile
users still get the SW. Restored kiosk restart in SSH update flow since
SW is no longer an obstacle.
The SW used stale-while-revalidate for JS/CSS, serving old cached
bundles even after a new build. Changed to network-first (Vite already
content-hashes filenames), bumped cache version v24→v25, and added
Cache-Control: no-cache to the sw.js endpoint so browsers always fetch
the latest service worker.
Check button had no visual feedback because isFetching doesn't trigger
reliably with cached data — replaced with manual loading state. Removed
kiosk restart via getty; the frontend now detects daemon re-registration
via WebSocket and calls window.location.reload(), keeping the user on
the same page and fetching all fresh data.
Merged version, status, and update check into a single card. Buttons
are side by side when up to date. Reduced padding and font sizes.
Removed separate "complete" banner since status is now cleared on
re-registration. SSH Setup section is smaller and more compact.
The SSH update set status to "complete" after the daemon had already
restarted and re-registered, overwriting the cleared state so it stuck
forever. Removed the post-restart "complete" write — daemon
re-registration is now the completion signal, clearing any update status.
After an SSH update completed, the UI kept showing "Update complete,
daemon restarting..." and the old "update available" banner because
nothing cleared the stale state. Registration now resets update_status
when the daemon comes back, and WebSocket handlers for spoolbuddy_update
and spoolbuddy_online invalidate the frontend queries immediately.
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. After updating the daemon, the kiosk browser is also
restarted so it loads the updated frontend.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
Changes:
- New: backend/app/services/spoolbuddy_ssh.py
- Rewritten: trigger_daemon_update endpoint (SSH instead of pending_command)
- New: GET /spoolbuddy/ssh/public-key endpoint
- Auto SSH key deployment via registration response + daemon
- Removed: daemon _perform_update() and cmd=="update" handler
- Install script: bash shell, sudoers for daemon + kiosk restart, .ssh/ setup
- Dockerfile: added openssh-client
- Frontend: SSH key display, force update button
- Fixed: update check compares APP_VERSION, not GitHub releases
- Fixed: kiosk browser restart after update
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
- New: backend/app/services/spoolbuddy_ssh.py (keypair, SSH commands, update orchestration)
- Rewritten: trigger_daemon_update endpoint uses SSH instead of pending_command
- New: GET /spoolbuddy/ssh/public-key endpoint for manual pairing
- Removed: daemon _perform_update() and cmd=="update" heartbeat handler
- Updated: install.sh — bash shell, sudoers for systemctl restart, .ssh/ setup
- Updated: Dockerfile — added openssh-client
- Updated: frontend — SSH key display, force update button
- Fixed: update check now compares against APP_VERSION, not GitHub releases
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. Install script updated with SSH access, sudoers entry,
and --ssh-pubkey flag for pairing.
The SpoolBuddy daemon update endpoint fetched GitHub releases and compared
them against device.firmware_version, which always showed "up to date"
because the comparison source was wrong. Now compares directly against
APP_VERSION from the running backend, so a daemon at 0.2.3b1 correctly
sees 0.2.3 as an available update.
BambuStudio connects to undocumented proprietary ports 2024-2026
on A1/P1S models during the print flow. The proxy wasn't forwarding
these ports, causing connection refused (RST) and triggering the
access code dialog instead of printing. MQTT and FTP worked fine —
port 2024 was the sole blocker.
Added transparent TCP pass-through proxies for ports 2024-2026,
following the same pattern as the existing FileTransfer (6000) and
RTSP (322) proxies. Silently ignored on models that don't use them.
The OTA update feature added update_status and update_message to the
SpoolBuddyDevice model but no ALTER TABLE migration, causing
"no such column: spoolbuddy_devices.update_status" on existing databases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SpoolBuddy devices can now be updated from Settings → Updates without
SSH access. The daemon picks up an "update" command via its existing
heartbeat, runs git fetch/reset + pip install, reports progress back
to the backend, then exits for systemd to restart with the new code.
Backend: update_status/update_message fields, trigger + status endpoints
Daemon: _perform_update() handler, report_update_status() API method
Frontend: "Apply Update" button with live progress in UpdatesTab
When targeting a specific printer, the scheduler's power-on-wait loop
created new MQTT clients on each attempt. Each new client re-tried the
request topic subscription, which some brokers (e.g. A1) reject by
disconnecting. This caused a 170s thrash loop leaving the connection
fragile, so the eventual print command silently failed to reach the
printer.
Cache request topic support per serial number at the class level so
new client instances inherit the knowledge and skip the subscription.
When a printer shuts down it sends a final MQTT message with
tray_exist_bits=0 and power_on_flag=false. The tray_exist_bits
clearing code processed this all-zero value, wiping every AMS
slot's filament data. On reconnect, the auto-unlink check saw
empty tray data (no color, no type) and deleted all spool
assignments as "fingerprint mismatch".
Fix: skip tray_exist_bits slot clearing when power_on_flag is
false. Defaults to true when absent for backwards compatibility.
Adds 3 regression tests covering shutdown preservation, genuine
removal still working, and missing power_on_flag fallback.
Replace fixed 30-second debug log collection with an interactive
3-step flow: start logging, reproduce the issue, stop & submit.
Users now control timing instead of racing a countdown.
Backend: split _collect_debug_logs() into POST /start-logging and
POST /stop-logging endpoints; add debug_logs field to submit request.
Frontend: 3-step progress indicator with elapsed timer, pulsing
active state, and 5-minute auto-stop. Updated all 7 locale files.
Add a "Rotate spool during drying" checkbox to the manual drying popover
for AMS 2 Pro and AMS-HT units. The firmware-level rotate_tray field was
already sent (hardcoded to false) — this makes it user-configurable.
The checkbox defaults to unchecked and resets each time the popover opens.
Firmware silently disables rotation if filament is currently loaded.
When all matching printers were busy and a job was queued with ASAP
timing, the scheduler immediately fired a "Job Waiting for Filament"
notification even though the job was just waiting for a printer to
finish — no user action required.
Added _is_busy_only() check to skip the waiting notification when the
only reason is "Busy". Notifications still fire for actionable reasons
(missing filament, offline, wrong color). Also renamed the default
notification template title to "Queue Job Waiting" and updated
descriptions across all 7 locales.