TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ArrowRight, BookOpen, CheckCircle2, Circle, KeyRound, Play } from "lucide-react";3import { PLAN_LIMITS, isUnlimited, normalizePlan } from "@fetcha/core";4import { getWorkspace } from "@/lib/session";5import { formatBytes, formatCompact, formatMs, formatNumber, formatPercent, formatUsd, formatDateOnly } from "@/lib/format";6import { getActiveSessionCount, getMonthStats, getNetworkDistribution, getOnboardingState, getRecentRequests, getRequestSeries, type Scope } from "@/lib/queries/dashboard";7import { PageHeader, SectionTitle } from "@/components/ui/page-header";8import { Stat, StatGrid } from "@/components/ui/stat";9import { Badge } from "@/components/ui/badge";10import { Alert } from "@/components/ui/alert";11import { Button } from "@/components/ui/button";12import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";13import { EmptyState } from "@/components/ui/empty-state";14import { ChartFrame } from "@/components/dashboard/charts/chart-frame";15import { RequestsBarChart } from "@/components/dashboard/charts/requests-bar-chart";16import { NetworkDonut } from "@/components/dashboard/charts/network-donut";17import { QuotaBar } from "@/components/dashboard/charts/quota-bar";18import { RequestsTable } from "@/components/dashboard/requests/requests-table";1920export const dynamic = "force-dynamic";2122export default async function DashboardOverviewPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {23 const [ws, sp] = await Promise.all([getWorkspace(), searchParams]);24 const scope: Scope = { organizationId: ws.organization.id, projectId: ws.project.id };25 const now = new Date();26 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));27 const thirtyDaysAgo = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 29));2829 const [month, series, network, recent, onboarding, activeSessions] = await Promise.all([30 getMonthStats(scope, monthStart),31 getRequestSeries(scope, thirtyDaysAgo, "day"),32 getNetworkDistribution(scope, thirtyDaysAgo),33 getRecentRequests(scope, 8),34 getOnboardingState(scope),35 getActiveSessionCount(scope),36 ]);3738 const limits = PLAN_LIMITS[normalizePlan(ws.organization.plan)];39 const unlimited = isUnlimited(limits);40 const projectLimit = ws.project.monthlyRequestLimit;41 const effectiveLimit = projectLimit && (unlimited || projectLimit < limits.monthly_requests) ? projectLimit : unlimited ? null : limits.monthly_requests;42 const firstName = ws.user.name?.trim().split(/\s+/)[0] || ws.user.email.split("@")[0];43 const verified = sp.verified === "1";44 const seriesHasData = series.some((p) => p.success + p.failed > 0);45 const onboardingDone = onboarding.hasApiKey && onboarding.hasRequest;46 const softLimit = ws.project.softLimitUsd ?? ws.organization.softLimitUsd ?? null;47 const hardLimit = ws.project.hardLimitUsd ?? ws.organization.hardLimitUsd ?? null;48 const limitSource = ws.project.softLimitUsd !== null || ws.project.hardLimitUsd !== null ? "project" : "organization";4950 return (51 <div className="flex flex-col gap-6">52 {verified ? (53 <Alert variant="success" title="Email verified">54 Production API access is unlocked for this organization.55 </Alert>56 ) : null}5758 <PageHeader59 eyebrow={60 <>61 {ws.project.name} · {ws.project.environment}62 </>63 }64 title={65 <span className="inline-flex flex-wrap items-center gap-2.5">66 Welcome back, {firstName}67 <Badge variant="accent">{limits.label} plan</Badge>68 </span>69 }70 description={`Here is what happened in ${ws.project.name} since ${formatDateOnly(monthStart)}. Figures reflect requests billed to this project.`}71 actions={72 <>73 <Button asChild variant="outline" size="sm">74 <Link href="/dashboard/requests">View requests</Link>75 </Button>76 <Button asChild variant="primary" size="sm">77 <Link href="/dashboard/playground">78 <Play /> Open Playground79 </Link>80 </Button>81 </>82 }83 />8485 {!onboardingDone ? (86 <Card>87 <CardHeader className="pb-2">88 <CardTitle>Getting started</CardTitle>89 <CardDescription>Three steps to your first routed request. This card disappears once they are all done.</CardDescription>90 </CardHeader>91 <CardContent>92 <ol className="divide-y divide-border">93 {[94 { done: onboarding.hasApiKey, icon: KeyRound, title: "Create an API key", body: "Keys are shown once at creation. Use a test key while you explore.", href: "/dashboard/api-keys", cta: "Create key" },95 { done: onboarding.hasRequest, icon: Play, title: "Run your first request", body: "Try the Playground or send a POST /v1/fetch with cURL.", href: "/dashboard/playground", cta: "Open Playground" },96 { done: false, icon: BookOpen, title: "Read the quickstart", body: "Five minutes on routing, network classes, sessions and error codes.", href: "/docs/quickstart", cta: "Read docs", external: true },97 ].map((step) => (98 <li key={step.title} className="flex items-start gap-3 py-3 first:pt-0 last:pb-0">99 {step.done ? <CheckCircle2 className="mt-0.5 size-4 shrink-0 text-success" aria-label="Done" /> : <Circle className="mt-0.5 size-4 shrink-0 text-fg-subtle" aria-label="To do" />}100 <div className="min-w-0 flex-1">101 <div className={`text-[13.5px] font-medium ${step.done ? "text-fg-muted line-through decoration-fg-subtle" : ""}`}>{step.title}</div>102 <p className="text-[12.5px] text-fg-muted">{step.body}</p>103 </div>104 {!step.done ? (105 <Button asChild variant="ghost" size="sm" className="shrink-0">106 <Link href={step.href} target={step.external ? "_blank" : undefined}>107 {step.cta} <ArrowRight />108 </Link>109 </Button>110 ) : null}111 </li>112 ))}113 </ol>114 </CardContent>115 </Card>116 ) : null}117118 <section aria-label="This month">119 <StatGrid cols={6}>120 <Stat label="Requests" value={formatNumber(month.total)} hint="this month" />121 <Stat label="Successful" value={formatNumber(month.successful)} hint={month.failed ? `${formatNumber(month.failed)} failed` : "no failures"} />122 <Stat label="Success rate" value={month.successRate === null ? "—" : formatPercent(month.successRate)} hint="of completed requests" />123 <Stat label="Bandwidth" value={formatBytes(month.bytes)} hint="in + out" />124 <Stat label="Estimated spend" value={formatUsd(month.spendUsd)} hint="billed usage" />125 <Stat label="Avg latency" value={formatMs(month.avgLatencyMs)} hint="end to end" />126 </StatGrid>127 <StatGrid cols={5} className="mt-3">128 <Stat label="P50 latency" value={formatMs(month.p50)} />129 <Stat label="P95 latency" value={formatMs(month.p95)} />130 <Stat label="P99 latency" value={formatMs(month.p99)} />131 <Stat label="Attempts / request" value={month.avgAttempts === null ? "—" : month.avgAttempts.toFixed(2)} hint="routes tried on average" />132 <Stat label="Active sessions" value={formatNumber(activeSessions)} hint={<Link href="/dashboard/sessions" className="underline-offset-4 hover:underline">manage</Link>} />133 </StatGrid>134 </section>135136 <Card>137 <CardHeader className="pb-3">138 <CardTitle>Quota and limits</CardTitle>139 <CardDescription>140 {unlimited ? "Requests are unlimited on this private platform" : `Monthly request allowance on the ${limits.label} plan`}141 {projectLimit ? ` (project cap: ${formatNumber(projectLimit)})` : ""}. Spending limits stop new requests with <code className="font-mono text-[12px]">USAGE_LIMIT_REACHED</code>.142 </CardDescription>143 </CardHeader>144 <CardContent className="grid gap-6 lg:grid-cols-2">145 <QuotaBar label="Requests this month" used={month.total} limit={effectiveLimit} unlimited={effectiveLimit === null} usedLabel={formatNumber(month.total)} limitLabel={effectiveLimit ? formatCompact(effectiveLimit) : undefined} hint={effectiveLimit ? `${formatNumber(Math.max(0, effectiveLimit - month.total))} remaining` : "no monthly cap"} />146 <div className="grid grid-cols-2 gap-4 text-[13px]">147 <div>148 <div className="text-[12px] font-medium text-fg-subtle">Soft limit</div>149 <div className="mt-1 font-mono tabular text-[15px]">{softLimit === null ? <span className="text-fg-subtle">not set</span> : formatUsd(softLimit)}</div>150 <div className="text-[11.5px] text-fg-subtle">{softLimit === null ? "alerts only" : `${limitSource} · email alert`}</div>151 </div>152 <div>153 <div className="text-[12px] font-medium text-fg-subtle">Hard limit</div>154 <div className="mt-1 font-mono tabular text-[15px]">{hardLimit === null ? <span className="text-fg-subtle">not set</span> : formatUsd(hardLimit)}</div>155 <div className="text-[11.5px] text-fg-subtle">{hardLimit === null ? "requests never blocked" : `${limitSource} · blocks requests`}</div>156 </div>157 <div className="col-span-2 text-[12px] text-fg-subtle">158 Spend so far: <span className="font-mono tabular text-fg">{formatUsd(month.spendUsd)}</span>159 {hardLimit ? <> of {formatUsd(hardLimit)} ({formatPercent(Math.min(100, (month.spendUsd / hardLimit) * 100), 0)})</> : null} ·{" "}160 <Link href="/dashboard/projects" className="underline-offset-4 hover:underline">161 Edit limits162 </Link>163 </div>164 </div>165 </CardContent>166 </Card>167168 <div className="grid gap-4 lg:grid-cols-3">169 <ChartFrame170 className="lg:col-span-2"171 title="Requests per day"172 description="Last 30 days, UTC. Successful and failed requests stacked."173 empty={!seriesHasData}174 emptyMessage="No requests in the last 30 days."175 table={{ columns: ["Day", "Successful", "Failed"], rows: series.filter((p) => p.success + p.failed > 0).map((p) => [p.t.slice(0, 10), p.success, p.failed]) }}176 >177 <RequestsBarChart data={series} bucket="day" height={240} />178 </ChartFrame>179 <ChartFrame title="Network distribution" description="Resolved network class, last 30 days." empty={network.resolved === 0} emptyMessage="No routed requests yet.">180 <NetworkDonut data={network.shares} />181 {network.unresolved > 0 ? <p className="mt-3 text-[11.5px] text-fg-subtle">{formatNumber(network.unresolved)} request(s) failed before a network was selected and are not counted.</p> : null}182 </ChartFrame>183 </div>184185 <section>186 <SectionTitle187 right={188 <Link href="/dashboard/requests" className="text-[12.5px] text-fg-muted underline-offset-4 hover:text-fg hover:underline">189 All requests <ArrowRight className="ml-0.5 inline size-3.5" />190 </Link>191 }192 >193 Recent requests194 </SectionTitle>195 {recent.length ? (196 <Card className="overflow-hidden">197 <RequestsTable rows={recent} compact />198 </Card>199 ) : (200 <EmptyState201 compact202 icon={Play}203 title="No requests yet"204 description="Send your first request from the Playground or with an API key. Each request shows up here with its route, latency and cost."205 action={206 <>207 <Button asChild size="sm" variant="primary">208 <Link href="/dashboard/playground">Open Playground</Link>209 </Button>210 <Button asChild size="sm" variant="outline">211 <Link href="/docs/quickstart">Quickstart</Link>212 </Button>213 </>214 }215 />216 )}217 </section>218 </div>219 );220}221