From a2144a1c44b1ae8acb864fc6b7b1c53859366a7f Mon Sep 17 00:00:00 2001 From: Sr Dev Agent Date: Sun, 12 Apr 2026 00:05:50 +0000 Subject: [PATCH 1/2] feat: add Agent Sign-Ups admin page New admin page at /agent-signups that displays API key sign-up records from AI agents. Includes line chart over time, pie chart by email, sortable data table, and period filtering. Co-Authored-By: Paperclip --- app/agent-signups/page.tsx | 10 +++ components/AgentSignups/AgentSignupsPage.tsx | 79 +++++++++++++++++++ components/AgentSignups/AgentSignupsStats.tsx | 15 ++++ components/AgentSignups/AgentSignupsTable.tsx | 78 ++++++++++++++++++ .../AgentSignups/agentSignupsColumns.tsx | 31 ++++++++ components/Home/AdminDashboard.tsx | 1 + hooks/useAgentSignups.ts | 20 +++++ lib/agent-signups/getSignupsByDate.ts | 22 ++++++ lib/agent-signups/getSignupsByEmail.ts | 22 ++++++ lib/recoup/fetchAgentSignups.ts | 22 ++++++ types/agentSignups.ts | 12 +++ 11 files changed, 312 insertions(+) create mode 100644 app/agent-signups/page.tsx create mode 100644 components/AgentSignups/AgentSignupsPage.tsx create mode 100644 components/AgentSignups/AgentSignupsStats.tsx create mode 100644 components/AgentSignups/AgentSignupsTable.tsx create mode 100644 components/AgentSignups/agentSignupsColumns.tsx create mode 100644 hooks/useAgentSignups.ts create mode 100644 lib/agent-signups/getSignupsByDate.ts create mode 100644 lib/agent-signups/getSignupsByEmail.ts create mode 100644 lib/recoup/fetchAgentSignups.ts create mode 100644 types/agentSignups.ts diff --git a/app/agent-signups/page.tsx b/app/agent-signups/page.tsx new file mode 100644 index 0000000..69fefe8 --- /dev/null +++ b/app/agent-signups/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import AgentSignupsPage from "@/components/AgentSignups/AgentSignupsPage"; + +export const metadata: Metadata = { + title: "Agent Sign-Ups — Recoup Admin", +}; + +export default function Page() { + return ; +} diff --git a/components/AgentSignups/AgentSignupsPage.tsx b/components/AgentSignups/AgentSignupsPage.tsx new file mode 100644 index 0000000..33f7bc0 --- /dev/null +++ b/components/AgentSignups/AgentSignupsPage.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState } from "react"; +import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; +import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; +import { useAgentSignups } from "@/hooks/useAgentSignups"; +import AgentSignupsTable from "@/components/AgentSignups/AgentSignupsTable"; +import AgentSignupsStats from "@/components/AgentSignups/AgentSignupsStats"; +import PeriodSelector from "@/components/Admin/PeriodSelector"; +import AdminLineChart from "@/components/Admin/AdminLineChart"; +import AdminPieChart from "@/components/Admin/AdminPieChart"; +import TableSkeleton from "@/components/Sandboxes/TableSkeleton"; +import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton"; +import { getSignupsByDate } from "@/lib/agent-signups/getSignupsByDate"; +import { getSignupsByEmail } from "@/lib/agent-signups/getSignupsByEmail"; +import type { AdminPeriod } from "@/types/admin"; + +export default function AgentSignupsPage() { + const [period, setPeriod] = useState("all"); + const { data, isLoading, error } = useAgentSignups(period); + + const signupsByDate = data ? getSignupsByDate(data.signups) : []; + const signupsByEmail = data ? getSignupsByEmail(data.signups) : []; + + return ( +
+
+
+ +

+ Agent Sign-Ups +

+

+ API key sign-ups from AI agents, grouped by time period. +

