From 40fdcf280f5d27a79d87a8d6c7d88fb1f9adcbfa Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 25 Jun 2026 11:31:56 +0330 Subject: [PATCH] feat(render): always-available, fully-cancel render controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend cancel was solid (CancelJob/StopJob; the dev worker abandons a cancelled job, a real node kills its process) — but the UI couldn't reach it: the render page had NO cancel button, and the global progress pill's X only HID the pill (the job kept running). So a render couldn't actually be stopped from where you watch it. - Render page: a prominent «لغو رندر» button while a render is in flight (Queued or Running); cancelRender() calls /renders/:id/cancel and returns to config optimistically. The poll now also handles a `cancelled` status (when stopped from another surface). - Global pill: the X now CANCELS the render (with confirm) instead of just hiding it — so any in-flight render is cancellable from any page. - (Dashboard MyRenders already had a working cancel.) Co-Authored-By: Claude Opus 4.8 --- .../studio/render/[projectId]/page.tsx | 43 +++++++++++++++++-- .../render/GlobalRenderProgress.tsx | 19 +++++--- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/app/[locale]/studio/render/[projectId]/page.tsx b/src/app/[locale]/studio/render/[projectId]/page.tsx index bdac867..3dc380e 100644 --- a/src/app/[locale]/studio/render/[projectId]/page.tsx +++ b/src/app/[locale]/studio/render/[projectId]/page.tsx @@ -10,6 +10,7 @@ import { Link2, Loader2, RefreshCw, + XCircle, } from "lucide-react"; import { useLocale } from "next-intl"; @@ -205,6 +206,12 @@ export default function RenderPage() { } else if (data.status === "failed") { setPhase("failed"); setErrorMessage(data.errorMessage ?? "Render failed."); + } else if (data.status?.toLowerCase() === "cancelled") { + // Cancelled here or from another surface (pill / dashboard) — reflect it. + setPhase("config"); + setProgress(0); + setEtaSec(null); + setErrorMessage("رندر لغو شد."); } } catch { setPhase("failed"); @@ -279,6 +286,26 @@ export default function RenderPage() { } }, [projectId, resolution, fps, locale]); + /** Fully cancel the active render — works in any state (Queued or Running). The + * server marks it Cancelled (the dev worker abandons it, a real node kills its + * process), so it stops fully. Optimistically return to config right away. */ + const cancelRender = useCallback(async () => { + if (!jobId) return; + const id = jobId; + setPhase("config"); + setProgress(0); + setEtaSec(null); + setProgressMessage(""); + setPreviewB64(null); + setErrorMessage("رندر لغو شد."); + setJobId(null); + try { + await apiFetch(`/api/renders/${id}/cancel`, { method: "POST" }); + } catch { + /* server is the source of truth; the dashboard can also cancel */ + } + }, [jobId]); + const backToStudio = `/studio/video/${projectId}`; const isBusy = phase === "submitting" || phase === "polling"; @@ -393,9 +420,19 @@ export default function RenderPage() { ) : isBusy ? ( -

- می‌توانید این صفحه را ببندید؛ رندر در پس‌زمینه ادامه می‌یابد و از هر صفحه‌ای قابل پیگیری است. -

+
+

+ می‌توانید این صفحه را ببندید؛ رندر در پس‌زمینه ادامه می‌یابد و از هر صفحه‌ای قابل پیگیری است. +

+ +
) : ( // Config
diff --git a/src/components/render/GlobalRenderProgress.tsx b/src/components/render/GlobalRenderProgress.tsx index f831c25..e6ad8b8 100644 --- a/src/components/render/GlobalRenderProgress.tsx +++ b/src/components/render/GlobalRenderProgress.tsx @@ -103,16 +103,25 @@ export function GlobalRenderProgress({ authed }: { authed: boolean }) {
- {/* Dismiss (hides until job id changes) */} + {/* Cancel — fully stops the render server-side (works from any page). */} { + onClick={async (e) => { e.stopPropagation(); - setDismissed(active.id); + if (!window.confirm("این رندر لغو شود؟")) return; + const id = active.id; + setActive(null); + setDismissed(id); + try { + await fetch(`/api/renders/${id}/cancel`, { method: "POST" }); + } catch { + /* server is the source of truth; ignore transient errors */ + } }} - className="shrink-0 rounded p-1 text-gray-600 hover:text-gray-300" - aria-label="پنهان کردن" + className="shrink-0 rounded p-1 text-gray-500 transition-colors hover:bg-red-500/20 hover:text-red-300" + aria-label="لغو رندر" + title="لغو رندر" >