Files
bambuddy/backend/app/models/print_queue.py
maziggy 85fbd7fc35 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.
2026-06-09 10:43:07 +02:00

122 lines
6.5 KiB
Python

from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.core.database import Base
class PrintQueueItem(Base):
"""Print queue item for scheduled/queued prints."""
__tablename__ = "print_queue"
id: Mapped[int] = mapped_column(primary_key=True)
# Links
printer_id: Mapped[int | None] = mapped_column(ForeignKey("printers.id", ondelete="CASCADE"), nullable=True)
# Target printer model for model-based assignment (mutually exclusive with printer_id)
# When set, scheduler assigns to any idle printer of matching model
target_model: Mapped[str | None] = mapped_column(String(50), nullable=True)
# Target location filter for model-based assignment (only used with target_model)
# When set, only printers in this location are considered
target_location: Mapped[str | None] = mapped_column(String(100), nullable=True)
# Required filament types for model-based assignment (JSON array, e.g., '["PLA", "PETG"]')
# Used by scheduler to validate printer has compatible filaments loaded
required_filament_types: Mapped[str | None] = mapped_column(Text, nullable=True)
# Waiting reason - explains why a model-based job hasn't started yet
# Set by scheduler when no matching printer is available
waiting_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
# Either archive_id OR library_file_id must be set (archive created at print start from library file)
archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id", ondelete="CASCADE"), nullable=True)
library_file_id: Mapped[int | None] = mapped_column(
ForeignKey("library_files.id", ondelete="CASCADE"), nullable=True
)
project_id: Mapped[int | None] = mapped_column(ForeignKey("projects.id", ondelete="SET NULL"), nullable=True)
batch_id: Mapped[int | None] = mapped_column(ForeignKey("print_batches.id", ondelete="SET NULL"), nullable=True)
# Scheduling
position: Mapped[int] = mapped_column(Integer, default=0) # Queue order
scheduled_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # None = ASAP
manual_start: Mapped[bool] = mapped_column(Boolean, default=False) # Requires manual trigger to start
# Conditions
require_previous_success: Mapped[bool] = mapped_column(Boolean, default=False)
# Power management
auto_off_after: Mapped[bool] = mapped_column(Boolean, default=False) # Power off printer after print
# AMS mapping: JSON array 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: Mapped[str | None] = mapped_column(Text, nullable=True)
# Filament overrides for model-based assignment: JSON array of override objects
# Format: '[{"slot_id": 1, "type": "PLA", "color": "#FFFFFF"}]'
# Only slots with overrides are included (sparse). null = use original 3MF values.
filament_overrides: Mapped[str | None] = mapped_column(Text, nullable=True)
# Plate ID for multi-plate 3MF files (1-indexed, None = auto-detect/plate 1)
plate_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Shortest-job-first scheduling
print_time_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) # Cached from archive/library
been_jumped: Mapped[bool] = mapped_column(Boolean, default=False) # Starvation guard for SJF
# Auto-print G-code injection (#422)
gcode_injection: Mapped[bool] = mapped_column(Boolean, default=False)
# Print options
bed_levelling: Mapped[bool] = mapped_column(Boolean, default=True)
flow_cali: Mapped[bool] = mapped_column(Boolean, default=False)
vibration_cali: Mapped[bool] = mapped_column(Boolean, default=True)
layer_inspect: Mapped[bool] = mapped_column(Boolean, default=False)
timelapse: Mapped[bool] = mapped_column(Boolean, default=False)
use_ams: Mapped[bool] = mapped_column(Boolean, default=True)
# Nozzle offset calibration — dual-nozzle printers only, MQTT-gated (#1682)
nozzle_offset_cali: Mapped[bool] = mapped_column(Boolean, default=True)
# Status: pending, printing, completed, failed, skipped, cancelled
status: Mapped[str] = mapped_column(String(20), default="pending")
# Set by the dispatch scheduler when the assigned spool can't satisfy
# this print's per-slot filament weight (#1496). Display-only flag — the
# actual deficit is recomputed live every time the user clicks ▶, so
# swapping a spool to a fuller one between flag and dispatch clears the
# 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)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
# User tracking (who added this to the queue)
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
# Relationships
printer: Mapped["Printer"] = relationship()
archive: Mapped["PrintArchive | None"] = relationship()
library_file: Mapped["LibraryFile | None"] = relationship()
project: Mapped["Project | None"] = relationship(back_populates="queue_items")
batch: Mapped["PrintBatch | None"] = relationship(back_populates="queue_items")
created_by: Mapped["User | None"] = relationship()
from backend.app.models.archive import PrintArchive # noqa: E402
from backend.app.models.library import LibraryFile # noqa: E402
from backend.app.models.print_batch import PrintBatch # noqa: E402
from backend.app.models.printer import Printer # noqa: E402
from backend.app.models.project import Project # noqa: E402
from backend.app.models.user import User # noqa: E402