From 61ba52612253924990482c060aa297eeb4af85b0 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 12 Jun 2026 09:47:42 +0330 Subject: [PATCH] feat(admin): render-engine kill switch (block renders + show message) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets an admin disable rendering when no render node is available — users can't start new renders and see a localized "service unavailable until " message. - Admin → فارم رندر → موتور رندر (RenderEngineAdmin): on/off toggle + fa/en message + optional Jalali "until" date; saved as one `render_service` Website Setting (jsonb) via /v1/settings — no backend change, no migration. - lib/render-service.ts: fetchRenderServiceStatus (fail-open) + renderServiceMessage (locale + appends the date). - Enforcement: POST /api/render returns 503 {code:render_disabled, messages} when off; studio render page reads GET /api/render/service on mount → disables "شروع رندر" and shows the banner, and handles the 503 on click. - i18n: appAdminLayout.renderEngine (fa+en, parity 1045/1045). tsc + next build clean. Verified: disabled setting → /api/render/service returns enabled:false. Co-Authored-By: Claude Opus 4.8 --- messages/en.json | 1 + messages/fa.json | 1 + src/app/[locale]/admin/layout.tsx | 1 + src/app/[locale]/admin/render-engine/page.tsx | 7 + .../studio/render/[projectId]/page.tsx | 59 +++++- src/app/api/render/route.ts | 16 ++ src/app/api/render/service/route.ts | 14 ++ src/components/admin/RenderEngineAdmin.tsx | 168 ++++++++++++++++++ src/lib/render-service.ts | 62 +++++++ 9 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 src/app/[locale]/admin/render-engine/page.tsx create mode 100644 src/app/api/render/service/route.ts create mode 100644 src/components/admin/RenderEngineAdmin.tsx create mode 100644 src/lib/render-service.ts diff --git a/messages/en.json b/messages/en.json index a2469fd..e3d70f0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -369,6 +369,7 @@ "auto": { "appAdminLayout": { "brand": "FlatRender", + "renderEngine": "Render Engine", "nodes": "Nodes", "renderQueue": "Render Queue", "backToDashboard": "← Back to Dashboard", diff --git a/messages/fa.json b/messages/fa.json index 2bf1533..54c12f1 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -369,6 +369,7 @@ "auto": { "appAdminLayout": { "brand": "فلت‌رندر", + "renderEngine": "موتور رندر", "nodes": "نودها", "renderQueue": "صف رندر", "backToDashboard": "← بازگشت به داشبورد", diff --git a/src/app/[locale]/admin/layout.tsx b/src/app/[locale]/admin/layout.tsx index 07e3bfb..2f971e8 100644 --- a/src/app/[locale]/admin/layout.tsx +++ b/src/app/[locale]/admin/layout.tsx @@ -64,6 +64,7 @@ export default async function AdminLayout({ { title: "فارم رندر", items: [ + { href: "/admin/render-engine", label: t("renderEngine") }, { href: "/admin/nodes", label: t("nodes") }, { href: "/admin/node-fonts", label: t("nodeFonts") }, { href: "/admin/renders", label: t("renderQueue") }, diff --git a/src/app/[locale]/admin/render-engine/page.tsx b/src/app/[locale]/admin/render-engine/page.tsx new file mode 100644 index 0000000..7d4a943 --- /dev/null +++ b/src/app/[locale]/admin/render-engine/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { RenderEngineAdmin } from "@/components/admin/RenderEngineAdmin"; + +export default function Page() { + return ; +} diff --git a/src/app/[locale]/studio/render/[projectId]/page.tsx b/src/app/[locale]/studio/render/[projectId]/page.tsx index cebc005..00f3201 100644 --- a/src/app/[locale]/studio/render/[projectId]/page.tsx +++ b/src/app/[locale]/studio/render/[projectId]/page.tsx @@ -12,9 +12,12 @@ import { RefreshCw, } from "lucide-react"; +import { useLocale } from "next-intl"; + import { apiFetch } from "@/lib/api/fetch"; import { RENDER_EXPORT_PRESETS, type RenderExportPreset } from "@/lib/render-presets"; import type { RenderSettings } from "@/lib/render-schemas"; +import { renderServiceMessage, type RenderServiceStatus } from "@/lib/render-service"; import { cn } from "@/lib/utils"; type Phase = "config" | "submitting" | "polling" | "completed" | "failed"; @@ -57,6 +60,13 @@ export default function RenderPage() { const projectId = params.projectId; const presetKey = search.get("preset") as RenderExportPreset | null; + const locale = useLocale(); + const [renderService, setRenderService] = useState(null); + const serviceDisabled = renderService?.enabled === false; + const serviceMessage = renderService + ? renderServiceMessage(renderService, locale, "سرویس رندر در حال حاضر در دسترس نیست.") + : ""; + const [resolution, setResolution] = useState("1080p"); const [fps, setFps] = useState(30); const [phase, setPhase] = useState("config"); @@ -80,6 +90,23 @@ export default function RenderPage() { setFps(cfg.settings.fps); }, [presetKey]); + // Render-engine kill switch: learn whether new renders are allowed. + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await fetch("/api/render/service", { cache: "no-store" }); + const data = (await res.json()) as RenderServiceStatus; + if (!cancelled) setRenderService(data); + } catch { + if (!cancelled) setRenderService({ enabled: true }); + } + })(); + return () => { + cancelled = true; + }; + }, []); + // On mount: resume this project's render if one is in flight, or flag a render // running on another project so we can block starting a new one. useEffect(() => { @@ -170,7 +197,26 @@ export default function RenderPage() { settings: { resolution, format: "mp4" as const, fps }, }), }); - const data = (await res.json()) as { jobId?: string; error?: string; code?: string }; + const data = (await res.json()) as { + jobId?: string; + error?: string; + code?: string; + messageFa?: string; + messageEn?: string; + untilDate?: string; + }; + if (data.code === "render_disabled") { + const status: RenderServiceStatus = { + enabled: false, + messageFa: data.messageFa, + messageEn: data.messageEn, + untilDate: data.untilDate, + }; + setRenderService(status); + setPhase("config"); + setErrorMessage(renderServiceMessage(status, locale, data.error ?? "سرویس رندر در حال حاضر در دسترس نیست.")); + return; + } if (res.status === 409 || data.code === "active_render_limit") { setPhase("config"); setErrorMessage( @@ -191,7 +237,7 @@ export default function RenderPage() { setPhase("failed"); setErrorMessage("Could not reach the render service."); } - }, [projectId, resolution, fps]); + }, [projectId, resolution, fps, locale]); const backToStudio = `/studio/video/${projectId}`; const isBusy = phase === "submitting" || phase === "polling"; @@ -313,7 +359,12 @@ export default function RenderPage() { ) : ( // Config
- {errorMessage && ( + {serviceDisabled && ( +
+ {serviceMessage} +
+ )} + {errorMessage && !serviceDisabled && (

{errorMessage}

@@ -372,7 +423,7 @@ export default function RenderPage() { +
+ + {error &&

{error}

} + {msg &&

{msg}

} + + {loading ? ( +

در حال بارگذاری…

+ ) : ( +
+ + +
+
+
+ +