diff --git a/src/app/(dashboard)/dashboard/batch/page.tsx b/src/app/(dashboard)/dashboard/batch/page.tsx new file mode 100644 index 0000000000..9e9fe12f69 --- /dev/null +++ b/src/app/(dashboard)/dashboard/batch/page.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Card from "@/shared/components/Card"; +import EmptyState from "@/shared/components/EmptyState"; +import { formatDistanceToNow } from "date-fns"; + +export default function BatchPage() { + const [batches, setBatches] = useState([]); + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<"batches" | "files">("batches"); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + setLoading(true); + try { + const [batchesRes, filesRes] = await Promise.all([ + fetch("/api/batches"), + fetch("/api/files"), + ]); + if (batchesRes.ok) { + const data = await batchesRes.json(); + setBatches(data.batches || []); + } + if (filesRes.ok) { + const data = await filesRes.json(); + setFiles(data.files || []); + } + } catch (error) { + console.error("Failed to fetch batches/files", error); + } finally { + setLoading(false); + } + }; + + const renderBatches = () => { + if (batches.length === 0) { + return ( + + ); + } + + return ( +
+ {batches.map((batch: any) => ( + +
+
+ {batch.id} + + {batch.status} + +
+ + {formatDistanceToNow(batch.createdAt, { addSuffix: true })} + +
+ +
+
+ Endpoint + {batch.endpoint} +
+
+ Input File + + {batch.inputFileId} + +
+
+ Output File + {batch.outputFileId || "—"} +
+
+ Progress + + {batch.requestCountsCompleted || 0} / {batch.requestCountsTotal || 0} reqs + +
+
+
+ ))} +
+ ); + }; + + const renderFiles = () => { + if (files.length === 0) { + return ( + + ); + } + + return ( +
+ {files.map((file: any) => ( + +
+
+ {file.id} +
+ + {formatDistanceToNow(file.createdAt, { addSuffix: true })} + +
+ +
+
+ Filename + {file.filename} +
+
+ Purpose + {file.purpose} +
+
+ Size + {(file.bytes / 1024).toFixed(2)} KB +
+
+ Status + {file.status} +
+
+
+ ))} +
+ ); + }; + + return ( +
+
+
+
+

Batch Processing

+

Monitor asynchronous batch requests and files

+
+ +
+ +
+ + +
+ + {loading && batches.length === 0 && files.length === 0 ? ( +
+
+
+ ) : activeTab === "batches" ? ( + renderBatches() + ) : ( + renderFiles() + )} +
+
+ ); +} diff --git a/src/app/api/batches/route.ts b/src/app/api/batches/route.ts new file mode 100644 index 0000000000..e67ff13507 --- /dev/null +++ b/src/app/api/batches/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { listBatches } from "@/lib/localDb"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; + +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + try { + const url = new URL(request.url); + const limit = Number.parseInt(url.searchParams.get("limit") || "100", 10); + const batches = listBatches(undefined, limit); + return NextResponse.json({ batches }); + } catch (error) { + console.log("Error fetching batches:", error); + return NextResponse.json({ error: "Failed to fetch batches" }, { status: 500 }); + } +} diff --git a/src/app/api/files/route.ts b/src/app/api/files/route.ts new file mode 100644 index 0000000000..fb34312f19 --- /dev/null +++ b/src/app/api/files/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { listFiles } from "@/lib/localDb"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; + +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + try { + const url = new URL(request.url); + const limit = Number.parseInt(url.searchParams.get("limit") || "100", 10); + const files = listFiles({ limit }); + return NextResponse.json({ files }); + } catch (error) { + console.log("Error fetching files:", error); + return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 }); + } +} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d0f1202c61..4ed9b281af 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -146,6 +146,7 @@ "dashboard": "Dashboard", "providers": "Providers", "combos": "Combos", + "batch": "Batch Jobs", "usage": "Usage", "analytics": "Analytics", "costs": "Costs", diff --git a/src/lib/db/migrations/027_create_files_and_batches.sql b/src/lib/db/migrations/028_create_files_and_batches.sql similarity index 100% rename from src/lib/db/migrations/027_create_files_and_batches.sql rename to src/lib/db/migrations/028_create_files_and_batches.sql diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 094729ea54..94933e007e 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -4,6 +4,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "api-manager", "providers", "combos", + "batch", "costs", "analytics", "cache", @@ -51,6 +52,7 @@ const PRIMARY_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [ { id: "api-manager", href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" }, { id: "providers", href: "/dashboard/providers", i18nKey: "providers", icon: "dns" }, { id: "combos", href: "/dashboard/combos", i18nKey: "combos", icon: "layers" }, + { id: "batch", href: "/dashboard/batch", i18nKey: "batch", icon: "view_list" }, { id: "costs", href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" }, { id: "analytics", href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" }, { id: "cache", href: "/dashboard/cache", i18nKey: "cache", icon: "cached" }, diff --git a/tests/unit/sidebar-visibility.test.ts b/tests/unit/sidebar-visibility.test.ts index b20829c284..14a33c10f0 100644 --- a/tests/unit/sidebar-visibility.test.ts +++ b/tests/unit/sidebar-visibility.test.ts @@ -29,6 +29,7 @@ test("primary sidebar items place limits after cache", () => { "api-manager", "providers", "combos", + "batch", "costs", "analytics", "cache",