diff --git a/app/orgs/page.tsx b/app/orgs/page.tsx
new file mode 100644
index 0000000..c23823b
--- /dev/null
+++ b/app/orgs/page.tsx
@@ -0,0 +1,5 @@
+import OrgsPage from "@/components/Orgs/OrgsPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/components/Home/AdminDashboard.tsx b/components/Home/AdminDashboard.tsx
index 358d00d..e0687e9 100644
--- a/components/Home/AdminDashboard.tsx
+++ b/components/Home/AdminDashboard.tsx
@@ -1,4 +1,5 @@
import SandboxesNavButton from "@/components/Home/SandboxesNavButton";
+import OrgsNavButton from "@/components/Home/OrgsNavButton";
export default function AdminDashboard() {
return (
@@ -9,6 +10,7 @@ export default function AdminDashboard() {
);
diff --git a/components/Home/OrgsNavButton.tsx b/components/Home/OrgsNavButton.tsx
new file mode 100644
index 0000000..d00f425
--- /dev/null
+++ b/components/Home/OrgsNavButton.tsx
@@ -0,0 +1,10 @@
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+
+export default function OrgsNavButton() {
+ return (
+
+ );
+}
diff --git a/components/Orgs/OrgsPage.tsx b/components/Orgs/OrgsPage.tsx
new file mode 100644
index 0000000..4ba4edc
--- /dev/null
+++ b/components/Orgs/OrgsPage.tsx
@@ -0,0 +1,17 @@
+import OrgsTableContainer from "@/components/Orgs/OrgsTableContainer";
+
+export default function OrgsPage() {
+ return (
+
+
+
+ Org Repos
+
+
+ All organisation-level repositories and their statistics.
+
+
+
+
+ );
+}
diff --git a/components/Orgs/OrgsTable.tsx b/components/Orgs/OrgsTable.tsx
new file mode 100644
index 0000000..bf15200
--- /dev/null
+++ b/components/Orgs/OrgsTable.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import {
+ flexRender,
+ getCoreRowModel,
+ getSortedRowModel,
+ useReactTable,
+ type SortingState,
+} from "@tanstack/react-table";
+import { useState } from "react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { orgsColumns } from "@/components/Orgs/orgsColumns";
+import type { OrgRepoRow } from "@/types/org";
+
+interface OrgsTableProps {
+ repos: OrgRepoRow[];
+}
+
+/**
+ * Renders a shadcn Data Table of org repos and their statistics.
+ * Default sort: Latest Commit descending (most recently active first).
+ *
+ * @param repos - Array of org repo rows from the admin API
+ */
+export default function OrgsTable({ repos }: OrgsTableProps) {
+ const [sorting, setSorting] = useState([
+ { id: "latest_commit_at", desc: true },
+ ]);
+
+ const table = useReactTable({
+ data: repos,
+ columns: orgsColumns,
+ state: { sorting },
+ onSortingChange: setSorting,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ });
+
+ return (
+
+
+
+ {table.getHeaderGroups().map(headerGroup => (
+
+ {headerGroup.headers.map(header => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.length ? (
+ table.getRowModel().rows.map(row => (
+
+ {row.getVisibleCells().map(cell => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+ );
+}
diff --git a/components/Orgs/OrgsTableContainer.tsx b/components/Orgs/OrgsTableContainer.tsx
new file mode 100644
index 0000000..3882ebd
--- /dev/null
+++ b/components/Orgs/OrgsTableContainer.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { useAdminOrgs } from "@/hooks/useAdminOrgs";
+import OrgsTable from "@/components/Orgs/OrgsTable";
+
+export default function OrgsTableContainer() {
+ const { data: repos, isLoading, error } = useAdminOrgs();
+
+ if (isLoading) {
+ return (
+
+ Loading…
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error instanceof Error ? error.message : "Failed to load org repos"}
+
+ );
+ }
+
+ if (!repos || repos.length === 0) {
+ return (
+
+ No org repos found.
+
+ );
+ }
+
+ return ;
+}
diff --git a/components/Orgs/orgsColumns.tsx b/components/Orgs/orgsColumns.tsx
new file mode 100644
index 0000000..be625c6
--- /dev/null
+++ b/components/Orgs/orgsColumns.tsx
@@ -0,0 +1,124 @@
+import { type ColumnDef } from "@tanstack/react-table";
+import { ArrowUpDown, ArrowUp, ArrowDown, ExternalLink } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import type { OrgRepoRow } from "@/types/org";
+
+export const orgsColumns: ColumnDef[] = [
+ {
+ accessorKey: "repo_name",
+ header: "Repository",
+ cell: ({ row }) => (
+
+ {row.getValue("repo_name")}
+
+
+ ),
+ },
+ {
+ accessorKey: "total_commits",
+ header: ({ column }) => (
+
+ ),
+ },
+ {
+ accessorKey: "latest_5_commit_messages",
+ header: "Latest 5 Commits",
+ cell: ({ row }) => {
+ const messages = row.getValue("latest_5_commit_messages");
+ return (
+
+ {messages.map((msg, i) => (
+ -
+ {msg}
+
+ ))}
+
+ );
+ },
+ },
+ {
+ accessorKey: "earliest_commit_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const val = row.getValue("earliest_commit_at");
+ return val ? new Date(val).toLocaleString() : "—";
+ },
+ sortingFn: "datetime",
+ },
+ {
+ accessorKey: "latest_commit_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const val = row.getValue("latest_commit_at");
+ return val ? new Date(val).toLocaleString() : "—";
+ },
+ sortingFn: "datetime",
+ },
+ {
+ accessorKey: "account_repos_with_submodule",
+ header: ({ column }) => (
+
+ ),
+ },
+];
diff --git a/hooks/useAdminOrgs.ts b/hooks/useAdminOrgs.ts
new file mode 100644
index 0000000..77b5f21
--- /dev/null
+++ b/hooks/useAdminOrgs.ts
@@ -0,0 +1,23 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { usePrivy } from "@privy-io/react-auth";
+import { fetchAdminOrgs } from "@/lib/fetchAdminOrgs";
+
+/**
+ * Fetches org repo statistics from GET /api/admins/sandboxes/orgs
+ * using TanStack Query. Authenticates with the Privy access token.
+ */
+export function useAdminOrgs() {
+ const { ready, authenticated, getAccessToken } = usePrivy();
+
+ return useQuery({
+ queryKey: ["admin", "orgs"],
+ queryFn: async () => {
+ const token = await getAccessToken();
+ if (!token) throw new Error("Not authenticated");
+ return fetchAdminOrgs(token);
+ },
+ enabled: ready && authenticated,
+ });
+}
diff --git a/lib/fetchAdminOrgs.ts b/lib/fetchAdminOrgs.ts
new file mode 100644
index 0000000..9788f5e
--- /dev/null
+++ b/lib/fetchAdminOrgs.ts
@@ -0,0 +1,23 @@
+import { API_BASE_URL } from "@/lib/consts";
+import type { OrgRepoRow } from "@/types/org";
+
+/**
+ * Fetches org repo statistics from GET /api/admins/sandboxes/orgs.
+ * Authenticates using the caller's Privy access token.
+ *
+ * @param accessToken - Privy access token from getAccessToken()
+ * @returns Array of org repo rows
+ */
+export async function fetchAdminOrgs(accessToken: string): Promise {
+ const res = await fetch(`${API_BASE_URL}/api/admins/sandboxes/orgs`, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ });
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.message ?? `HTTP ${res.status}`);
+ }
+
+ const data = await res.json();
+ return data.repos ?? [];
+}
diff --git a/types/org.ts b/types/org.ts
new file mode 100644
index 0000000..deae21a
--- /dev/null
+++ b/types/org.ts
@@ -0,0 +1,14 @@
+export interface OrgRepoRow {
+ repo_name: string;
+ repo_url: string;
+ total_commits: number;
+ latest_5_commit_messages: string[];
+ earliest_commit_at: string;
+ latest_commit_at: string;
+ account_repos_with_submodule: number;
+}
+
+export interface AdminOrgsResponse {
+ status: "success" | "error";
+ repos: OrgRepoRow[];
+}