diff --git a/src/app/api/render/[jobId]/status/route.ts b/src/app/api/render/[jobId]/status/route.ts index 9f26bbe..2a41aea 100644 --- a/src/app/api/render/[jobId]/status/route.ts +++ b/src/app/api/render/[jobId]/status/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; +import { getAccessToken } from "@/lib/auth/session"; import { getRenderJob } from "@/lib/render-jobs"; export const runtime = "nodejs"; @@ -15,7 +16,12 @@ export async function GET(_request: Request, context: RouteContext) { 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) { return NextResponse.json({ error: "Job not found" }, { status: 404 }); } diff --git a/src/app/api/render/route.ts b/src/app/api/render/route.ts index f0a5e76..21be826 100644 --- a/src/app/api/render/route.ts +++ b/src/app/api/render/route.ts @@ -1,11 +1,17 @@ import { NextResponse } from "next/server"; +import { getAccessToken } from "@/lib/auth/session"; +import { createRenderJob } from "@/lib/render-jobs"; import { renderRequestSchema } from "@/lib/render-schemas"; -import { createRenderJob, triggerRenderWorker } from "@/lib/render-jobs"; export const runtime = "nodejs"; export async function POST(request: Request) { + const token = await getAccessToken(); + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + let body: unknown; try { 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) { return NextResponse.json({ error: result.error }, { status: 500 }); } - await triggerRenderWorker(result.jobId); - return NextResponse.json({ jobId: result.jobId }); } diff --git a/src/lib/render-jobs.ts b/src/lib/render-jobs.ts index 1d7a0b6..e8b81c0 100644 --- a/src/lib/render-jobs.ts +++ b/src/lib/render-jobs.ts @@ -1,5 +1,27 @@ -import { createAdminClient } from "@/lib/supabase/admin"; -import type { RenderRequest, RenderJobStatus } from "@/lib/render-schemas"; +import { gatewayUrl } from "@/lib/api/gateway"; +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 { id: string; @@ -8,69 +30,136 @@ export interface RenderJobRow { progress: number; progress_message: string | null; output_url: string | null; - scenes: RenderRequest["scenes"]; - settings: RenderRequest["settings"]; 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 { + 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( - payload: RenderRequest + payload: RenderRequest, + token: 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 - .from("render_jobs") - .insert({ - project_id: payload.projectId, - status: "queued", - 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" }; + if (!res.ok) { + const err = (await res.json().catch(() => null)) as { + message?: string; + } | null; + return { error: err?.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 }; } +/** + * Fetch the current render job status from V2. + * If the job is completed, also resolves the presigned export download URL. + */ export async function getRenderJob( - jobId: string + jobId: string, + token: string ): Promise { - 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 - .from("render_jobs") - .select("*") - .eq("id", jobId) - .maybeSingle(); + const progress = (await progressRes.json().catch(() => null)) as { + step?: V2RenderStep; + progress?: number; + message?: string; + } | null; + if (!progress) return null; - if (error || !data) return null; - return data as RenderJobRow; -} + const step = progress.step ?? "Queued"; + const status = stepToStatus(step); -export async function triggerRenderWorker(jobId: string): Promise { - const workerUrl = process.env.RENDER_WORKER_URL; - if (!workerUrl) return; - - const secret = process.env.RENDER_WORKER_SECRET; - - try { - await fetch(`${workerUrl.replace(/\/$/, "")}/process`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(secret ? { Authorization: `Bearer ${secret}` } : {}), - }, - body: JSON.stringify({ jobId }), + // 2. If done, resolve the export download URL via MinIO presigned link + let outputUrl: string | null = null; + if (status === "completed") { + const jobRes = await fetch(gatewayUrl(`/v1/renders/${jobId}`), { + cache: "no-store", + headers: authHeaders(token), }); - } catch { - // Worker may be offline; job stays queued for retry/poll + if (jobRes.ok) { + 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 { + // V2 render service handles dispatch internally. } diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts deleted file mode 100644 index 1c1cd84..0000000 --- a/src/lib/supabase.ts +++ /dev/null @@ -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! - ); -} diff --git a/src/lib/supabase/admin.ts b/src/lib/supabase/admin.ts deleted file mode 100644 index 671a4af..0000000 --- a/src/lib/supabase/admin.ts +++ /dev/null @@ -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, - }, - }); -} diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts deleted file mode 100644 index 9f2891b..0000000 --- a/src/lib/supabase/client.ts +++ /dev/null @@ -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! - ); -} diff --git a/src/lib/supabase/config.ts b/src/lib/supabase/config.ts deleted file mode 100644 index 22b86b4..0000000 --- a/src/lib/supabase/config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function isSupabaseConfigured(): boolean { - return Boolean( - process.env.NEXT_PUBLIC_SUPABASE_URL && - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY - ); -} diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts deleted file mode 100644 index 1353d73..0000000 --- a/src/lib/supabase/middleware.ts +++ /dev/null @@ -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; -} diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts deleted file mode 100644 index 4adc9b3..0000000 --- a/src/lib/supabase/server.ts +++ /dev/null @@ -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. - } - }, - }, - }); -}