feat(admin): render-engine kill switch (block renders + show message)
Lets an admin disable rendering when no render node is available — users can't
start new renders and see a localized "service unavailable until <date>" 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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Render-engine kill switch. Admins can disable new renders (e.g. when no render
|
||||
* node is available) via the `render_service` Website Setting; the studio then
|
||||
* blocks "start render" and shows a localized "unavailable until <date>" message.
|
||||
*
|
||||
* Stored as one jsonb setting — no backend/migration. Reads fail OPEN (renders
|
||||
* stay enabled) so a settings hiccup never blocks a working render farm.
|
||||
*/
|
||||
import { gatewayUrl } from "@/lib/api/gateway";
|
||||
|
||||
export const RENDER_SERVICE_KEY = "render_service";
|
||||
|
||||
export interface RenderServiceStatus {
|
||||
enabled: boolean;
|
||||
messageFa?: string;
|
||||
messageEn?: string;
|
||||
untilDate?: string; // ISO date (optional)
|
||||
}
|
||||
|
||||
export async function fetchRenderServiceStatus(): Promise<RenderServiceStatus> {
|
||||
try {
|
||||
const res = await fetch(gatewayUrl("/v1/settings/"), {
|
||||
cache: "no-store",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) return { enabled: true };
|
||||
const rows = (await res.json()) as Array<{ key: string; value: string }>;
|
||||
const row = Array.isArray(rows) ? rows.find((r) => r.key === RENDER_SERVICE_KEY) : null;
|
||||
if (!row?.value) return { enabled: true };
|
||||
const v = typeof row.value === "string" ? JSON.parse(row.value) : row.value;
|
||||
return {
|
||||
enabled: v?.enabled !== false,
|
||||
messageFa: v?.messageFa ?? undefined,
|
||||
messageEn: v?.messageEn ?? undefined,
|
||||
untilDate: v?.untilDate ?? undefined,
|
||||
};
|
||||
} catch {
|
||||
return { enabled: true }; // fail open
|
||||
}
|
||||
}
|
||||
|
||||
/** Pick the localized message, appending the "until" date when present. */
|
||||
export function renderServiceMessage(
|
||||
status: RenderServiceStatus,
|
||||
locale: string,
|
||||
fallback: string,
|
||||
): string {
|
||||
const base = (locale === "fa" ? status.messageFa : status.messageEn) || status.messageEn || status.messageFa || fallback;
|
||||
if (status.untilDate) {
|
||||
try {
|
||||
const d = new Intl.DateTimeFormat(locale === "fa" ? "fa-IR" : "en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(new Date(status.untilDate));
|
||||
return `${base} (${d})`;
|
||||
} catch {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
Reference in New Issue
Block a user