mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
fix(vp): Send All enqueues one item per plate; archive delete cascades to queue
VP queue-mode multi-plate Send All
==========================================
BambuStudio / OrcaSlicer "Send All" of a multi-plate project uploads ONE
3MF containing every plate (one FTP STOR, single filename) — slice_info.config
inside the file lists N <plate> blocks with their own index metadata and
their own Metadata/plate_N.gcode payload. Pre-#1733 the VP queue path
called _extract_plate_id which returned only the FIRST plate index, and
_add_to_print_queue built exactly one PrintQueueItem from it. Plates 2..N
silently dropped on the floor. From the user's perspective: Send All of a
3-plate project produced 1 queue item, indistinguishable from a regular
single-plate Send, with no log line to explain the discrepancy.
The wire was confirmed against the live H2D-1 Proxy VP: the same file
ships whether the user clicked Send or Send All; the only intent signal
is the count of <plate> blocks inside slice_info.config.
Fix: replaced _extract_plate_id (-> int | None) with _extract_plate_ids
(-> list[int]). The list contains every <plate> block's index in order;
falls back to [1] when slice_info.config is missing / unparseable so the
single-plate case is preserved. _add_to_print_queue now loops over the
list and creates one PrintQueueItem per plate, with:
- plate-specific position = MAX(position) + iteration_number, so the
items inherit consecutive positions and the slicer's plate order
becomes the queue execution order.
- per-plate required_filament_types / filament_overrides via
extract_filament_requirements(file_path, plate_id) — the plate-aware
filter shipped with #1697 — so the scheduler's per-printer "Any X"
matching dispatches each plate onto a printer with the right
colours loaded for THAT plate, not for plate 1's filament set.
- shared archive_id across all plates (one upload = one archive row).
- the VP's auto_dispatch + manual_start posture inherited unchanged.
Net behaviour: single-plate Send hits the loop once → exactly today's
result (one queue item, plate_id from the slicer, one archive). Multi-
plate Send All of a 3-plate file → 3 queue items, plate_id 1/2/3,
consecutive positions, all referencing the same backing archive.
Archive delete cascades to queue rows
=============================================
Previously the soft-delete path (the default the trash-can button uses)
called _cancel_pending_queue_items which only flipped queue rows with
status='pending' to status='cancelled' while leaving every other status
alone AND leaving every row in the DB. The Send All multi-plate work
above made this much more visible: deleting an archive backed by N
queue items now had to clean up N rows, and what users saw instead was
N "cancelled" rows lingering in the queue history.
Backend:
- Replaced _cancel_pending_queue_items with _delete_related_queue_items
(db, archive_id) -> int. DELETEs every queue row where
archive_id = X regardless of status. Matches what the hard-delete
path already did via the ON DELETE CASCADE FK on
print_queue.archive_id — both paths now produce the same end state.
- Print history lives in PrintLogEntry (FK ON DELETE SET NULL) and is
untouched; Quick Stats / accuracy bands are preserved across both
delete paths.
- 409 guard on archives.py::delete_archive when any related queue
item is currently status='printing'. Both soft and hard delete are
gated; deleting the archive while a print is live would strip the
dispatcher's metadata trail (filament / plate / ams_mapping) out
from under the running print.
- New GET /archives/{id}/delete-impact endpoint returns
{related_queue_items: N, currently_printing: M}. Cheap, single
endpoint, deliberately NOT folded into the archive list response
so the much larger list endpoint isn't forced to run the same
query per row.
Frontend:
- ArchivesPage delete-confirm modal queries the new endpoint when the
modal opens (useQuery with enabled: showDeleteConfirm) and renders
an amber "N queue items linked to this archive will also be removed"
line when total > 0 AND printing = 0, OR a red "Cannot delete —
M queue items are currently printing" line when printing > 0
(confirm button disabled in that case so the user can't bonk the
409 on submit).
- ConfirmModal gained an optional confirmDisabled?: boolean prop —
isLoading was the only disable knob before; this adds the external-
precondition path.
- 2 new i18n keys (deleteQueueItemsWarning, deleteBlockedByPrinting)
translated across all 11 locales per feedback_translate_dont_fallback —
no English fallbacks.
No DB migration — the CASCADE FK was already in place; only the helper's
semantics changed.
This commit is contained in:
@@ -2008,13 +2008,25 @@ class TestAbortedStatusNormalisation:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_soft_delete_archive_cancels_pending_queue_items(
|
||||
async def test_soft_delete_archive_deletes_all_related_queue_items(
|
||||
self, async_client: AsyncClient, printer_factory, archive_factory, queue_item_factory, db_session
|
||||
):
|
||||
"""Soft-deleting an archive cancels its pending queue items with a
|
||||
clear reason. The 3MF is gone from disk so the item can never
|
||||
dispatch — leaving it in 'pending' would 404-storm the queue page
|
||||
and confuse the user about why nothing prints."""
|
||||
"""Soft-deleting an archive removes every related queue item, regardless
|
||||
of status (#1734). Pre-#1734 only ``pending`` rows were flipped to
|
||||
``cancelled`` and stayed in the DB, surprising users who expected the
|
||||
queue lines to disappear with the archive — especially on multi-plate
|
||||
Send All uploads (#1733), where ONE archive backed N queue items and
|
||||
soft-deleting the archive left N "cancelled" rows behind. The change
|
||||
keeps the printing guard (a row with ``status='printing'`` blocks the
|
||||
delete one layer up at the API route), so we never delete the row of
|
||||
an actively-running print here.
|
||||
|
||||
Print history lives in ``PrintLogEntry`` (FK ``ON DELETE SET NULL``) —
|
||||
the audit trail survives independently of the queue rows.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models.print_queue import PrintQueueItem
|
||||
from backend.app.services.archive import ArchiveService
|
||||
|
||||
printer = await printer_factory()
|
||||
@@ -2025,12 +2037,18 @@ class TestAbortedStatusNormalisation:
|
||||
service = ArchiveService(db_session)
|
||||
assert await service.soft_delete_archive(archive.id) is True
|
||||
|
||||
await db_session.refresh(pending)
|
||||
await db_session.refresh(completed)
|
||||
assert pending.status == "cancelled"
|
||||
assert pending.waiting_reason == "Source archive deleted"
|
||||
# Historical rows untouched — they're audit-trail.
|
||||
assert completed.status == "completed"
|
||||
# Every queue row that referenced this archive is gone — both the
|
||||
# pending and the completed rows. Print history (PrintLogEntry) is
|
||||
# the authoritative record and is preserved by the FK SET NULL.
|
||||
remaining = (
|
||||
(await db_session.execute(select(PrintQueueItem).where(PrintQueueItem.id.in_([pending.id, completed.id]))))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
assert remaining == [], (
|
||||
"Soft-deleting the archive must delete every related queue row, "
|
||||
f"got {[(r.id, r.status) for r in remaining]} still present"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
|
||||
Reference in New Issue
Block a user