mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
PR A/B turned the slice modal's preset bundle into a one-click dispatch
with a pinned target printer. PR C closes the original issue: operators
type in a number of copies, Bambuddy slices once and distributes prints
across a fleet per the pipeline's chosen fanout strategy. A new dashboard
surfaces every run with filters, expandable per-copy status, cancel,
and retry-failed-copies. WS pushes keep everything live.
Backend
- copies field on POST /run, capped by new pipeline_max_copies setting
(default 50, hard cap 1000). PipelineRun.parent_run_id chains retries.
- SlicerPipelineUpdate accepts target_kind (specific_printer /
printer_class), target_model_class, fanout_strategy.
- Eligibility matcher branches: class-targeting enumerates matching
Printer rows, runs per-printer checks via a status_lookup closure,
returns printer_reports[]. New issue kinds: no_class_matches,
class_not_set.
- _pick_assignments distributes copies per strategy:
- max_parallel: target_model set, printer_id None — scheduler picks
- round_robin: copy i → eligible[i % N], fixed printer_id
- fill_one_first: all copies pinned to eligible[0]
All three reuse the slice-once path through slice_dispatch.enqueue.
- New routes:
- GET /pipeline-runs (paginated, filterable by pipeline + status)
- POST /pipeline-runs/{id}/retry-failed (creates child run with
copies = failed+cancelled count, parent_run_id set)
- Cancel cascades to all N queue entries (only pending/queued)
- _roll_up_run_status computes run-level status from per-job statuses;
introduces partial_failure for "some completed, some failed".
- ws_manager.broadcast_to_user emits pipeline_run_updated on every
state transition with the full materialised response.
Frontend
- Pipeline editor: target_kind radio + class picker (filtered to
installed models) + fanout-strategy radio. Read-only row shows
"X1C · Round robin" for class pipelines.
- RunWithPipelineModal: copies number input bounded by
settings.pipeline_max_copies. Accepts class-targeted pipelines.
- Settings → Workflow → Queue & Dispatch: new "Slicer Pipeline limits"
card with the max-copies input.
- New /pipelines/runs dashboard page (sidebar entry, gated on
pipelines:read). Two-filter dropdown, 25-per-page pagination, per-row
expandable to job list, Cancel + Retry-failed buttons.
- useWebSocket case for pipeline_run_updated invalidates both
pipeline-runs-all and pipeline-runs/{id} query keys.
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
"""Pydantic schemas for the Slicer Pipeline API (#1425, PR A).
|
|
|
|
A pipeline bundles printer / process / filament(s) / bed-type picks under a
|
|
reusable name. PR A surfaces only the bundle; target_kind / target_printer_id /
|
|
target_model_class / fanout_strategy are persisted but the API treats them as
|
|
opaque defaults — they come alive in PR B (single-target dispatch) and PR C
|
|
(multi-copy + class targeting + fanout).
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from backend.app.schemas.slicer import PresetRef
|
|
|
|
|
|
class SlicerPipelineBase(BaseModel):
|
|
"""Fields editable on create + update."""
|
|
|
|
name: str = Field(..., min_length=1, max_length=200)
|
|
description: str | None = Field(default=None, max_length=1000)
|
|
|
|
printer_preset: PresetRef
|
|
process_preset: PresetRef
|
|
filament_presets: list[PresetRef] = Field(
|
|
...,
|
|
min_length=1,
|
|
description="One PresetRef per AMS slot. Order matches the source plate's filament-slot order.",
|
|
)
|
|
bed_type: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
class SlicerPipelineCreate(SlicerPipelineBase):
|
|
"""Payload for POST /slicer-pipelines."""
|
|
|
|
|
|
class SlicerPipelineUpdate(BaseModel):
|
|
"""Payload for PUT /slicer-pipelines/{id}. All fields optional; only those
|
|
present are written. Preset and filament list are replaced wholesale when
|
|
set (we don't support partial filament-slot edits)."""
|
|
|
|
name: str | None = Field(default=None, min_length=1, max_length=200)
|
|
description: str | None = Field(default=None, max_length=1000)
|
|
printer_preset: PresetRef | None = None
|
|
process_preset: PresetRef | None = None
|
|
filament_presets: list[PresetRef] | None = Field(default=None, min_length=1)
|
|
bed_type: str | None = Field(default=None, max_length=64)
|
|
|
|
# PR B target binding. ``target_kind='specific_printer'`` requires
|
|
# ``target_printer_id`` to be set OR cleared in the same payload (route
|
|
# handler enforces). ``target_kind='printer_class'`` is wired by PR C
|
|
# together with ``target_model_class`` (a Bambu model code like 'X1C')
|
|
# and the fanout strategy that distributes copies across matching
|
|
# printers.
|
|
target_kind: Literal["specific_printer", "printer_class"] | None = None
|
|
target_printer_id: int | None = None
|
|
target_model_class: str | None = Field(default=None, max_length=20)
|
|
fanout_strategy: Literal["max_parallel", "fill_one_first", "round_robin"] | None = None
|
|
|
|
|
|
class SlicerPipelineResponse(SlicerPipelineBase):
|
|
"""A single pipeline as returned by the API."""
|
|
|
|
id: int
|
|
created_by: int | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
# Echoed for PR B+ readiness; PR A always returns the persisted defaults.
|
|
target_kind: Literal["specific_printer", "printer_class"] = "printer_class"
|
|
target_printer_id: int | None = None
|
|
target_model_class: str | None = None
|
|
fanout_strategy: Literal["max_parallel", "fill_one_first", "round_robin"] = "max_parallel"
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SlicerPipelineListResponse(BaseModel):
|
|
"""Wraps the list so the response stays additive when run/job counts get
|
|
surfaced in PR B+ (e.g. a ``meta`` field for last-run timestamps)."""
|
|
|
|
pipelines: list[SlicerPipelineResponse] = []
|