P1S 01.10.00.00 (and similar firmware revisions) only echo the .3mf
filename in print.gcode_file, dropping the Metadata/plate_N.gcode path.
The /cover route's regex falls back to plate 1 — and the printer card
shows the wrong plate's thumbnail on multi-plate prints.
Resolution order in the new resolve_plate_id() helper (used by both
the status route's current_plate_id and /cover):
1. The plate Bambuddy dispatched. start_print() now records
(dispatched_plate_id, dispatched_subtask) on PrinterState; the
subtask check rejects stale records from a previous Bambuddy
dispatch bleeding into a Studio-direct print on the same project.
2. plate_(\d+)\.gcode regex on state.gcode_file (existing behaviour
for firmware that does include the path).
3. After download, scan the 3MF for a unique Metadata/plate_*.gcode —
covers per-plate archives sliced separately in Studio without a
Bambuddy dispatch record.
4. Default to plate 1.
Cover-byte cache key simplified to (subtask_name, view_key) now that
plate resolution is late-bound. clear_cover_cache() already fires on
every print start, so re-dispatches with a different plate always
fetch a fresh thumbnail.
Bambuddy-dispatched prints additionally register the local archive
3MF in the cover cache at dispatch time, so /cover reads straight
from the archive directory and doesn't refetch the file over FTP
from a printer whose FTP server is busy serving the active print.
Coverage: 5 unit tests for resolve_plate_id, 4 unit tests for the
dispatch record on start_print, 2 integration tests for the cover
route (dispatch wins over plate-1 default; 3MF-scan fallback for
per-plate archive without dispatch record).
Background: P1P firmware can take ~135 s after a project_file MQTT publish
to actually start parsing the uploaded .3mf — gcode_state stays IDLE and
subtask_id doesn't advance until parse completes. The dispatch watchdogs
treated the missed transition as a #887/#936 half-broken session and called
force_reconnect_stale_session, which interrupts the printer's in-progress
parse and triggers 0500_4003 ("can't parse print file") on the printer side.
Both #1150 (slow parse) and #887/#936 (zombie session) look identical from
state and subtask_id alone — both have stale state and stale subtask_id with
fresh telemetry. The distinguishing signal is the printer's gcode_file
field: it updates in push_status when the project_file command actually
lands on the printer, but stays unchanged when the publish was silently
swallowed.
Both watchdogs (_verify_print_response in background_dispatch and
_watchdog_print_start in print_scheduler) now capture pre_gcode_file from
printer_manager.get_status() before sending the publish, then on timeout
compare it against the last good status seen during the poll loop. If the
file changed, the command landed → log a #1150 warning, skip the forced
reconnect to avoid 0500_4003 mid-parse. If unchanged, fall through to the
original force_reconnect_stale_session call so the half-broken-session
recovery is preserved exactly.
Caveat documented in code: in a retry-same-file slow-parse scenario the
gcode_file looks identical pre/post-publish, so the watchdog falls through
to the reconnect path and the user still hits 0500_4003 on that retry.
Accepted to avoid breaking the half-broken-session recovery, which is the
more impactful regression of the two.
The new pre_gcode_file kwarg has a default of None on both watchdog
functions, so any caller that doesn't pass it keeps the original
reconnect-on-timeout behavior verbatim.
4 new unit tests cover both watchdogs: skip on gcode_file change (#1150
fix), reconnect when unchanged (#936 protection preserved), skip when
pre=None and current is non-None (printer just connected), reconnect when
pre_gcode_file arg is omitted (backward-compat). All 439 existing
dispatch / scheduler / mqtt tests pass unchanged.
Follow-up to #1042. The post-dispatch watchdog _verify_print_response was
fire-and-forget — it correctly detected when the printer never transitioned
(HMS error pending, half-broken MQTT session, plate-clear gate, SD card
fault) and force-reconnected the MQTT session, but the dispatch job had
already been marked successful on the optimistic MQTT-publish-acknowledged
path. The UI carried on showing "Print started successfully" while the
printer sat idle.
The watchdog now returns bool and is awaited inline by both call sites in
_run_reprint_archive and _run_print_library_file. On False the call sites
raise a RuntimeError carrying a user-actionable message ("Printer did not
acknowledge print command — state still {pre_state}. Check the printer for
a pending error...") which routes through the existing _run_active_job →
_mark_job_finished(failed=True) → background_dispatch WS broadcast path.
Library-file flow rolls back the freshly-created archive on timeout so no
phantom row is left behind for a print that never started.
The watchdog now also accepts subtask_id advancing past pre_subtask_id as a
definitive "command landed" signal — same as the queue-side watchdog at
print_scheduler.py:1992 — so slow H2D FINISH→PREPARE transitions (~50 s
observed) don't false-fail when the printer has clearly accepted the
project_file but is still in FINISH. Default timeout raised from 15 s to
90 s to match the queue-side watchdog and give the same headroom on both
dispatch paths. Brief mid-window MQTT disconnects keep polling instead of
immediately failing — matches what the queue watchdog already does and
avoids false-failing on transient telemetry gaps.
11 new tests in test_background_dispatch_watchdog.py: state-change pickup,
subtask_id-change pickup with state still FINISH, neither-changed timeout
plus force_reconnect_stale_session call, pre_subtask_id=None backwards-
compat, post-dispatch subtask_id=None not counting as a change, brief
disconnect not short-circuiting the window, persistent disconnect for the
full window returning False, default-timeout=90s contract, _run_reprint_archive
raises RuntimeError with the captured pre-state args on watchdog False,
_run_reprint_archive happy path doesn't rollback, _run_active_job marks
the job failed with the message when _process_job raises RuntimeError.
Library files now move to a configurable-retention trash bin on delete
instead of being hard-deleted from disk (default 30 days). Admins get a
"Purge old" bulk action on the File Manager with a live preview, plus an
optional auto-purge setting in Settings → File Manager that runs the same
operation once per 24h when enabled (default off). Regular users see and
manage their own trashed files; admins see everyone's. External (linked)
files bypass trash since their bytes aren't under Bambuddy's control.
- New `library:purge` permission (admin-only by default)
- Nullable indexed `deleted_at` column on library_files; dialect-aware
ALTER TABLE so the column actually gets added on PostgreSQL (raw
DATETIME is SQLite-only syntax)
- New `LibraryFile.active()` classmethod; every query site routed through
it so trashed rows don't leak into listings, print dispatch, MakerWorld
dedupe, or stats
- Trash page: select-all + bulk restore/delete, per-row checkboxes, wider
layout so datetime columns don't clip
- Auto-purge: 24h throttle via `library_auto_purge_last_run` setting so
the 15-minute sweeper cadence still runs the purge at most once per day
- Save toast wired into every trash/auto-purge setting change
- 17 new backend integration tests (service + routes + auto-purge throttle),
8 new frontend tests, localised across all 8 UI languages
- Wiki + website feature entries updated
The "Print" button on a printer card (and drag-drop-onto-card) used
FileUploadModal to persist the file as a LibraryFile, then dispatched
through POST /library/files/{id}/print. The LibraryFile row + disk file
were left behind after every one-off print, polluting File Manager with
entries the user never asked to save.
FilePrintRequest.cleanup_library_after_dispatch (default False) opts
into post-dispatch cleanup. When set, _run_print_library_file stages
db.delete(lib_file) in the same transaction as archive_print so a
mid-flight FTP / start_print failure rolls both back cleanly, commits
together, then unlinks the library disk file + thumbnail after commit
succeeds. External library files (is_external=True) are never touched.
Only the Printers-page Direct-Print PrintModal sets the flag. Every
other api.printLibraryFile caller (File Manager Print, Project Detail
Print) leaves it unset — their entries are there by user intent.
Also moves formatPrintName out of PrintersPage.tsx into a new
utils/printName.ts module — fa1c46d9 (#881) exported it inline so its
test could import it, tripping react-refresh/only-export-components.
The 0.2.3.1 fix (f03d0c4c) added current_user to the library print
endpoint and plumbed it into the dispatch job object, but the job
runner never read it back out. _run_print_library_file called
ArchiveService.archive_print() without created_by_id and never called
set_current_print_user, so archives created by the printer-card "Print"
button, File Manager prints, and Library prints all landed with
created_by_id=NULL and were invisible to the per-user statistics filter.
The post-print user-targeted notification also had no recipient.
Forward job.requested_by_user_id to archive_print() at creation time
and register the current-print user after start_print succeeds,
matching _run_reprint_archive (lines 686-691).
Reprint-from-Archive still shows NULL for archives whose original
created_by_id was NULL — the reprint path reuses the source row as-is
and only refreshes started_at. Tracked as a separate follow-up.
When a project_file command was unacknowledged for 15s, Bambuddy
previously logged "printer may need restart" and left the broken MQTT
session in place — requiring the user to power-cycle the printer. The
existing half-broken-session recovery only ran via the developer-mode
probe path, which skips printers with a known developer_mode value.
Extract the existing force-reconnect logic into a reusable helper and
call it from _verify_print_response on dispatch timeout. The next
dispatch attempt then lands on a fresh MQTT session without a reboot.
* feat: print files directly from project view (closes#930)
Show printable files from linked library folders directly in the project
detail page, with Print Now and Add to Queue buttons per file. Removes
the detour through the File Manager for common reprint workflows.
Filenames with spaces (e.g. "Junktion Box PRO 90.3mf") broke the MQTT
print command because the url field (ftp://file name.3mf) contained
unencoded spaces the firmware can't parse. FTP upload succeeded but the
printer silently ignored the command and stayed IDLE. Replace spaces
with underscores in the remote filename before upload and print command.
After sending a print command via MQTT, monitor whether the printer's
gcode_state changes within 15 seconds. If not, log a warning visible in
support packages. Addresses silent command drops observed on P1S firmware
01.09.01.00 where the printer ignores project_file commands while
continuing to send status updates.
The isSlicedFile() check only matched .gcode or .gcode.3mf extensions,
so archives from .3mf prints (the standard Bambu slicer output) showed
a "SOURCE" badge instead of "GCODE". Since .3mf can be either sliced or
a raw CAD export, now checks the archive's total_layers and
print_time_seconds metadata to distinguish them. Also passes the
original filename when creating archives from file manager prints.
Three bugs allowed uploaded gcode files to survive on the printer's SD
card, causing firmware restarts to auto-start old prints hours later.
1. The post-print cleanup retry loop always broke after attempt 1
regardless of delete success — delete_file_async returns False on
failure instead of raising, so the except-based retry never fired.
2. When start_print() failed after uploading, the file was never cleaned
up since on_print_complete never fires for prints that never started.
3. Cleanup failures were logged at DEBUG level, invisible in production.
This allows users sending particularly large prints to slow printers such as the P1-series to start a print and then move onto another task in Bambuddy immediately (such as starting more prints on more printers). It also gives the user visibility into what's happening instead of a loading indicator appearing for an indefinite period of time.
The new toast-based UI uses websockets to update in real time. It will also appear for other users / instances of Bambuddy, not just the user who started the prints, allowing more transparency and handling cases where the user closes the page and then comes back wanting to know the status of the dispatching.
fix: review fixes for background dispatch PR #408
- Restore missing imports in main.py (inventory, print_log, virtual_printers, mqtt_smart_plug_service)
- Guard voidresp() for A1 printers to prevent hang after upload
- Don't fail upload on voidresp() error since data transfer already completed
- Add ams_mapping to register_expected_print calls for Spoolman usage tracking
- Fix cancel_job TOCTOU race by using single lock acquisition
- Fix batch counter reset TOCTOU by re-checking condition inside second lock
- Add backgroundDispatch translations to fr.ts and pt-BR.ts
- Remove dead upload_progress_callback definitions
- Skip redundant "Print queued" toast in reprint mode (dispatch toast handles it)
- 5 backend tests: cancel_job single-lock TOCTOU, batch reset re-check,
job lifecycle
- 2 FTP regression tests: voidresp error handling (upload-loop fix),
A1 model voidresp skip
- 1 frontend test: reprint toast suppression
- CHANGELOG: background dispatch feature + test coverage entries
- README: add background dispatch to Scheduling & Automation
- Website: add feature item to Automation section
- Wiki: add Background Print Dispatch section to print-queue.md