diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbd672ce..3157fc4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ All notable changes to Bambuddy will be documented in this file. - **HMS Action buttons now reach the printer (#1830, H2D/H2C wrong-plate verification)** — The HMS Actions feature shipped in #1743 looked correct at the publish layer but the firmware silently dropped the commands at the printer, so clicking "Stop printing", "Problem solved and resume", or "Ignore and resume" did nothing visible on the live H2D — the modal kept reappearing, the print stayed paused, and the route still returned `200 OK`. Three independent bugs combined into one user-facing failure. **(1) Wrong command shape for resume / stop.** `hms_resume()` and `hms_stop()` sent the documented-but-not-actually-used `{"err": , "param": "reserve", "job_id": , ...}` shape that BambuStudio never produces. Bambu firmware rejects this silently — verified by injecting candidate shapes on `device//request` against a live H2D paused on a wrong-plate HMS: the `err`-bearing shape held PAUSE → PAUSE for the full window, the plain `{"print":{"command":"stop","param":"","sequence_id":"0"}}` transitioned PAUSE → FAILED in 1.7s, the same plain `resume` transitioned PAUSE → RUNNING in <2s. Fix: both helpers send the plain shape now, no `err`, no `job_id`, no `param:"reserve"`. **(2) `IGNORE_RESUME` mapped to the wrong command for paused prints.** The original mapping dispatched `idle_ignore` for both `IGNORE_RESUME` and `NO_REMINDER_NEXT_TIME`. `idle_ignore` is BambuStudio's "dismiss this warning" command and only works for non-pause warnings — verified against the H2D, idle_ignore on a paused print is silently rejected regardless of `err`. `hms_ignore()` now branches on `self.state.gcode_state == "PAUSE"`: paused → dispatch plain `resume` (which is what the button actually means on a paused print), running/idle → keep `idle_ignore` with the `type=0/1` persistence flag. `DONT_REMIND_NEXT_TIME` on PAUSE degrades to resume too — the "don't remind" flag can't ride along on a resume but the user's clicked-action intent (continue printing) is honoured. **(3) 64-bit `hms[]`-array faults truncated to a non-matching `err` (#1830 §(1)).** The hms[] parser at line 2740 built the short code as `f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}"`, discarding 32 of the 64 bits of the fault identifier. For codes whose full form is e.g. `0C00_0300_0002_000C`, the truncated `0C00000C` doesn't match what the firmware compares against in `idle_ignore`. New `HMSError.full_code` field carries the canonical hex identifier — 16 chars `f"{attr:08X}{code:08X}"` for hms[]-sourced faults, 8 chars `f"{print_error:08X}"` for print_error-sourced faults (which are already 32-bit). Catalog lookup tries the 16-char form first and falls back to the 8-char short code so existing entries keep matching. Frontend echoes `error.full_code` back as `HmsActionBody.print_error` instead of recomputing the short code; the schema's pattern relaxes to `^[0-9A-Fa-f]{8}([0-9A-Fa-f]{8})?$` to accept both lengths. **(4) Masking failure — publish-success returned as printer-ack (#1830 §(3)).** `execute_hms_action` returned True the moment the publish succeeded, so any of the three bugs above produced `200 OK` while the printer ignored the command and the modal kept popping. The `/hms/execute-action` route now snapshots `(gcode_state, print_error, hms_errors count)` before dispatch, awaits `HMS_ACTION_ACK_WAIT_SECONDS` (default 2.5s, module-level so tests override), and returns `502 "Printer did not acknowledge HMS action within 2.5s"` if none of those moved. Every accepted HMS action mutates at least one of the three, so this is a clean signal. **Empirical verification.** A test harness on `device/0948BB540200427/request` confirmed each shape against the live H2D: a print sent with deliberately-wrong build plate raises `print_error=0x05008051` ("Detected build plate is not the same as the Gcode file"), the printer enters `gcode_state=PAUSE`, and the new command shapes transition out correctly. The current Bambuddy code (before this fix) failed to act on every button. **Tests.** `test_hms_actions.py` shape assertions rewritten — `test_resume_is_plain_no_err_no_job_id`, `test_stop_is_plain_no_err_no_job_id`, `test_ignore_resume_dispatches_resume_when_print_paused`, `test_ignore_resume_uses_idle_ignore_when_not_paused`, `test_dont_remind_dispatches_resume_when_paused`, `test_dont_remind_uses_idle_ignore_type_one_when_not_paused`, `test_idle_ignore_accepts_16_char_full_code`. New `TestHMSFullCode` class in `test_bambu_mqtt.py` pins the parser contract — `test_hms_array_path_populates_16_char_full_code`, `test_print_error_path_populates_8_char_full_code`, `test_hms_array_catalog_lookup_tries_16_char_first`, `test_hms_array_catalog_falls_back_to_8_char`. New integration cases in `test_printers_api.py` — `test_execute_hms_action_no_printer_ack_returns_502`, `test_execute_hms_action_accepts_16_char_full_code`. The malformed-input test now covers 9- and 15-char rejections (the relaxed pattern accepts 8 OR 16, nothing in between). `pytest -n 30 backend/tests/unit/services/test_hms_actions.py backend/tests/unit/services/test_bambu_mqtt.py backend/tests/unit/services/test_printer_manager.py backend/tests/integration/test_printers_api.py` green (509 + 181). `ruff check` clean. Frontend `npm run build` clean. **Scope.** No DB migration. No new permission. No new i18n key — the frontend toast on action failure already uses the existing `hmsErrors.actionFailed` string, which now gets the more accurate "Printer did not acknowledge" message instead of "Failed to send action". The `HMSError.full_code` field defaults to `""` so old in-memory state surviving a backend upgrade (without an MQTT reconnect) degrades to the existing 8-char short code via the frontend's `||` fallback. ### Added +- **Slicer Pipelines — Archive entry point + progress toast for pipeline-driven slicing (#1425 PR B follow-up)** — Two real gaps from the PR B drop. (1) The Run-with-pipeline button only existed in the file manager — operators who keep their working files in archives had to copy them out 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 has the sticky `Slicing X — Generating G-code 75%` persistent toast, the pipeline path went through `asyncio.create_task` directly and never registered with `SliceJobTracker`. **Fix.** (1) `POST /slicer-pipelines/{id}/check-eligibility` and `POST /slicer-pipelines/{id}/run` now accept `source_archive_id` as an alternative to `source_library_file_id` (XOR — Pydantic validator rejects both-set and neither-set), and the eligibility-check and orchestration paths branch via `_resolve_source` which reads `archive.source_3mf_path` with a fallback to `archive.file_path`. `PipelineRun.source_archive_id` is a new nullable FK column (Postgres + SQLite `ALTER TABLE` in `run_migrations` — idempotent via `_safe_execute`). `PipelineRunResponse` echoes the field. ArchiveCard's context menu picks up a `Run with pipeline` item alongside the existing Slice action (only on source archives — gcode archives already have Print + Open in BambuStudio), gated on `useSlicerApi` + `pipelines:run`. Path-safety: `Path(base_dir) / archive.source_3mf_path` carries a `SEC-PATH-OK` marker citing the upload-time validator at `_resolve_source_3mf_path` (same comment style as `routes/archives.py:3955`); the `LibraryFile.file_path` site gets the same treatment. (2) The pipeline orchestrator is now the `run` callable of a `slice_dispatch.enqueue` call — the same dispatcher the manual `SliceModal` flow uses — instead of a bare `asyncio.create_task`. The SliceJob's lifecycle (`pending → running → completed/failed`) drives the existing progress toast end to end: same persistent toast, same `Generating G-code 75%` weave from the sidecar's `--pipe` channel, same auto-replace with a transient success/error toast on terminal. `PipelineRun.slice_job_id` is set on the run row before the route returns 202, so the frontend can call `useSliceJobTracker().trackJob(slice_job_id, source.kind, source.filename)` from `RunWithPipelineModal`'s `runMutation.onSuccess` — same one-call surface that `SliceModal`'s slice mutation already uses. (3) `RunWithPipelineModal`'s `source` prop is now `{kind: 'libraryFile' | 'archive', id, filename}` (mirrors `SliceModal.SliceSource`); `api.checkPipelineEligibility` + `api.runPipeline` take a discriminated-union source argument and route to the right backend field. `PipelineRun` TS type grows `source_archive_id`. **Tests.** Three new backend cases in `test_pipeline_runs_api.py` — archive-source happy path (creates a PrintArchive row + on-disk file, posts with `source_archive_id`, verifies the response carries `source_archive_id` + `slice_job_id` from a stubbed `slice_dispatch.enqueue`), XOR rejection both-set, XOR rejection neither-set. The existing three run/cancel cases were updated to patch `backend.app.services.slice_dispatch.slice_dispatch.enqueue` (the new mock target) instead of the removed `_run_pipeline_orchestration` helper, and the run-happy-path now asserts `slice_job_id == 9001` arrives on the response. One new frontend case in `RunWithPipelineModal.test.tsx` pins the archive flow end to end (`checkPipelineEligibility` called with `{kind: 'archive', id: 7}`, then `runPipeline` with the same). The existing fast/slow path tests were updated to wrap in `SliceJobTrackerProvider` (the new `useSliceJobTracker` hook requires it) and to assert the new discriminated-union source argument. **Suites.** `pytest -n 30 backend/tests/` 6533/6533 green; `npx vitest run` 2279/2279 green (172 files); `npm run build` clean; `python -m ruff check backend/` clean; `node scripts/check-i18n-parity.mjs` clean. **Scope.** No new i18n keys — both fixes reuse the existing PR B keys. No new permission. The archive flow only branches at the source-resolution layer; everything downstream (eligibility, slice, queue dispatch) is the same code path the library flow uses. PR C scope (multi-copy + class targeting + fanout) is unchanged. + +- **Slicer Pipelines — Run a pipeline on a file with one click (#1425 PR B)** — PR A landed the bundle (save & apply preset slots in the SliceModal). PR B turns that bundle into an actual one-click dispatcher: file-manager rows now carry a `Run with pipeline ▾` button that slices the source through the pipeline's pinned printer/process/filament/bed-type combo and enqueues the print on the pipeline's pinned target printer. **Scope.** Single-target dispatch — `target_kind='specific_printer'` only. Multi-copy batch + class targeting + fanout strategies are PR C; the schema columns are already in place from PR A so PR C is code-only. **Backend.** Two new SQLAlchemy models — `PipelineRun` (one row per Run-pipeline click, carries the slice_job + sliced_library_file ids + snapshot status) and `PipelineJob` (one row per copy; PR B always 1, PR C variable). Soft-link to slicer_pipelines via `ondelete='SET NULL'` so run history survives a pipeline delete; same for source_library_file. **`status`** on the run is a *persisted snapshot* that gets terminal transitions written (slice failure, cancel, completion); in-flight reads roll up the live state of the linked queue entry via `_compute_run_status` — that keeps the status accurate (`pending → printing → completed`) without a background watcher writing on every queue tick. **Eligibility matcher** at `services/pipeline_eligibility.py` — given a pipeline + the live `PrinterState` from `printer_manager.get_status`, returns a structured report with typed issues: `printer_not_set`, `printer_not_found`, `printer_disabled` (from `Printer.is_active` shipped with #1476), `printer_offline`, `filament_type_mismatch`, `filament_color_mismatch`, `ams_slot_missing`, `filament_unverified` (cloud/standard tier presets can't be statically read here; surface as info, not a block). Canonical filament-type map mirrors `print_scheduler._canonical_filament_type` so `PLA Basic` / `PLA Matte` / etc. all collapse to `PLA` for the type comparison; colour normalises to six-hex-digit lowercase. **Eligibility is lenient with confirmation** — the report drives the frontend confirmation modal, but the user can `Run anyway` (sets `eligibility_overridden=True` on the run row so the audit trail shows which runs bypassed pre-flight). **Routes.** Two new routers — `pipeline_run_create_router` mounted under `/slicer-pipelines` (POST `/{id}/check-eligibility`, POST `/{id}/run`, GET `/{id}/runs?limit=N`) and `pipeline_run_router` at `/pipeline-runs` (GET `/{id}`, POST `/{id}/cancel`). `POST /run` returns 202 with the run shape; orchestration happens in a fire-and-forget `asyncio.create_task` that opens its own DB session (the request's session is closed by the time it runs) and walks: status='slicing' → `slice_and_persist` with the pipeline's `SliceRequest` → on success `status='dispatching'` + insert `PrintQueueItem` with `printer_id=target_printer_id, library_file_id=sliced_library_file_id`. The existing scheduler picks the queue entry up on its next tick. `POST /run` with eligibility issues and no `force` returns 409 with the report inside `detail` so the frontend can render the same confirmation modal it would for an explicit pre-flight; `force=true` bypasses the 409 but a missing `target_printer_id` still 400s (defence in depth — the UI can't enqueue the print without a target). `POST /cancel` is idempotent on terminal states and cascades to the linked queue entry when its status is still `pending` / `queued` (in-flight prints continue — operator must Stop on the printer itself). **SlicerPipeline.target_kind / target_printer_id** become writable via `PUT /slicer-pipelines/{id}` — the schema accepts both fields, the route treats `target_printer_id=0` as "clear" (the empty-` + {printers.map((p) => ( + + ))} + + ) : ( <> @@ -277,6 +351,43 @@ function PipelineRow({ {pipeline.bed_type} )} +
+ + + {t('settings.pipelines.field.targetPrinter', 'Target printer')}: + {' '} + {targetPrinter ? ( + {targetPrinter.name} + ) : ( + + {t('settings.pipelines.noTargetHint', 'Set a target printer to run this')} + + )} +
+ + )} + + {!editing && lastRun && ( +
+ + {t('settings.pipelines.runs.lastRun', 'Last run')}: + {' '} + + {lastRun.created_at && ( + + · {new Date(lastRun.created_at).toLocaleString()} + + )} +
+ )} + + {needsTarget && !editing && ( +
+ + {t( + 'settings.pipelines.noTargetWarning', + 'Set a target printer before running this pipeline.', + )}
)} @@ -293,6 +404,24 @@ function PipelineRow({ ); } +function RunStatusBadge({ status }: { status: PipelineRun['status'] }) { + const { t } = useTranslation(); + const colourClass: Record = { + queued: 'text-bambu-gray', + slicing: 'text-blue-400', + dispatching: 'text-blue-400', + in_progress: 'text-bambu-green', + completed: 'text-bambu-green', + failed: 'text-red-400', + cancelled: 'text-bambu-gray', + }; + return ( + + {t(`settings.pipelines.runs.status.${status}`, status)} + + ); +} + function PresetLine({ label, ref, diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 2751cbb23..be6e4ce73 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -2570,6 +2570,8 @@ export default { field: { name: 'Pipeline-Name', description: 'Beschreibung', + targetPrinter: 'Zieldrucker', + noTarget: '— Kein Ziel —', }, action: { save: 'Speichern', @@ -2590,6 +2592,20 @@ export default { deleted: 'Pipeline gelöscht', deleteFailed: 'Löschen fehlgeschlagen', }, + noTargetHint: 'Lege einen Zieldrucker fest, um diese auszuführen', + noTargetWarning: 'Lege einen Zieldrucker fest, bevor du diese Pipeline ausführst.', + runs: { + lastRun: 'Letzter Lauf', + status: { + queued: 'in Warteschlange', + slicing: 'wird geschnitten', + dispatching: 'wird gesendet', + in_progress: 'druckt', + completed: 'abgeschlossen', + failed: 'fehlgeschlagen', + cancelled: 'abgebrochen', + }, + }, }, }, @@ -3820,6 +3836,36 @@ export default { deleteConfirm: 'Möchten Sie dieses Filament wirklich löschen?', importFromPrinter: 'Vom Drucker importieren', exportToFile: 'In Datei exportieren', + runWithPipeline: { + actionLabel: 'Mit Pipeline ausführen', + noPermission: 'Du hast keine Berechtigung, Pipelines auszuführen', + modalTitle: 'Mit Pipeline ausführen', + confirmTitle: 'Lauf bestätigen', + confirmIntro: 'Pre-Flight hat Probleme mit diesem Lauf gefunden', + sourceHint: 'Quelle', + pipelineHint: 'Pipeline', + targetHint: 'Ziel', + pipelineListAria: 'Verfügbare Pipelines', + runAnyway: 'Trotzdem ausführen', + loading: 'Wird geladen…', + empty: 'Noch keine Pipelines gespeichert. Öffne den Slice-Dialog und klicke „Als Pipeline speichern“, um eine zu erstellen.', + noTarget: 'Kein Zieldrucker festgelegt', + noTargetMessage: 'Diese Pipeline hat keinen Zieldrucker. Öffne sie in den Einstellungen, um einen festzulegen.', + toast: { + started: 'Pipeline-Lauf gestartet', + failed: 'Lauf konnte nicht gestartet werden', + }, + issue: { + printerNotSet: 'Kein Zieldrucker für diese Pipeline festgelegt.', + printerNotFound: 'Zieldrucker existiert nicht mehr.', + printerDisabled: 'Zieldrucker ist deaktiviert.', + printerOffline: 'Zieldrucker ist offline.', + filamentType: 'Filament-Slot {{slot}}: erwartet {{expected}}, AMS hat {{actual}}', + filamentColor: 'Filament-Slot {{slot}}: Farbe weicht ab (erwartet {{expected}}, AMS hat {{actual}})', + amsSlotMissing: 'AMS-Slot {{slot}} ist auf diesem Drucker nicht verfügbar', + filamentUnverified: 'Filament-Slot {{slot}} stammt aus einem Cloud-/Standard-Preset und konnte nicht statisch verifiziert werden.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index e03974376..ca9278f3d 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2586,6 +2586,8 @@ export default { field: { name: 'Pipeline name', description: 'Description', + targetPrinter: 'Target printer', + noTarget: '— No target —', }, action: { save: 'Save', @@ -2606,6 +2608,21 @@ export default { deleted: 'Pipeline deleted', deleteFailed: 'Delete failed', }, + // PR B target binding + last-run summary + noTargetHint: 'Set a target printer to run this', + noTargetWarning: 'Set a target printer before running this pipeline.', + runs: { + lastRun: 'Last run', + status: { + queued: 'queued', + slicing: 'slicing', + dispatching: 'dispatching', + in_progress: 'printing', + completed: 'completed', + failed: 'failed', + cancelled: 'cancelled', + }, + }, }, }, @@ -3836,6 +3853,39 @@ export default { deleteConfirm: 'Are you sure you want to delete this filament?', importFromPrinter: 'Import from Printer', exportToFile: 'Export to File', + // Slicer Pipelines run from the File Manager (#1425 PR B). The Run-with-pipeline + // modal is a two-step dialog: pick a pipeline, then either fire (eligibility ok) + // or confirm-and-fire (eligibility report shown). Lives in components/RunWithPipelineModal.tsx. + runWithPipeline: { + actionLabel: 'Run with pipeline', + noPermission: 'You do not have permission to run pipelines', + modalTitle: 'Run with pipeline', + confirmTitle: 'Confirm run', + confirmIntro: 'Pre-flight found issues with this run', + sourceHint: 'Source', + pipelineHint: 'Pipeline', + targetHint: 'Target', + pipelineListAria: 'Available pipelines', + runAnyway: 'Run anyway', + loading: 'Loading…', + empty: 'No pipelines saved yet. Open the Slice dialog and click "Save as pipeline" to create one.', + noTarget: 'No target printer set', + noTargetMessage: 'This pipeline has no target printer set. Open it in Settings to pick one.', + toast: { + started: 'Pipeline run started', + failed: 'Could not start run', + }, + issue: { + printerNotSet: 'No target printer set on this pipeline.', + printerNotFound: 'Target printer no longer exists.', + printerDisabled: 'Target printer is disabled.', + printerOffline: 'Target printer is offline.', + filamentType: 'Filament slot {{slot}}: expected {{expected}}, AMS has {{actual}}', + filamentColor: 'Filament slot {{slot}}: colour differs (expected {{expected}}, AMS has {{actual}})', + amsSlotMissing: 'AMS slot {{slot}} not available on this printer', + filamentUnverified: 'Filament slot {{slot}} comes from a cloud / standard preset and could not be statically verified.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index d54d18a9c..bae4238bf 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -2573,6 +2573,8 @@ export default { field: { name: 'Nombre de la pipeline', description: 'Descripción', + targetPrinter: 'Impresora de destino', + noTarget: '— Sin destino —', }, action: { save: 'Guardar', @@ -2593,6 +2595,20 @@ export default { deleted: 'Pipeline eliminada', deleteFailed: 'Error al eliminar', }, + noTargetHint: 'Establece una impresora de destino para ejecutar', + noTargetWarning: 'Establece una impresora de destino antes de ejecutar esta pipeline.', + runs: { + lastRun: 'Última ejecución', + status: { + queued: 'en cola', + slicing: 'cortando', + dispatching: 'enviando', + in_progress: 'imprimiendo', + completed: 'completada', + failed: 'fallida', + cancelled: 'cancelada', + }, + }, }, }, @@ -3823,6 +3839,36 @@ export default { deleteConfirm: '¿Está seguro de que desea eliminar este filamento?', importFromPrinter: 'Importar desde la impresora', exportToFile: 'Exportar a un archivo', + runWithPipeline: { + actionLabel: 'Ejecutar con pipeline', + noPermission: 'No tienes permiso para ejecutar pipelines', + modalTitle: 'Ejecutar con pipeline', + confirmTitle: 'Confirmar ejecución', + confirmIntro: 'El pre-vuelo encontró problemas con esta ejecución', + sourceHint: 'Origen', + pipelineHint: 'Pipeline', + targetHint: 'Destino', + pipelineListAria: 'Pipelines disponibles', + runAnyway: 'Ejecutar de todos modos', + loading: 'Cargando…', + empty: 'Aún no hay pipelines guardadas. Abre el diálogo Cortar y haz clic en "Guardar como pipeline" para crear una.', + noTarget: 'Sin impresora de destino', + noTargetMessage: 'Esta pipeline no tiene impresora de destino. Ábrela en Ajustes para elegir una.', + toast: { + started: 'Ejecución de pipeline iniciada', + failed: 'No se pudo iniciar la ejecución', + }, + issue: { + printerNotSet: 'Sin impresora de destino en esta pipeline.', + printerNotFound: 'La impresora de destino ya no existe.', + printerDisabled: 'La impresora de destino está deshabilitada.', + printerOffline: 'La impresora de destino está desconectada.', + filamentType: 'Slot de filamento {{slot}}: esperado {{expected}}, AMS tiene {{actual}}', + filamentColor: 'Slot de filamento {{slot}}: color distinto (esperado {{expected}}, AMS tiene {{actual}})', + amsSlotMissing: 'Slot AMS {{slot}} no disponible en esta impresora', + filamentUnverified: 'Slot de filamento {{slot}} proviene de un preset de nube / estándar y no se pudo verificar estáticamente.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index a962babdf..59b2aaac8 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -2559,6 +2559,8 @@ export default { field: { name: 'Nom du pipeline', description: 'Description', + targetPrinter: 'Imprimante cible', + noTarget: '— Aucune cible —', }, action: { save: 'Enregistrer', @@ -2579,6 +2581,20 @@ export default { deleted: 'Pipeline supprimé', deleteFailed: 'Échec de la suppression', }, + noTargetHint: 'Définissez une imprimante cible pour exécuter', + noTargetWarning: 'Définissez une imprimante cible avant d\'exécuter ce pipeline.', + runs: { + lastRun: 'Dernière exécution', + status: { + queued: 'en file', + slicing: 'tranchage', + dispatching: 'envoi', + in_progress: 'impression', + completed: 'terminé', + failed: 'échec', + cancelled: 'annulé', + }, + }, }, }, @@ -3809,6 +3825,36 @@ export default { deleteConfirm: 'Supprimer ce filament ?', importFromPrinter: 'Importer de l\'imprimante', exportToFile: 'Exporter vers fichier', + runWithPipeline: { + actionLabel: 'Exécuter avec un pipeline', + noPermission: 'Vous n\'avez pas la permission d\'exécuter des pipelines', + modalTitle: 'Exécuter avec un pipeline', + confirmTitle: 'Confirmer l\'exécution', + confirmIntro: 'Le pré-vol a trouvé des problèmes avec cette exécution', + sourceHint: 'Source', + pipelineHint: 'Pipeline', + targetHint: 'Cible', + pipelineListAria: 'Pipelines disponibles', + runAnyway: 'Exécuter quand même', + loading: 'Chargement…', + empty: 'Aucun pipeline enregistré pour le moment. Ouvrez le dialogue Trancher et cliquez sur « Enregistrer comme pipeline » pour en créer un.', + noTarget: 'Aucune imprimante cible définie', + noTargetMessage: 'Ce pipeline n\'a pas d\'imprimante cible. Ouvrez-le dans les Paramètres pour en choisir une.', + toast: { + started: 'Exécution de pipeline démarrée', + failed: 'Impossible de démarrer l\'exécution', + }, + issue: { + printerNotSet: 'Aucune imprimante cible définie pour ce pipeline.', + printerNotFound: 'L\'imprimante cible n\'existe plus.', + printerDisabled: 'L\'imprimante cible est désactivée.', + printerOffline: 'L\'imprimante cible est hors ligne.', + filamentType: 'Emplacement filament {{slot}} : attendu {{expected}}, AMS a {{actual}}', + filamentColor: 'Emplacement filament {{slot}} : couleur différente (attendu {{expected}}, AMS a {{actual}})', + amsSlotMissing: 'Emplacement AMS {{slot}} indisponible sur cette imprimante', + filamentUnverified: 'L\'emplacement filament {{slot}} provient d\'un préréglage cloud / standard et n\'a pas pu être vérifié statiquement.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index d5d15bd9d..3c525c7be 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -2558,6 +2558,8 @@ export default { field: { name: 'Nome pipeline', description: 'Descrizione', + targetPrinter: 'Stampante di destinazione', + noTarget: '— Nessuna destinazione —', }, action: { save: 'Salva', @@ -2578,6 +2580,20 @@ export default { deleted: 'Pipeline eliminata', deleteFailed: 'Eliminazione non riuscita', }, + noTargetHint: 'Imposta una stampante di destinazione per eseguire', + noTargetWarning: 'Imposta una stampante di destinazione prima di eseguire questa pipeline.', + runs: { + lastRun: 'Ultima esecuzione', + status: { + queued: 'in coda', + slicing: 'slicing', + dispatching: 'invio', + in_progress: 'stampa', + completed: 'completata', + failed: 'fallita', + cancelled: 'annullata', + }, + }, }, }, @@ -3808,6 +3824,36 @@ export default { deleteConfirm: 'Sei sicuro di voler eliminare questo filamento?', importFromPrinter: 'Importa da stampante', exportToFile: 'Esporta su file', + runWithPipeline: { + actionLabel: 'Esegui con pipeline', + noPermission: 'Non hai il permesso di eseguire pipeline', + modalTitle: 'Esegui con pipeline', + confirmTitle: 'Conferma esecuzione', + confirmIntro: 'Il pre-volo ha trovato problemi con questa esecuzione', + sourceHint: 'Sorgente', + pipelineHint: 'Pipeline', + targetHint: 'Destinazione', + pipelineListAria: 'Pipeline disponibili', + runAnyway: 'Esegui comunque', + loading: 'Caricamento…', + empty: 'Nessuna pipeline salvata. Apri il dialogo Slice e fai clic su "Salva come pipeline" per crearne una.', + noTarget: 'Nessuna stampante di destinazione', + noTargetMessage: 'Questa pipeline non ha una stampante di destinazione. Aprila nelle Impostazioni per sceglierne una.', + toast: { + started: 'Esecuzione della pipeline avviata', + failed: 'Impossibile avviare l\'esecuzione', + }, + issue: { + printerNotSet: 'Nessuna stampante di destinazione su questa pipeline.', + printerNotFound: 'La stampante di destinazione non esiste più.', + printerDisabled: 'La stampante di destinazione è disabilitata.', + printerOffline: 'La stampante di destinazione è offline.', + filamentType: 'Slot filamento {{slot}}: atteso {{expected}}, AMS ha {{actual}}', + filamentColor: 'Slot filamento {{slot}}: colore differente (atteso {{expected}}, AMS ha {{actual}})', + amsSlotMissing: 'Slot AMS {{slot}} non disponibile su questa stampante', + filamentUnverified: 'Lo slot filamento {{slot}} proviene da un preset cloud / standard e non può essere verificato staticamente.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 41eb652c2..d4677b0e2 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -2570,6 +2570,8 @@ export default { field: { name: 'パイプライン名', description: '説明', + targetPrinter: '対象プリンター', + noTarget: '— 対象なし —', }, action: { save: '保存', @@ -2590,6 +2592,20 @@ export default { deleted: 'パイプラインを削除しました', deleteFailed: '削除に失敗しました', }, + noTargetHint: '実行するには対象プリンターを設定してください', + noTargetWarning: 'このパイプラインを実行する前に対象プリンターを設定してください。', + runs: { + lastRun: '前回の実行', + status: { + queued: '待機中', + slicing: 'スライス中', + dispatching: '送信中', + in_progress: '印刷中', + completed: '完了', + failed: '失敗', + cancelled: 'キャンセル', + }, + }, }, }, @@ -3820,6 +3836,36 @@ export default { deleteConfirm: 'このフィラメントを削除しますか?', importFromPrinter: 'プリンターからインポート', exportToFile: 'ファイルにエクスポート', + runWithPipeline: { + actionLabel: 'パイプラインで実行', + noPermission: 'パイプラインを実行する権限がありません', + modalTitle: 'パイプラインで実行', + confirmTitle: '実行を確認', + confirmIntro: 'プリフライトでこの実行に関する問題が見つかりました', + sourceHint: 'ソース', + pipelineHint: 'パイプライン', + targetHint: '対象', + pipelineListAria: '利用可能なパイプライン', + runAnyway: 'とにかく実行', + loading: '読み込み中…', + empty: '保存されたパイプラインはまだありません。スライスダイアログを開き「パイプラインとして保存」をクリックして作成してください。', + noTarget: '対象プリンターが未設定', + noTargetMessage: 'このパイプラインには対象プリンターが設定されていません。設定で開いて選択してください。', + toast: { + started: 'パイプラインの実行を開始しました', + failed: '実行を開始できませんでした', + }, + issue: { + printerNotSet: 'このパイプラインに対象プリンターが設定されていません。', + printerNotFound: '対象プリンターが存在しません。', + printerDisabled: '対象プリンターは無効です。', + printerOffline: '対象プリンターはオフラインです。', + filamentType: 'フィラメントスロット {{slot}}: 期待 {{expected}}、AMS は {{actual}}', + filamentColor: 'フィラメントスロット {{slot}}: 色が異なります(期待 {{expected}}、AMS は {{actual}})', + amsSlotMissing: 'AMS スロット {{slot}} はこのプリンターで利用できません', + filamentUnverified: 'フィラメントスロット {{slot}} はクラウド/標準プリセットからのため、静的に検証できませんでした。', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index f04a76550..24ce11c7b 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -2426,6 +2426,8 @@ export default { field: { name: '파이프라인 이름', description: '설명', + targetPrinter: '대상 프린터', + noTarget: '— 대상 없음 —', }, action: { save: '저장', @@ -2446,6 +2448,20 @@ export default { deleted: '파이프라인이 삭제되었습니다', deleteFailed: '삭제 실패', }, + noTargetHint: '실행하려면 대상 프린터를 설정하세요', + noTargetWarning: '이 파이프라인을 실행하기 전에 대상 프린터를 설정하세요.', + runs: { + lastRun: '마지막 실행', + status: { + queued: '대기 중', + slicing: '슬라이싱 중', + dispatching: '전송 중', + in_progress: '인쇄 중', + completed: '완료됨', + failed: '실패', + cancelled: '취소됨', + }, + }, }, }, notification: { @@ -3614,7 +3630,37 @@ export default { noFilaments: '라이브러리에 필라멘트 없음', deleteConfirm: '이 필라멘트를 삭제하시겠습니까?', importFromPrinter: '프린터에서 가져오기', - exportToFile: '파일로 내보내기' + exportToFile: '파일로 내보내기', + runWithPipeline: { + actionLabel: '파이프라인으로 실행', + noPermission: '파이프라인을 실행할 권한이 없습니다', + modalTitle: '파이프라인으로 실행', + confirmTitle: '실행 확인', + confirmIntro: '사전 점검에서 이 실행과 관련된 문제를 발견했습니다', + sourceHint: '소스', + pipelineHint: '파이프라인', + targetHint: '대상', + pipelineListAria: '사용 가능한 파이프라인', + runAnyway: '그래도 실행', + loading: '불러오는 중…', + empty: '저장된 파이프라인이 없습니다. 슬라이스 대화상자를 열고 "파이프라인으로 저장"을 클릭하여 생성하세요.', + noTarget: '대상 프린터가 설정되지 않음', + noTargetMessage: '이 파이프라인에는 대상 프린터가 없습니다. 설정에서 열어 선택하세요.', + toast: { + started: '파이프라인 실행 시작됨', + failed: '실행을 시작할 수 없습니다', + }, + issue: { + printerNotSet: '이 파이프라인에 대상 프린터가 설정되어 있지 않습니다.', + printerNotFound: '대상 프린터가 더 이상 존재하지 않습니다.', + printerDisabled: '대상 프린터가 비활성화되어 있습니다.', + printerOffline: '대상 프린터가 오프라인입니다.', + filamentType: '필라멘트 슬롯 {{slot}}: 예상 {{expected}}, AMS 실제 {{actual}}', + filamentColor: '필라멘트 슬롯 {{slot}}: 색상이 다릅니다 (예상 {{expected}}, AMS {{actual}})', + amsSlotMissing: 'AMS 슬롯 {{slot}}이(가) 이 프린터에서 사용 불가', + filamentUnverified: '필라멘트 슬롯 {{slot}}은(는) 클라우드/표준 프리셋이며 정적으로 검증할 수 없습니다.', + }, + }, }, slice: { title: '모델 슬라이싱', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 08cbacdfa..a860d68c0 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -2558,6 +2558,8 @@ export default { field: { name: 'Nome da pipeline', description: 'Descrição', + targetPrinter: 'Impressora de destino', + noTarget: '— Sem destino —', }, action: { save: 'Salvar', @@ -2578,6 +2580,20 @@ export default { deleted: 'Pipeline excluída', deleteFailed: 'Falha ao excluir', }, + noTargetHint: 'Defina uma impressora de destino para executar', + noTargetWarning: 'Defina uma impressora de destino antes de executar esta pipeline.', + runs: { + lastRun: 'Última execução', + status: { + queued: 'na fila', + slicing: 'cortando', + dispatching: 'enviando', + in_progress: 'imprimindo', + completed: 'concluída', + failed: 'falhou', + cancelled: 'cancelada', + }, + }, }, }, @@ -3808,6 +3824,36 @@ export default { deleteConfirm: 'Tem certeza de que deseja excluir este filamento?', importFromPrinter: 'Importar da Impressora', exportToFile: 'Exportar para Arquivo', + runWithPipeline: { + actionLabel: 'Executar com pipeline', + noPermission: 'Você não tem permissão para executar pipelines', + modalTitle: 'Executar com pipeline', + confirmTitle: 'Confirmar execução', + confirmIntro: 'A pré-verificação encontrou problemas com esta execução', + sourceHint: 'Origem', + pipelineHint: 'Pipeline', + targetHint: 'Destino', + pipelineListAria: 'Pipelines disponíveis', + runAnyway: 'Executar mesmo assim', + loading: 'Carregando…', + empty: 'Ainda não há pipelines salvas. Abra o diálogo Cortar e clique em "Salvar como pipeline" para criar uma.', + noTarget: 'Sem impressora de destino', + noTargetMessage: 'Esta pipeline não tem impressora de destino. Abra-a em Configurações para escolher uma.', + toast: { + started: 'Execução da pipeline iniciada', + failed: 'Não foi possível iniciar a execução', + }, + issue: { + printerNotSet: 'Sem impressora de destino nesta pipeline.', + printerNotFound: 'Impressora de destino não existe mais.', + printerDisabled: 'Impressora de destino está desativada.', + printerOffline: 'Impressora de destino está offline.', + filamentType: 'Slot de filamento {{slot}}: esperado {{expected}}, AMS tem {{actual}}', + filamentColor: 'Slot de filamento {{slot}}: cor diferente (esperado {{expected}}, AMS tem {{actual}})', + amsSlotMissing: 'Slot AMS {{slot}} indisponível nesta impressora', + filamentUnverified: 'Slot de filamento {{slot}} vem de uma predefinição da nuvem / padrão e não pôde ser verificado estaticamente.', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index 9a123fb6e..140e36db9 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -2574,6 +2574,8 @@ export default { field: { name: 'Pipeline adı', description: 'Açıklama', + targetPrinter: 'Hedef yazıcı', + noTarget: '— Hedef yok —', }, action: { save: 'Kaydet', @@ -2594,6 +2596,20 @@ export default { deleted: 'Pipeline silindi', deleteFailed: 'Silme başarısız', }, + noTargetHint: 'Çalıştırmak için bir hedef yazıcı belirleyin', + noTargetWarning: 'Bu pipeline\'ı çalıştırmadan önce bir hedef yazıcı belirleyin.', + runs: { + lastRun: 'Son çalıştırma', + status: { + queued: 'kuyrukta', + slicing: 'dilimleniyor', + dispatching: 'gönderiliyor', + in_progress: 'yazdırılıyor', + completed: 'tamamlandı', + failed: 'başarısız', + cancelled: 'iptal edildi', + }, + }, }, }, @@ -3810,6 +3826,36 @@ export default { deleteConfirm: 'Bu filamenti silmek istediğinizden emin misiniz?', importFromPrinter: 'Yazıcıdan İçe Aktar', exportToFile: 'Dosyaya Dışa Aktar', + runWithPipeline: { + actionLabel: 'Pipeline ile çalıştır', + noPermission: 'Pipeline çalıştırma izniniz yok', + modalTitle: 'Pipeline ile çalıştır', + confirmTitle: 'Çalıştırmayı onayla', + confirmIntro: 'Ön kontrol bu çalıştırmayla ilgili sorunlar buldu', + sourceHint: 'Kaynak', + pipelineHint: 'Pipeline', + targetHint: 'Hedef', + pipelineListAria: 'Kullanılabilir pipeline\'lar', + runAnyway: 'Yine de çalıştır', + loading: 'Yükleniyor…', + empty: 'Henüz kayıtlı pipeline yok. Dilimleme diyalogunu açın ve oluşturmak için "Pipeline olarak kaydet"e tıklayın.', + noTarget: 'Hedef yazıcı belirlenmemiş', + noTargetMessage: 'Bu pipeline\'ın hedef yazıcısı yok. Birini seçmek için Ayarlar\'da açın.', + toast: { + started: 'Pipeline çalıştırması başladı', + failed: 'Çalıştırma başlatılamadı', + }, + issue: { + printerNotSet: 'Bu pipeline\'da hedef yazıcı belirlenmemiş.', + printerNotFound: 'Hedef yazıcı artık mevcut değil.', + printerDisabled: 'Hedef yazıcı devre dışı.', + printerOffline: 'Hedef yazıcı çevrimdışı.', + filamentType: 'Filament yuvası {{slot}}: beklenen {{expected}}, AMS\'de {{actual}}', + filamentColor: 'Filament yuvası {{slot}}: renk farklı (beklenen {{expected}}, AMS\'de {{actual}})', + amsSlotMissing: 'AMS yuvası {{slot}} bu yazıcıda yok', + filamentUnverified: 'Filament yuvası {{slot}} bulut / standart ön ayardan geldiği için statik olarak doğrulanamadı.', + }, + }, }, // Dilimle (SliceModal ile slicer-API entegrasyonu) diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index db502a300..c6d13ef2c 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -2558,6 +2558,8 @@ export default { field: { name: '流水线名称', description: '描述', + targetPrinter: '目标打印机', + noTarget: '— 无目标 —', }, action: { save: '保存', @@ -2578,6 +2580,20 @@ export default { deleted: '流水线已删除', deleteFailed: '删除失败', }, + noTargetHint: '设置目标打印机以运行', + noTargetWarning: '运行此流水线前请先设置目标打印机。', + runs: { + lastRun: '上次运行', + status: { + queued: '排队中', + slicing: '切片中', + dispatching: '发送中', + in_progress: '打印中', + completed: '已完成', + failed: '失败', + cancelled: '已取消', + }, + }, }, }, @@ -3808,6 +3824,36 @@ export default { deleteConfirm: '确定要删除此耗材吗?', importFromPrinter: '从打印机导入', exportToFile: '导出到文件', + runWithPipeline: { + actionLabel: '用流水线运行', + noPermission: '您没有运行流水线的权限', + modalTitle: '用流水线运行', + confirmTitle: '确认运行', + confirmIntro: '预检发现此次运行存在问题', + sourceHint: '来源', + pipelineHint: '流水线', + targetHint: '目标', + pipelineListAria: '可用流水线', + runAnyway: '仍然运行', + loading: '加载中…', + empty: '尚未保存流水线。请打开切片对话框并点击"另存为流水线"创建一个。', + noTarget: '未设置目标打印机', + noTargetMessage: '此流水线没有目标打印机。请在设置中打开并选择一个。', + toast: { + started: '流水线运行已开始', + failed: '无法启动运行', + }, + issue: { + printerNotSet: '此流水线未设置目标打印机。', + printerNotFound: '目标打印机已不存在。', + printerDisabled: '目标打印机已禁用。', + printerOffline: '目标打印机已离线。', + filamentType: '耗材槽 {{slot}}:期望 {{expected}},AMS 实际 {{actual}}', + filamentColor: '耗材槽 {{slot}}:颜色不同(期望 {{expected}},AMS 实际 {{actual}})', + amsSlotMissing: '此打印机上没有 AMS 槽 {{slot}}', + filamentUnverified: '耗材槽 {{slot}} 来自云端 / 标准预设,无法静态验证。', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 712b1a617..2ef7753ac 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -2558,6 +2558,8 @@ export default { field: { name: '管線名稱', description: '描述', + targetPrinter: '目標印表機', + noTarget: '— 無目標 —', }, action: { save: '儲存', @@ -2578,6 +2580,20 @@ export default { deleted: '管線已刪除', deleteFailed: '刪除失敗', }, + noTargetHint: '設定目標印表機以執行', + noTargetWarning: '執行此管線前請先設定目標印表機。', + runs: { + lastRun: '上次執行', + status: { + queued: '排隊中', + slicing: '切片中', + dispatching: '傳送中', + in_progress: '列印中', + completed: '已完成', + failed: '失敗', + cancelled: '已取消', + }, + }, }, }, @@ -3808,6 +3824,36 @@ export default { deleteConfirm: '確定要刪除此耗材嗎?', importFromPrinter: '從印表機匯入', exportToFile: '匯出到檔案', + runWithPipeline: { + actionLabel: '以管線執行', + noPermission: '您沒有執行管線的權限', + modalTitle: '以管線執行', + confirmTitle: '確認執行', + confirmIntro: '預檢發現此次執行存在問題', + sourceHint: '來源', + pipelineHint: '管線', + targetHint: '目標', + pipelineListAria: '可用管線', + runAnyway: '仍然執行', + loading: '載入中…', + empty: '尚未儲存管線。請開啟切片對話框並點擊「另存為管線」建立一個。', + noTarget: '未設定目標印表機', + noTargetMessage: '此管線沒有目標印表機。請在設定中開啟並選擇一個。', + toast: { + started: '管線執行已開始', + failed: '無法啟動執行', + }, + issue: { + printerNotSet: '此管線未設定目標印表機。', + printerNotFound: '目標印表機已不存在。', + printerDisabled: '目標印表機已停用。', + printerOffline: '目標印表機已離線。', + filamentType: '耗材槽 {{slot}}:預期 {{expected}},AMS 實際 {{actual}}', + filamentColor: '耗材槽 {{slot}}:顏色不同(預期 {{expected}},AMS 實際 {{actual}})', + amsSlotMissing: '此印表機上沒有 AMS 槽 {{slot}}', + filamentUnverified: '耗材槽 {{slot}} 來自雲端 / 標準預設,無法靜態驗證。', + }, + }, }, // Slice (slicer-API integration via SliceModal) diff --git a/frontend/src/pages/ArchivesPage.tsx b/frontend/src/pages/ArchivesPage.tsx index f23b724c4..71a9ee73b 100644 --- a/frontend/src/pages/ArchivesPage.tsx +++ b/frontend/src/pages/ArchivesPage.tsx @@ -59,6 +59,7 @@ import { } from 'lucide-react'; import { api } from '../api/client'; import { SliceModal } from '../components/SliceModal'; +import { RunWithPipelineModal } from '../components/RunWithPipelineModal'; import { openInSlicer, type SlicerType } from '../utils/slicer'; import { formatDateTime, formatDateOnly, parseUTCDate, type TimeFormat, formatDuration } from '../utils/date'; import { getCurrencySymbol } from '../utils/currency'; @@ -183,6 +184,7 @@ function ArchiveCard({ const navigate = useNavigate(); const [showReprint, setShowReprint] = useState(false); const [showSliceModal, setShowSliceModal] = useState(false); + const [showRunPipeline, setShowRunPipeline] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // #1343: when true, the delete also drops the row from Quick Stats. Default // off — soft delete preserves the archive's filament/time/cost contribution. @@ -438,6 +440,19 @@ function ArchiveCard({ } }, }, + // Run-with-pipeline (#1425 PR B follow-up). Sources from archive's + // source 3MF (or file_path fallback). Only when slicer-api is on. + ...(useSlicerApi + ? [{ + label: t('library.runWithPipeline.actionLabel'), + icon: , + onClick: () => setShowRunPipeline(true), + disabled: !hasPermission('pipelines:run'), + title: !hasPermission('pipelines:run') + ? t('library.runWithPipeline.noPermission') + : undefined, + }] + : []), ]), { label: archive.external_url ? t('archives.menu.externalLink') : t('archives.menu.viewOnMakerWorld'), @@ -1277,6 +1292,15 @@ function ArchiveCard({ /> )} + {/* Run-with-Pipeline Modal (#1425 PR B). Sources from archive — backend + reads source_3mf_path, falls back to file_path. */} + {showRunPipeline && ( + setShowRunPipeline(false)} + /> + )} + {/* Delete Confirmation */} {showDeleteConfirm && ( >([]); @@ -1797,6 +1822,19 @@ function ArchiveListRow({ } }, }, + // Run-with-pipeline (#1425 PR B follow-up). Sources from archive's + // source 3MF (or file_path fallback). Only when slicer-api is on. + ...(useSlicerApi + ? [{ + label: t('library.runWithPipeline.actionLabel'), + icon: , + onClick: () => setShowRunPipeline(true), + disabled: !hasPermission('pipelines:run'), + title: !hasPermission('pipelines:run') + ? t('library.runWithPipeline.noPermission') + : undefined, + }] + : []), ]), { label: archive.external_url ? t('archives.menu.externalLink') : t('archives.menu.viewOnMakerWorld'), @@ -2280,6 +2318,15 @@ function ArchiveListRow({ /> )} + {/* Run-with-Pipeline Modal (#1425 PR B). Sources from archive — backend + reads source_3mf_path, falls back to file_path. */} + {showRunPipeline && ( + setShowRunPipeline(false)} + /> + )} + {/* Delete Confirmation */} {showDeleteConfirm && ( void; onPrint?: (file: LibraryFileListItem) => void; onSlice?: (file: LibraryFileListItem) => void; + onRunPipeline?: (file: LibraryFileListItem) => void; useSlicerApi?: boolean; onPreview3d?: (file: LibraryFileListItem) => void; onRename?: (file: LibraryFileListItem) => void; @@ -741,7 +744,7 @@ interface FileCardProps { t: TFunction; } -function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, onPrint, onSlice, useSlicerApi, onPreview3d, onRename, onGenerateThumbnail, onTagClick, thumbnailVersion, hasPermission, canModify, authEnabled, t }: FileCardProps) { +function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, onPrint, onSlice, onRunPipeline, useSlicerApi, onPreview3d, onRename, onGenerateThumbnail, onTagClick, thumbnailVersion, hasPermission, canModify, authEnabled, t }: FileCardProps) { const [showActions, setShowActions] = useState(false); return ( @@ -864,6 +867,19 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, {t('slice.action')} )} + {onRunPipeline && useSlicerApi && isSliceableFilename(file.filename) && ( + + )} {onPreview3d && (file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'stl' || file.file_type === 'gcode.3mf') && ( )} + {(settings?.use_slicer_api ?? false) && isSliceableFilename(file.filename) && ( + + )} {(file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'gcode.3mf' || file.file_type === 'stl') && (