mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
feat(slicer): Pipelines — archive entry point + slicer progress toast (#1425 PR B follow-up)
Two real gaps from the PR B drop:
1. Run-with-pipeline only existed in the file manager. Operators who keep
working files in archives had to copy them to the library to use a
pipeline.
2. Triggering a slice via a pipeline produced a silent multi-second-to-
minute wait. The manual SliceModal flow shows the sticky
"Slicing X - Generating G-code 75%" persistent toast; the pipeline
path went through asyncio.create_task directly and never registered
with SliceJobTracker.
Archive entry point
- POST /slicer-pipelines/{id}/check-eligibility and /run accept
source_archive_id as an alternative to source_library_file_id (XOR,
enforced by Pydantic validator).
- PipelineRun.source_archive_id is a new nullable FK column with the
ALTER TABLE migration in run_migrations (idempotent via _safe_execute,
works on SQLite + Postgres).
- _resolve_source branches: archive path reads source_3mf_path with
fallback to file_path, mirroring routes/archives.py.
- ArchiveCard's context menu picks up a "Run with pipeline" item next to
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
- Path-safety: SEC-PATH-OK markers added at both LibraryFile.file_path
and archive.source_3mf_path join sites, citing the upload-time
validators.
Progress toast
- Pipeline orchestration is now the `run` callable of a
slice_dispatch.enqueue call — the same dispatcher SliceModal uses —
instead of a bare asyncio.create_task. The SliceJob lifecycle drives
the existing progress toast end to end with no separate notification
surface for pipeline runs.
- PipelineRun.slice_job_id is set before the 202 returns.
- RunWithPipelineModal calls useSliceJobTracker().trackJob() from
runMutation.onSuccess.
- RunWithPipelineModal source prop is now {kind, id, filename}
mirroring SliceModal.SliceSource; api.checkPipelineEligibility +
api.runPipeline take a discriminated-union source argument.
This commit is contained in:
+112
-1
@@ -1530,11 +1530,87 @@ export interface SlicerPipelineCreateRequest {
|
||||
filament_presets: PresetRef[];
|
||||
bed_type?: string | null;
|
||||
}
|
||||
export type SlicerPipelineUpdateRequest = Partial<SlicerPipelineCreateRequest>;
|
||||
export type SlicerPipelineUpdateRequest = Partial<SlicerPipelineCreateRequest> & {
|
||||
target_kind?: 'specific_printer' | 'printer_class';
|
||||
// ``target_printer_id: 0`` means "clear the target" — the backend maps that
|
||||
// to null. Use null in TypeScript for the same intent.
|
||||
target_printer_id?: number | null;
|
||||
};
|
||||
export interface SlicerPipelinesListResponse {
|
||||
pipelines: SlicerPipeline[];
|
||||
}
|
||||
|
||||
// Slicer Pipeline runs (#1425 PR B)
|
||||
export type PipelineEligibilityKind =
|
||||
| 'printer_not_set'
|
||||
| 'printer_not_found'
|
||||
| 'printer_disabled'
|
||||
| 'printer_offline'
|
||||
| 'filament_type_mismatch'
|
||||
| 'filament_color_mismatch'
|
||||
| 'ams_slot_missing'
|
||||
| 'filament_unverified';
|
||||
export interface PipelineEligibilityIssue {
|
||||
kind: PipelineEligibilityKind;
|
||||
slot_index: number | null;
|
||||
expected: string | null;
|
||||
actual: string | null;
|
||||
}
|
||||
export interface PipelineEligibilityReport {
|
||||
ok: boolean;
|
||||
target_printer_id: number | null;
|
||||
target_printer_name: string | null;
|
||||
issues: PipelineEligibilityIssue[];
|
||||
}
|
||||
export interface PipelineJob {
|
||||
id: number;
|
||||
pipeline_run_id: number;
|
||||
copy_index: number;
|
||||
assigned_printer_id: number | null;
|
||||
assigned_printer_name: string | null;
|
||||
queue_entry_id: number | null;
|
||||
status:
|
||||
| 'pending'
|
||||
| 'awaiting_printer'
|
||||
| 'queued'
|
||||
| 'printing'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'cancelled';
|
||||
error_message: string | null;
|
||||
dispatched_at: string | null;
|
||||
completed_at: string | null;
|
||||
}
|
||||
export interface PipelineRun {
|
||||
id: number;
|
||||
pipeline_id: number | null;
|
||||
pipeline_name: string | null;
|
||||
source_library_file_id: number | null;
|
||||
source_archive_id: number | null;
|
||||
source_filename: string | null;
|
||||
copies: number;
|
||||
status:
|
||||
| 'queued'
|
||||
| 'slicing'
|
||||
| 'dispatching'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'cancelled';
|
||||
slice_job_id: number | null;
|
||||
sliced_library_file_id: number | null;
|
||||
eligibility_overridden: boolean;
|
||||
error_message: string | null;
|
||||
created_by: number | null;
|
||||
created_at: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
jobs: PipelineJob[];
|
||||
}
|
||||
export interface PipelineRunListResponse {
|
||||
runs: PipelineRun[];
|
||||
}
|
||||
|
||||
export interface SliceResponse {
|
||||
library_file_id: number;
|
||||
name: string;
|
||||
@@ -3035,6 +3111,7 @@ export type Permission =
|
||||
| 'api_keys:read' | 'api_keys:create' | 'api_keys:update' | 'api_keys:delete'
|
||||
| 'users:read' | 'users:create' | 'users:update' | 'users:delete'
|
||||
| 'groups:read' | 'groups:create' | 'groups:update' | 'groups:delete'
|
||||
| 'pipelines:read' | 'pipelines:write' | 'pipelines:run'
|
||||
| 'websocket:connect';
|
||||
|
||||
// Group types
|
||||
@@ -6250,6 +6327,40 @@ export const api = {
|
||||
}),
|
||||
deleteSlicerPipeline: (id: number) =>
|
||||
request<void>(`/slicer-pipelines/${id}`, { method: 'DELETE' }),
|
||||
checkPipelineEligibility: (
|
||||
pipelineId: number,
|
||||
source: { kind: 'libraryFile'; id: number } | { kind: 'archive'; id: number },
|
||||
) =>
|
||||
request<PipelineEligibilityReport>(`/slicer-pipelines/${pipelineId}/check-eligibility`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
source.kind === 'libraryFile'
|
||||
? { source_library_file_id: source.id }
|
||||
: { source_archive_id: source.id },
|
||||
),
|
||||
}),
|
||||
runPipeline: (
|
||||
pipelineId: number,
|
||||
source: { kind: 'libraryFile'; id: number } | { kind: 'archive'; id: number },
|
||||
force = false,
|
||||
) =>
|
||||
request<PipelineRun>(`/slicer-pipelines/${pipelineId}/run`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...(source.kind === 'libraryFile'
|
||||
? { source_library_file_id: source.id }
|
||||
: { source_archive_id: source.id }),
|
||||
force,
|
||||
}),
|
||||
}),
|
||||
listPipelineRuns: (pipelineId: number, limit = 5) =>
|
||||
request<PipelineRunListResponse>(
|
||||
`/slicer-pipelines/${pipelineId}/runs?limit=${limit}`,
|
||||
),
|
||||
getPipelineRun: (runId: number) =>
|
||||
request<PipelineRun>(`/pipeline-runs/${runId}`),
|
||||
cancelPipelineRun: (runId: number) =>
|
||||
request<PipelineRun>(`/pipeline-runs/${runId}/cancel`, { method: 'POST' }),
|
||||
|
||||
// Canonical Bambu printer-model registry — "Bambu Lab <model>" → short code.
|
||||
// Single source of truth shared with backend (PRINTER_MODEL_MAP); the
|
||||
|
||||
Reference in New Issue
Block a user