mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
fix(queue): persist "Print Anyway" so scheduler stops re-flagging it
When the user clicked Print Anyway on a filament-deficit warning, the
acknowledgement was one-shot. The route cleared manual_start and
filament_short, then the next scheduler tick re-ran
compute_deficit_for_queue_item against identical spool state, found
the same deficit, and re-set both flags. The item bounced between
"user said anyway" and "scheduler re-blocked" — every Play click
returned 409, every confirm got rolled back on the next tick.
Add a persistent acknowledgement flag on the queue item:
- New column `skip_filament_check` on print_queue. SQLite + Postgres
migration branched on is_sqlite() so Postgres doesn't reject
DEFAULT 0 on BOOLEAN.
- PrintQueueItemCreate + PrintQueueItemResponse schemas + the
TypeScript types carry the field.
- POST /print-queue/{id}/start with skip_filament_check=true now
ALSO sets item.skip_filament_check = True (not just clearing
manual_start / filament_short).
- PrintScheduler._block_on_filament_deficit short-circuits to
False — no compute, no flag-setting, no notification — when
item.skip_filament_check is True. We trust the operator's
decision and stop fighting them.
- PrintModal at queue-creation time threads
skip_filament_check=true into the create payload when the user
clicks Print Anyway on the frontend deficit warning, so a print
that was warned-then-acknowledged at add-to-queue time goes in
pre-acknowledged — scheduler never blocks it on first tick.
Flag is not auto-cleared on spool swap by design: if remaining is
now sufficient, the check returns no deficit anyway, so the flag
is moot. Auto-clearing would add lifecycle complexity without
changing behaviour.
AMS Backup awareness (the other half of the discussion) intentionally
NOT included — verified the H2D's bit-26 of print.cfg toggles with
the printer-side AMS Backup setting, but the X1C's cfg has a
different shape entirely and verifying every model family isn't
realistic. Silently under-warning would be worse than always
per-slot. The check stays single-slot for now.
This commit is contained in:
parent
fdcc063d9f
commit
85fbd7fc35
11 changed files with 175 additions and 2 deletions
|
|
@ -199,6 +199,7 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
|
|||
"auto_off_after": item.auto_off_after,
|
||||
"manual_start": item.manual_start,
|
||||
"filament_short": bool(item.filament_short),
|
||||
"skip_filament_check": bool(item.skip_filament_check),
|
||||
"ams_mapping": ams_mapping_parsed,
|
||||
"plate_id": item.plate_id,
|
||||
"bed_levelling": item.bed_levelling,
|
||||
|
|
@ -539,6 +540,7 @@ async def add_to_queue(
|
|||
require_previous_success=data.require_previous_success,
|
||||
auto_off_after=data.auto_off_after,
|
||||
manual_start=data.manual_start,
|
||||
skip_filament_check=data.skip_filament_check,
|
||||
ams_mapping=ams_mapping_json,
|
||||
plate_id=data.plate_id,
|
||||
bed_levelling=data.bed_levelling,
|
||||
|
|
@ -1096,6 +1098,12 @@ async def start_queue_item(
|
|||
# Print Anyway / no deficit: clear the flags and let the scheduler dispatch.
|
||||
item.manual_start = False
|
||||
item.filament_short = False
|
||||
# Persist the user's "Print Anyway" decision so the scheduler does not
|
||||
# immediately re-flag this item on the next tick (#1698-followup). The
|
||||
# pre-fix behaviour bounced between "user said anyway" and
|
||||
# "scheduler re-blocked on same deficit" forever.
|
||||
if skip_filament_check:
|
||||
item.skip_filament_check = True
|
||||
# Credit the clicker as the item's owner when no prior owner is set —
|
||||
# VP-uploaded queue items arrive over FTP unattributed, so without this
|
||||
# the print log's User column stays blank even when auth is on
|
||||
|
|
|
|||
|
|
@ -935,6 +935,17 @@ async def run_migrations(conn):
|
|||
else:
|
||||
await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN filament_short BOOLEAN DEFAULT false")
|
||||
|
||||
# Migration: skip_filament_check flag on print_queue (#1698-followup).
|
||||
# Persists the user's "Print Anyway" acknowledgement so the scheduler
|
||||
# doesn't re-flag the item every tick after they've confirmed dispatch
|
||||
# despite the deficit warning. Set from the start route's skip_filament_check
|
||||
# query param and from PrintModal at queue-creation time. Postgres / SQLite
|
||||
# boolean default branch matches filament_short above.
|
||||
if is_sqlite():
|
||||
await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN skip_filament_check BOOLEAN DEFAULT 0")
|
||||
else:
|
||||
await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN skip_filament_check BOOLEAN DEFAULT false")
|
||||
|
||||
# Migration: Add queue_force_color_match column to virtual_printers (#1188).
|
||||
# Opt-in flag: when true, VP queue-mode uploads pin the per-slot type+color
|
||||
# from the 3MF onto the queue item's filament_overrides so the scheduler
|
||||
|
|
|
|||
|
|
@ -85,6 +85,14 @@ class PrintQueueItem(Base):
|
|||
# block automatically.
|
||||
filament_short: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# User has acknowledged the filament-shortage warning for this item
|
||||
# ("Print Anyway"). Set by the start route when the user passes
|
||||
# skip_filament_check=true, or at queue-creation time if PrintModal's
|
||||
# frontend deficit warning was acknowledged. Survives scheduler ticks so
|
||||
# the dispatch no longer bounces between "user said anyway" and
|
||||
# "scheduler re-flagged" (#1698-followup).
|
||||
skip_filament_check: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Tracking
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ class PrintQueueItemCreate(BaseModel):
|
|||
require_previous_success: bool = False
|
||||
auto_off_after: bool = False # Power off printer after print completes
|
||||
manual_start: bool = False # Requires manual trigger to start (staged)
|
||||
# Persistent "Print Anyway" acknowledgement (#1698-followup). When set,
|
||||
# PrintModal already showed the deficit warning and the user confirmed,
|
||||
# so the scheduler does not re-flag this item on the next tick.
|
||||
skip_filament_check: bool = False
|
||||
# AMS mapping: list of global tray IDs for each filament slot
|
||||
# Format: [5, -1, 2, -1] where position = slot_id-1, value = global tray ID (-1 = unused)
|
||||
ams_mapping: list[int] | None = None
|
||||
|
|
@ -96,6 +100,9 @@ class PrintQueueItemResponse(BaseModel):
|
|||
# (#1496). Display-only — the ▶ click recomputes deficit against live
|
||||
# spool state.
|
||||
filament_short: bool = False
|
||||
# User has acknowledged "Print Anyway" — scheduler skips the deficit check
|
||||
# for this item (#1698-followup).
|
||||
skip_filament_check: bool = False
|
||||
ams_mapping: list[int] | None = None
|
||||
plate_id: int | None = None # Plate ID for multi-plate 3MF files
|
||||
# Print options
|
||||
|
|
|
|||
|
|
@ -1846,6 +1846,14 @@ class PrintScheduler:
|
|||
since been swapped to one with enough material clears the flag here
|
||||
so the next scheduler tick dispatches it.
|
||||
"""
|
||||
# User has explicitly acknowledged the deficit ("Print Anyway") —
|
||||
# don't re-flag, don't even compute. Without this short-circuit the
|
||||
# scheduler bounces between "user said anyway" (route clears
|
||||
# manual_start) and "scheduler re-blocked" (this method re-flags it
|
||||
# on identical spool state) (#1698-followup).
|
||||
if item.skip_filament_check:
|
||||
return False
|
||||
|
||||
try:
|
||||
deficit = await compute_deficit_for_queue_item(db, item)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -147,6 +147,40 @@ class TestPrintQueueAPI:
|
|||
assert result["status"] == "pending"
|
||||
assert result["manual_start"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_add_to_queue_with_skip_filament_check(
|
||||
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
|
||||
):
|
||||
"""PrintModal "Print Anyway" persists skip_filament_check on creation (#1698-followup)."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory()
|
||||
|
||||
data = {
|
||||
"printer_id": printer.id,
|
||||
"archive_id": archive.id,
|
||||
"skip_filament_check": True,
|
||||
}
|
||||
response = await async_client.post("/api/v1/queue/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["skip_filament_check"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_add_to_queue_skip_filament_check_defaults_false(
|
||||
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
|
||||
):
|
||||
"""Default add-to-queue has skip_filament_check=False — no silent bypass."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory()
|
||||
|
||||
data = {"printer_id": printer.id, "archive_id": archive.id}
|
||||
response = await async_client.post("/api/v1/queue/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["skip_filament_check"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_add_to_queue_with_project_id(
|
||||
|
|
@ -583,6 +617,53 @@ class TestQueueStartEndpoint:
|
|||
# decision to print anyway.
|
||||
assert called_with == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_start_with_skip_flag_persists_acknowledgement(
|
||||
self,
|
||||
async_client: AsyncClient,
|
||||
queue_item_factory,
|
||||
db_session,
|
||||
):
|
||||
"""skip_filament_check=true sets the persistent flag on the queue item
|
||||
so the scheduler doesn't re-flag it on the next tick (#1698-followup).
|
||||
|
||||
Without persistence the route's flag-clearing only survives until the
|
||||
next scheduler tick re-runs the deficit check on identical spool
|
||||
state and re-promotes the item — the user has to click Play+Confirm
|
||||
every single tick.
|
||||
"""
|
||||
item = await queue_item_factory(manual_start=True, filament_short=True)
|
||||
assert item.skip_filament_check is False
|
||||
|
||||
response = await async_client.post(f"/api/v1/queue/{item.id}/start?skip_filament_check=true")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["skip_filament_check"] is True
|
||||
|
||||
await db_session.refresh(item)
|
||||
assert item.skip_filament_check is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_start_without_skip_flag_does_not_set_acknowledgement(
|
||||
self,
|
||||
async_client: AsyncClient,
|
||||
queue_item_factory,
|
||||
db_session,
|
||||
):
|
||||
"""A successful Play click with no deficit must NOT silently set the
|
||||
acknowledgement flag — only an explicit Print Anyway should.
|
||||
"""
|
||||
item = await queue_item_factory(manual_start=False, filament_short=False)
|
||||
assert item.skip_filament_check is False
|
||||
|
||||
response = await async_client.post(f"/api/v1/queue/{item.id}/start")
|
||||
assert response.status_code == 200
|
||||
|
||||
await db_session.refresh(item)
|
||||
assert item.skip_filament_check is False
|
||||
|
||||
|
||||
class TestQueueCancelEndpoint:
|
||||
"""Tests for the /queue/{item_id}/cancel endpoint."""
|
||||
|
|
|
|||
|
|
@ -117,3 +117,41 @@ async def test_helper_exception_does_not_wedge_dispatch(scheduler, db_session, q
|
|||
assert blocked is False
|
||||
await db_session.refresh(item)
|
||||
assert item.filament_short is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skip_filament_check_short_circuits_without_compute(scheduler, db_session, queue_item):
|
||||
"""User clicked Print Anyway (skip_filament_check=True): no compute, no flag (#1698-followup).
|
||||
|
||||
Pre-fix the scheduler re-ran the deficit check on every tick, re-set
|
||||
manual_start/filament_short to True, and the item bounced between
|
||||
"user said anyway" (route clears flags) and "scheduler re-blocked"
|
||||
forever. With the persistent acknowledgement flag the scheduler bails
|
||||
early without even touching the deficit helper.
|
||||
"""
|
||||
item = await queue_item(skip_filament_check=True)
|
||||
compute_mock = AsyncMock(
|
||||
return_value=[
|
||||
FilamentDeficit(
|
||||
slot_id=1,
|
||||
ams_id=0,
|
||||
tray_id=0,
|
||||
filament_type="PLA",
|
||||
required_grams=270.0,
|
||||
remaining_grams=200.0,
|
||||
),
|
||||
]
|
||||
)
|
||||
with patch(
|
||||
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
||||
compute_mock,
|
||||
):
|
||||
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
||||
|
||||
assert blocked is False
|
||||
compute_mock.assert_not_awaited()
|
||||
await db_session.refresh(item)
|
||||
# Flags must not get re-set by the scheduler now that the user has
|
||||
# acknowledged the deficit.
|
||||
assert item.filament_short is False
|
||||
assert item.manual_start is False
|
||||
|
|
|
|||
|
|
@ -1898,6 +1898,11 @@ export interface PrintQueueItem {
|
|||
// any required slot's grams (#1496). Surfaced on the queue row as a
|
||||
// "filament short" badge; cleared on a successful ▶ click (live recheck).
|
||||
filament_short: boolean;
|
||||
// Persistent "Print Anyway" acknowledgement — once true the scheduler
|
||||
// skips the deficit check for this item (#1698-followup). Set by the
|
||||
// start route when skip_filament_check=true, or at queue creation if
|
||||
// PrintModal's deficit warning was acknowledged.
|
||||
skip_filament_check: boolean;
|
||||
ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
|
||||
filament_overrides: Array<{ slot_id: number; type: string; color: string; color_name?: string; force_color_match?: boolean }> | null; // Filament overrides for model-based assignment
|
||||
plate_id: number | null; // Plate ID for multi-plate 3MF files
|
||||
|
|
@ -1966,6 +1971,9 @@ export interface PrintQueueItemCreate {
|
|||
require_previous_success?: boolean;
|
||||
auto_off_after?: boolean;
|
||||
manual_start?: boolean; // Requires manual trigger to start (staged)
|
||||
// PrintModal "Print Anyway" on the deficit warning — persisted so the
|
||||
// scheduler doesn't immediately re-flag this item (#1698-followup).
|
||||
skip_filament_check?: boolean;
|
||||
ams_mapping?: number[] | null; // AMS slot mapping for multi-color prints
|
||||
plate_id?: number | null; // Plate ID for multi-plate 3MF files
|
||||
// Print options
|
||||
|
|
|
|||
|
|
@ -653,6 +653,10 @@ export function PrintModal({
|
|||
auto_off_after: scheduleOptions.autoOffAfter,
|
||||
gcode_injection: scheduleOptions.gcodeInjection,
|
||||
manual_start: scheduleOptions.scheduleType === 'manual',
|
||||
// When the user clicks "Print Anyway" on the frontend deficit warning,
|
||||
// persist that acknowledgement so the scheduler doesn't immediately
|
||||
// re-flag the item on its first dispatch tick (#1698-followup).
|
||||
skip_filament_check: options?.skipFilamentCheck === true ? true : undefined,
|
||||
ams_mapping: printerId ? getMappingForPrinter(printerId) : undefined,
|
||||
plate_id: plateOverride !== undefined ? plateOverride : selectedPlate,
|
||||
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-lEQPGYKn.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Dx3eHcCx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BvmIMSUd.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue