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",