feat(frontend): migrate render jobs off Supabase to V2 render orchestrator; drop all Supabase lib files
- render-jobs.ts: replace Supabase client with V2 gateway calls POST /v1/renders (saved_project_id + quality + resolution + frame_rate) GET /v1/renders/:id/progress for status polling GET /v1/renders/:id + /v1/exports/:id/download-url for completed output URL triggerRenderWorker is now a no-op (V2 dispatches internally) - render/route.ts: add getAccessToken() guard, pass token to createRenderJob - render/[jobId]/status/route.ts: add getAccessToken() guard, pass token to getRenderJob - Delete src/lib/supabase/, src/lib/supabase.ts — no remaining consumers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { getAccessToken } from "@/lib/auth/session";
|
||||||
import { getRenderJob } from "@/lib/render-jobs";
|
import { getRenderJob } from "@/lib/render-jobs";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@@ -15,7 +16,12 @@ export async function GET(_request: Request, context: RouteContext) {
|
|||||||
return NextResponse.json({ error: "jobId required" }, { status: 400 });
|
return NextResponse.json({ error: "jobId required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await getRenderJob(jobId);
|
const token = await getAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await getRenderJob(jobId, token);
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return NextResponse.json({ error: "Job not found" }, { status: 404 });
|
return NextResponse.json({ error: "Job not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { getAccessToken } from "@/lib/auth/session";
|
||||||
|
import { createRenderJob } from "@/lib/render-jobs";
|
||||||
import { renderRequestSchema } from "@/lib/render-schemas";
|
import { renderRequestSchema } from "@/lib/render-schemas";
|
||||||
import { createRenderJob, triggerRenderWorker } from "@/lib/render-jobs";
|
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
const token = await getAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
let body: unknown;
|
let body: unknown;
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
@@ -21,12 +27,10 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await createRenderJob(parsed.data);
|
const result = await createRenderJob(parsed.data, token);
|
||||||
if ("error" in result) {
|
if ("error" in result) {
|
||||||
return NextResponse.json({ error: result.error }, { status: 500 });
|
return NextResponse.json({ error: result.error }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await triggerRenderWorker(result.jobId);
|
|
||||||
|
|
||||||
return NextResponse.json({ jobId: result.jobId });
|
return NextResponse.json({ jobId: result.jobId });
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-49
@@ -1,5 +1,27 @@
|
|||||||
import { createAdminClient } from "@/lib/supabase/admin";
|
import { gatewayUrl } from "@/lib/api/gateway";
|
||||||
import type { RenderRequest, RenderJobStatus } from "@/lib/render-schemas";
|
import type { RenderRequest } from "@/lib/render-schemas";
|
||||||
|
|
||||||
|
// ── V2 render service types ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type V2RenderStep =
|
||||||
|
| "Queued"
|
||||||
|
| "Preparing"
|
||||||
|
| "TemplateCache"
|
||||||
|
| "JsxGen"
|
||||||
|
| "Music"
|
||||||
|
| "Rendering"
|
||||||
|
| "Validating"
|
||||||
|
| "Repairing"
|
||||||
|
| "Optimisation"
|
||||||
|
| "Video"
|
||||||
|
| "Mixing"
|
||||||
|
| "Final"
|
||||||
|
| "Uploading"
|
||||||
|
| "Done"
|
||||||
|
| "Failed"
|
||||||
|
| "Cancelled";
|
||||||
|
|
||||||
|
export type RenderJobStatus = "queued" | "processing" | "completed" | "failed";
|
||||||
|
|
||||||
export interface RenderJobRow {
|
export interface RenderJobRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -8,69 +30,136 @@ export interface RenderJobRow {
|
|||||||
progress: number;
|
progress: number;
|
||||||
progress_message: string | null;
|
progress_message: string | null;
|
||||||
output_url: string | null;
|
output_url: string | null;
|
||||||
scenes: RenderRequest["scenes"];
|
|
||||||
settings: RenderRequest["settings"];
|
|
||||||
error_message: string | null;
|
error_message: string | null;
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Map frontend resolution string to V2 quality enum. */
|
||||||
|
function mapQuality(resolution: string): string {
|
||||||
|
if (resolution === "4K") return "Full";
|
||||||
|
if (resolution === "720p") return "Medium";
|
||||||
|
return "High"; // 1080p default
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map V2 step → legacy status for frontend compatibility. */
|
||||||
|
function stepToStatus(step: V2RenderStep): RenderJobStatus {
|
||||||
|
if (step === "Done") return "completed";
|
||||||
|
if (step === "Failed" || step === "Cancelled") return "failed";
|
||||||
|
if (step === "Queued") return "queued";
|
||||||
|
return "processing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function authHeaders(token: string): Record<string, string> {
|
||||||
|
return {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a render job in the V2 render orchestrator.
|
||||||
|
* `payload.projectId` must be the UUID of a V2 saved project.
|
||||||
|
*/
|
||||||
export async function createRenderJob(
|
export async function createRenderJob(
|
||||||
payload: RenderRequest
|
payload: RenderRequest,
|
||||||
|
token: string
|
||||||
): Promise<{ jobId: string } | { error: string }> {
|
): Promise<{ jobId: string } | { error: string }> {
|
||||||
const supabase = createAdminClient();
|
const res = await fetch(gatewayUrl("/v1/renders"), {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: authHeaders(token),
|
||||||
|
body: JSON.stringify({
|
||||||
|
saved_project_id: payload.projectId,
|
||||||
|
quality: mapQuality(payload.settings.resolution),
|
||||||
|
resolution: payload.settings.resolution,
|
||||||
|
frame_rate: payload.settings.fps,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const { data, error } = await supabase
|
if (!res.ok) {
|
||||||
.from("render_jobs")
|
const err = (await res.json().catch(() => null)) as {
|
||||||
.insert({
|
message?: string;
|
||||||
project_id: payload.projectId,
|
} | null;
|
||||||
status: "queued",
|
return { error: err?.message ?? "Failed to create render job" };
|
||||||
progress: 0,
|
|
||||||
progress_message: "Queued for rendering",
|
|
||||||
scenes: payload.scenes,
|
|
||||||
settings: payload.settings,
|
|
||||||
})
|
|
||||||
.select("id")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error || !data) {
|
|
||||||
return { error: error?.message ?? "Failed to create render job" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = (await res.json().catch(() => null)) as { id?: string } | null;
|
||||||
|
if (!data?.id) return { error: "No job ID returned from render service" };
|
||||||
return { jobId: data.id };
|
return { jobId: data.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current render job status from V2.
|
||||||
|
* If the job is completed, also resolves the presigned export download URL.
|
||||||
|
*/
|
||||||
export async function getRenderJob(
|
export async function getRenderJob(
|
||||||
jobId: string
|
jobId: string,
|
||||||
|
token: string
|
||||||
): Promise<RenderJobRow | null> {
|
): Promise<RenderJobRow | null> {
|
||||||
const supabase = createAdminClient();
|
// 1. Poll progress endpoint
|
||||||
|
const progressRes = await fetch(gatewayUrl(`/v1/renders/${jobId}/progress`), {
|
||||||
|
cache: "no-store",
|
||||||
|
headers: authHeaders(token),
|
||||||
|
});
|
||||||
|
if (!progressRes.ok) return null;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const progress = (await progressRes.json().catch(() => null)) as {
|
||||||
.from("render_jobs")
|
step?: V2RenderStep;
|
||||||
.select("*")
|
progress?: number;
|
||||||
.eq("id", jobId)
|
message?: string;
|
||||||
.maybeSingle();
|
} | null;
|
||||||
|
if (!progress) return null;
|
||||||
|
|
||||||
if (error || !data) return null;
|
const step = progress.step ?? "Queued";
|
||||||
return data as RenderJobRow;
|
const status = stepToStatus(step);
|
||||||
}
|
|
||||||
|
|
||||||
export async function triggerRenderWorker(jobId: string): Promise<void> {
|
// 2. If done, resolve the export download URL via MinIO presigned link
|
||||||
const workerUrl = process.env.RENDER_WORKER_URL;
|
let outputUrl: string | null = null;
|
||||||
if (!workerUrl) return;
|
if (status === "completed") {
|
||||||
|
const jobRes = await fetch(gatewayUrl(`/v1/renders/${jobId}`), {
|
||||||
const secret = process.env.RENDER_WORKER_SECRET;
|
cache: "no-store",
|
||||||
|
headers: authHeaders(token),
|
||||||
try {
|
|
||||||
await fetch(`${workerUrl.replace(/\/$/, "")}/process`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(secret ? { Authorization: `Bearer ${secret}` } : {}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ jobId }),
|
|
||||||
});
|
});
|
||||||
} catch {
|
if (jobRes.ok) {
|
||||||
// Worker may be offline; job stays queued for retry/poll
|
const job = (await jobRes.json().catch(() => null)) as {
|
||||||
|
export_id?: string | null;
|
||||||
|
} | null;
|
||||||
|
if (job?.export_id) {
|
||||||
|
const urlRes = await fetch(
|
||||||
|
gatewayUrl(`/v1/exports/${job.export_id}/download-url`),
|
||||||
|
{ cache: "no-store", headers: authHeaders(token) }
|
||||||
|
);
|
||||||
|
if (urlRes.ok) {
|
||||||
|
const urlData = (await urlRes.json().catch(() => null)) as {
|
||||||
|
url?: string;
|
||||||
|
} | null;
|
||||||
|
outputUrl = urlData?.url ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: jobId,
|
||||||
|
project_id: "",
|
||||||
|
status,
|
||||||
|
progress: progress.progress ?? 0,
|
||||||
|
progress_message: step,
|
||||||
|
output_url: outputUrl,
|
||||||
|
error_message:
|
||||||
|
status === "failed" ? (progress.message ?? "Render failed") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op in V2 — the render orchestrator dispatches jobs to node agents
|
||||||
|
* automatically. Kept for API compatibility.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export async function triggerRenderWorker(_jobId: string): Promise<void> {
|
||||||
|
// V2 render service handles dispatch internally.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { createBrowserClient } from "@supabase/ssr";
|
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
||||||
|
|
||||||
import { isSupabaseConfigured } from "@/lib/supabase/config";
|
|
||||||
|
|
||||||
export function createClient(): SupabaseClient | null {
|
|
||||||
if (!isSupabaseConfigured()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createBrowserClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createClient } from "@supabase/supabase-js";
|
|
||||||
|
|
||||||
export function createAdminClient() {
|
|
||||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
||||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
||||||
|
|
||||||
if (!url || !serviceRoleKey) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY environment variables."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createClient(url, serviceRoleKey, {
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createBrowserClient } from "@supabase/ssr";
|
|
||||||
|
|
||||||
export function createClient() {
|
|
||||||
return createBrowserClient(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export function isSupabaseConfigured(): boolean {
|
|
||||||
return Boolean(
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL &&
|
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { createServerClient } from "@supabase/ssr";
|
|
||||||
import { NextResponse, type NextRequest } from "next/server";
|
|
||||||
|
|
||||||
export async function updateSession(request: NextRequest) {
|
|
||||||
let supabaseResponse = NextResponse.next({
|
|
||||||
request,
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
||||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
||||||
|
|
||||||
if (!url || !anonKey) {
|
|
||||||
return supabaseResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = createServerClient(url, anonKey, {
|
|
||||||
cookies: {
|
|
||||||
getAll() {
|
|
||||||
return request.cookies.getAll();
|
|
||||||
},
|
|
||||||
setAll(cookiesToSet) {
|
|
||||||
cookiesToSet.forEach(({ name, value }) => {
|
|
||||||
request.cookies.set(name, value);
|
|
||||||
});
|
|
||||||
supabaseResponse = NextResponse.next({
|
|
||||||
request,
|
|
||||||
});
|
|
||||||
cookiesToSet.forEach(({ name, value, options }) => {
|
|
||||||
supabaseResponse.cookies.set(name, value, options);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await supabase.auth.getUser();
|
|
||||||
|
|
||||||
return supabaseResponse;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { createServerClient } from "@supabase/ssr";
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
|
|
||||||
export async function createClient() {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
|
|
||||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
||||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
||||||
|
|
||||||
if (!url || !anonKey) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createServerClient(url, anonKey, {
|
|
||||||
cookies: {
|
|
||||||
getAll() {
|
|
||||||
return cookieStore.getAll();
|
|
||||||
},
|
|
||||||
setAll(cookiesToSet) {
|
|
||||||
try {
|
|
||||||
cookiesToSet.forEach(({ name, value, options }) => {
|
|
||||||
cookieStore.set(name, value, options);
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// setAll is a no-op when called from a Server Component.
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user