+
+ +
+ +
+ + {data && } +
+ + {isLoading && ( + <> + + + + )} + + {error && ( +
+ {error instanceof Error ? error.message : "Failed to load agent sign-ups"} +
+ )} + + {!isLoading && !error && data && data.signups.length === 0 && ( +
+ No agent sign-ups found for this period. +
+ )} + + {!isLoading && !error && data && data.signups.length > 0 && ( + <> + ({ date: d.date, count: d.count }))} + label="Sign-Ups" + /> +
+ +
+ + + )} +
+ ); +} diff --git a/components/AgentSignups/AgentSignupsStats.tsx b/components/AgentSignups/AgentSignupsStats.tsx new file mode 100644 index 0000000..01ce793 --- /dev/null +++ b/components/AgentSignups/AgentSignupsStats.tsx @@ -0,0 +1,15 @@ +import type { AgentSignupsResponse } from "@/types/agentSignups"; + +interface AgentSignupsStatsProps { + data: AgentSignupsResponse; +} + +export default function AgentSignupsStats({ data }: AgentSignupsStatsProps) { + return ( +
+ + {data.total} sign-ups + +
+ ); +} diff --git a/components/AgentSignups/AgentSignupsTable.tsx b/components/AgentSignups/AgentSignupsTable.tsx new file mode 100644 index 0000000..4ef64e0 --- /dev/null +++ b/components/AgentSignups/AgentSignupsTable.tsx @@ -0,0 +1,78 @@ +"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 { agentSignupsColumns } from "@/components/AgentSignups/agentSignupsColumns"; +import type { AgentSignup } from "@/types/agentSignups"; + +interface AgentSignupsTableProps { + signups: AgentSignup[]; +} + +export default function AgentSignupsTable({ signups }: AgentSignupsTableProps) { + const [sorting, setSorting] = useState([ + { id: "created_at", desc: true }, + ]); + + const table = useReactTable({ + data: signups, + columns: agentSignupsColumns, + 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/AgentSignups/agentSignupsColumns.tsx b/components/AgentSignups/agentSignupsColumns.tsx new file mode 100644 index 0000000..054b1c9 --- /dev/null +++ b/components/AgentSignups/agentSignupsColumns.tsx @@ -0,0 +1,31 @@ +import { type ColumnDef } from "@tanstack/react-table"; +import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader"; +import type { AgentSignup } from "@/types/agentSignups"; + +export const agentSignupsColumns: ColumnDef[] = [ + { + id: "email", + accessorKey: "email", + header: ({ column }) => , + cell: ({ getValue }) => ( + {getValue()} + ), + }, + { + id: "name", + accessorKey: "name", + header: "Key Name", + cell: ({ getValue }) => ( + ()}> + {getValue()} + + ), + }, + { + id: "created_at", + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ getValue }) => new Date(getValue()).toLocaleString(), + sortingFn: "datetime", + }, +]; diff --git a/components/Home/AdminDashboard.tsx b/components/Home/AdminDashboard.tsx index 3633e68..2c23816 100644 --- a/components/Home/AdminDashboard.tsx +++ b/components/Home/AdminDashboard.tsx @@ -14,6 +14,7 @@ export default function AdminDashboard() { + ); diff --git a/hooks/useAgentSignups.ts b/hooks/useAgentSignups.ts new file mode 100644 index 0000000..99c69af --- /dev/null +++ b/hooks/useAgentSignups.ts @@ -0,0 +1,20 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; +import { fetchAgentSignups } from "@/lib/recoup/fetchAgentSignups"; +import type { AdminPeriod } from "@/types/admin"; + +export function useAgentSignups(period: AdminPeriod) { + const { ready, authenticated, getAccessToken } = usePrivy(); + + return useQuery({ + queryKey: ["admin", "agent-signups", period], + queryFn: async () => { + const token = await getAccessToken(); + if (!token) throw new Error("Not authenticated"); + return fetchAgentSignups(token, period); + }, + enabled: ready && authenticated, + }); +} diff --git a/lib/agent-signups/getSignupsByDate.ts b/lib/agent-signups/getSignupsByDate.ts new file mode 100644 index 0000000..c8270ce --- /dev/null +++ b/lib/agent-signups/getSignupsByDate.ts @@ -0,0 +1,22 @@ +import type { AgentSignup } from "@/types/agentSignups"; + +export interface SignupsByDateEntry { + date: string; + count: number; +} + +/** + * Aggregates agent sign-ups by UTC date (YYYY-MM-DD) for charting. + */ +export function getSignupsByDate(signups: AgentSignup[]): SignupsByDateEntry[] { + const counts: Record = {}; + + for (const signup of signups) { + const date = signup.created_at.slice(0, 10); + counts[date] = (counts[date] || 0) + 1; + } + + return Object.entries(counts) + .map(([date, count]) => ({ date, count })) + .sort((a, b) => a.date.localeCompare(b.date)); +} diff --git a/lib/agent-signups/getSignupsByEmail.ts b/lib/agent-signups/getSignupsByEmail.ts new file mode 100644 index 0000000..c4a63d1 --- /dev/null +++ b/lib/agent-signups/getSignupsByEmail.ts @@ -0,0 +1,22 @@ +import type { AgentSignup } from "@/types/agentSignups"; + +export interface SignupsByEmailEntry { + name: string; + value: number; +} + +/** + * Aggregates agent sign-ups by email address for pie chart display. + */ +export function getSignupsByEmail(signups: AgentSignup[]): SignupsByEmailEntry[] { + const counts: Record = {}; + + for (const signup of signups) { + const key = signup.email || "Unknown"; + counts[key] = (counts[key] || 0) + 1; + } + + return Object.entries(counts) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} diff --git a/lib/recoup/fetchAgentSignups.ts b/lib/recoup/fetchAgentSignups.ts new file mode 100644 index 0000000..5fe3335 --- /dev/null +++ b/lib/recoup/fetchAgentSignups.ts @@ -0,0 +1,22 @@ +import { API_BASE_URL } from "@/lib/consts"; +import type { AdminPeriod } from "@/types/admin"; +import type { AgentSignupsResponse } from "@/types/agentSignups"; + +export async function fetchAgentSignups( + accessToken: string, + period: AdminPeriod, +): Promise { + const url = new URL(`${API_BASE_URL}/api/admins/agent-signups`); + url.searchParams.set("period", period); + + const res = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? body.message ?? `HTTP ${res.status}`); + } + + return res.json(); +} diff --git a/types/agentSignups.ts b/types/agentSignups.ts new file mode 100644 index 0000000..1d1cf52 --- /dev/null +++ b/types/agentSignups.ts @@ -0,0 +1,12 @@ +export type AgentSignup = { + id: string; + name: string; + email: string; + created_at: string; +}; + +export type AgentSignupsResponse = { + status: "success" | "error"; + total: number; + signups: AgentSignup[]; +}; From 3a2efd62ecb231712f1bb0839e93c42e5d058df3 Mon Sep 17 00:00:00 2001 From: Sr Dev Agent Date: Sun, 12 Apr 2026 19:02:32 +0000 Subject: [PATCH 2/2] fix: remove non-existent AdminPieChart import from AgentSignupsPage Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- components/AgentSignups/AgentSignupsPage.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/components/AgentSignups/AgentSignupsPage.tsx b/components/AgentSignups/AgentSignupsPage.tsx index 33f7bc0..47d9ab7 100644 --- a/components/AgentSignups/AgentSignupsPage.tsx +++ b/components/AgentSignups/AgentSignupsPage.tsx @@ -8,11 +8,9 @@ import AgentSignupsTable from "@/components/AgentSignups/AgentSignupsTable"; import AgentSignupsStats from "@/components/AgentSignups/AgentSignupsStats"; import PeriodSelector from "@/components/Admin/PeriodSelector"; import AdminLineChart from "@/components/Admin/AdminLineChart"; -import AdminPieChart from "@/components/Admin/AdminPieChart"; import TableSkeleton from "@/components/Sandboxes/TableSkeleton"; import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton"; import { getSignupsByDate } from "@/lib/agent-signups/getSignupsByDate"; -import { getSignupsByEmail } from "@/lib/agent-signups/getSignupsByEmail"; import type { AdminPeriod } from "@/types/admin"; export default function AgentSignupsPage() { @@ -20,7 +18,6 @@ export default function AgentSignupsPage() { const { data, isLoading, error } = useAgentSignups(period); const signupsByDate = data ? getSignupsByDate(data.signups) : []; - const signupsByEmail = data ? getSignupsByEmail(data.signups) : []; return (
@@ -68,9 +65,6 @@ export default function AgentSignupsPage() { data={signupsByDate.map((d) => ({ date: d.date, count: d.count }))} label="Sign-Ups" /> -
- -
)}