feat(slicer): Pipelines — multi-copy + class targeting + fanout + runs dashboard + retry-failed + WS updates (#1425 PR C — completes the v3 design)

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.
This commit is contained in:
maziggy
2026-06-27 16:52:05 +02:00
parent 4bbf0f031e
commit 3ef197e4e0
37 changed files with 2476 additions and 364 deletions
+13
View File
@@ -11,6 +11,10 @@ interface WebSocketMessage {
data?: Record<string, unknown>;
printer_name?: string;
missing_slots?: Array<{ slot?: string }>;
// Slicer Pipeline run events (#1425 PR C). ``run`` carries the full
// PipelineRunResponse payload — typed loosely here so the WebSocket hook
// doesn't pull the full client.ts types in.
run?: { pipeline_id?: number | null };
}
export function useWebSocket() {
@@ -399,6 +403,15 @@ export function useWebSocket() {
case 'queue_item_failed':
window.dispatchEvent(new CustomEvent('bambuddy:dispatch-toast', { detail: message }));
break;
// Slicer Pipeline runs (#1425 PR C). State transitions on the run
// refresh both the dashboard list AND the per-pipeline "Last run"
// chip in Settings → Pipelines.
case 'pipeline_run_updated':
queryClient.invalidateQueries({ queryKey: ['pipeline-runs-all'] });
if (message.run?.pipeline_id) {
queryClient.invalidateQueries({ queryKey: ['pipeline-runs', message.run.pipeline_id] });
}
break;
}
}, [queryClient, debouncedInvalidate, throttledPrinterStatusUpdate, showToast, t]);