TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import { requireAdmin } from '@/lib/admin/auth';3import { costsOverview } from '@/lib/admin/queries';4import { AdminShell, Bars, Kpi, n } from '@/components/admin/shell';5import { Table, th, td, tdNum } from '@/components/ui/primitives';67export default async function CostsPage({ searchParams }: { searchParams: Promise<{ days?: string }> }) {8 await requireAdmin();9 const sp = await searchParams;10 const days = [7, 30, 90].includes(Number(sp.days)) ? Number(sp.days) : 30;11 const c = await costsOverview(days);12 const t = (c.totals ?? {}) as Record<string, unknown>;13 const raw = Number((c.perRecord as Record<string, unknown> | null)?.raw_records ?? 0);14 const dayMap = new Map<string, { usd: number; credits: number }>();15 for (const r of c.byDay) {16 const k = String(r.day);17 const cur = dayMap.get(k) ?? { usd: 0, credits: 0 };18 cur.usd += Number(r.usd);19 cur.credits += Number(r.credits);20 dayMap.set(k, cur);21 }22 const series = [...dayMap.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([label, v]) => ({ label, value: v.usd }));23 const creditSeries = [...dayMap.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([label, v]) => ({ label, value: v.credits }));24 return (25 <AdminShell26 current="/admin/costs"27 title="Costs"28 subtitle="Crawler credits and AI spend by day, kind, provider, connector, category and endpoint (estimates from list prices)"29 actions={<div className="flex gap-1 text-xs">{[7, 30, 90].map((d) => <Link key={d} href={`/admin/costs?days=${d}`} className={`rounded-md border px-2 py-1 ${d === days ? 'border-accent bg-accent text-accent-fg' : 'border-border hover:bg-inset'}`}>{d}d</Link>)}</div>}30 >31 <div className="grid grid-cols-2 gap-3 md:grid-cols-5">32 <Kpi label={`Spend ${days}d`} value={`$${n(t.usd, 2)}`} sub={`${n(t.events)} cost events`} />33 <Kpi label="AI" value={`$${n(t.ai_usd, 2)}`} />34 <Kpi label="Firecrawl credits" value={n(t.firecrawl_credits)} />35 <Kpi label="Scrapfly credits" value={n(t.scrapfly_credits)} />36 <Kpi label="Per raw record" value={raw ? `$${(Number(t.usd ?? 0) / raw).toFixed(4)}` : '—'} sub={`${n(raw)} raw records`} />37 </div>38 <div className="mt-6 grid gap-6 lg:grid-cols-2">39 <section className="card p-4"><h2 className="mb-2 text-sm font-semibold">USD per day</h2><Bars points={series} format={(v) => `$${v.toFixed(3)}`} /></section>40 <section className="card p-4"><h2 className="mb-2 text-sm font-semibold">Crawler credits per day</h2><Bars points={creditSeries} format={(v) => `${Math.round(v)} credits`} /></section>41 </div>42 <div className="mt-6 grid gap-6 lg:grid-cols-2">43 <section className="card"><header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">By kind / provider</h2></header>44 <Table><thead><tr><th className={th}>Kind</th><th className={th}>Provider</th><th className={`${th} text-right`}>Credits</th><th className={`${th} text-right`}>USD</th><th className={`${th} text-right`}>Events</th></tr></thead>45 <tbody>{c.byKind.length === 0 ? <tr><td className={td} colSpan={5}><span className="text-subtle">No cost events yet.</span></td></tr> : c.byKind.map((r, i) => <tr key={i}><td className={td}>{String(r.kind)}</td><td className={td}>{String(r.provider)}</td><td className={tdNum}>{n(r.credits, 1)}</td><td className={tdNum}>${n(r.usd, 3)}</td><td className={tdNum}>{n(r.events)}</td></tr>)}</tbody></Table>46 </section>47 <section className="card"><header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">By connector</h2></header>48 <Table><thead><tr><th className={th}>Connector</th><th className={`${th} text-right`}>Credits</th><th className={`${th} text-right`}>USD</th><th className={`${th} text-right`}>Events</th></tr></thead>49 <tbody>{c.byConnector.length === 0 ? <tr><td className={td} colSpan={4}><span className="text-subtle">—</span></td></tr> : c.byConnector.map((r, i) => <tr key={i}><td className={td}><Link href={`/admin/connectors/${String(r.connector_id)}`} className="hover:underline">{String(r.connector_id)}</Link></td><td className={tdNum}>{n(r.credits, 1)}</td><td className={tdNum}>${n(r.usd, 3)}</td><td className={tdNum}>{n(r.events)}</td></tr>)}</tbody></Table>50 </section>51 <section className="card"><header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">By category</h2></header>52 <Table><thead><tr><th className={th}>Category</th><th className={`${th} text-right`}>Credits</th><th className={`${th} text-right`}>USD</th></tr></thead>53 <tbody>{c.byCategory.length === 0 ? <tr><td className={td} colSpan={3}><span className="text-subtle">—</span></td></tr> : c.byCategory.map((r, i) => <tr key={i}><td className={td}>{String(r.category_slug)}</td><td className={tdNum}>{n(r.credits, 1)}</td><td className={tdNum}>${n(r.usd, 3)}</td></tr>)}</tbody></Table>54 </section>55 <section className="card"><header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">AI by endpoint / model</h2></header>56 <Table><thead><tr><th className={th}>Endpoint</th><th className={th}>Model</th><th className={`${th} text-right`}>Calls</th><th className={`${th} text-right`}>In tok</th><th className={`${th} text-right`}>Out tok</th><th className={`${th} text-right`}>USD</th></tr></thead>57 <tbody>{c.byEndpoint.length === 0 ? <tr><td className={td} colSpan={6}><span className="text-subtle">—</span></td></tr> : c.byEndpoint.map((r, i) => <tr key={i}><td className={td}>{String(r.endpoint)}</td><td className={`${td} font-mono text-[11px]`}>{String(r.model)}</td><td className={tdNum}>{n(r.events)}</td><td className={tdNum}>{n(r.input_tokens)}</td><td className={tdNum}>{n(r.output_tokens)}</td><td className={tdNum}>${n(r.usd, 4)}</td></tr>)}</tbody></Table>58 </section>59 </div>60 </AdminShell>61 );62}63