TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ArrowRight, Check, KeyRound, ShieldCheck } from "lucide-react";3import { PLAN_LIMITS, isUnlimited, normalizePlan } from "@fetcha/core";4import { getWorkspace } from "@/lib/session";5import { monthlyUsage } from "@/lib/queries/account";6import { formatBytes, formatNumber, formatUsd, formatDateOnly } from "@/lib/format";7import { PageHeader } from "@/components/ui/page-header";8import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";9import { Badge } from "@/components/ui/badge";10import { Button } from "@/components/ui/button";11import { Alert } from "@/components/ui/alert";1213export const dynamic = "force-dynamic";1415export const metadata = { title: "Plan & access · Fetcha" };1617function seconds(ms: number) {18 return `${ms / 1000} s`;19}2021export default async function BillingPage() {22 const ws = await getWorkspace();23 const limits = PLAN_LIMITS[normalizePlan(ws.organization.plan)];24 const unlimited = isUnlimited(limits);25 const now = new Date();26 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));27 const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));28 const usage = await monthlyUsage(ws.organization.id);2930 const included: Array<{ label: string; detail?: string }> = [31 { label: unlimited ? "Unlimited requests" : `${formatNumber(limits.monthly_requests)} requests / month`, detail: "no monthly quota, no overage" },32 { label: `${formatNumber(limits.concurrency)} concurrent requests`, detail: "per organization, all projects together" },33 { label: `${seconds(limits.max_timeout_ms)} maximum timeout`, detail: `up to ${limits.max_retries} automatic retries per request` },34 { label: "All network classes", detail: limits.networks.map((n) => n[0]!.toUpperCase() + n.slice(1)).join(", ") },35 { label: "Managed browser rendering", detail: `${limits.browser_concurrency} concurrent renders, routed through the same networks` },36 { label: `Crawl jobs up to ${formatNumber(limits.crawl_max_pages)} pages`, detail: `${limits.crawl_concurrent_jobs} jobs in parallel, sitemap discovery and Markdown output` },37 { label: "Sticky sessions and geo targeting", detail: "country, region and city on every request" },38 { label: `${limits.retention_days}-day request logs`, detail: "full metadata, attempts and timings" },39 ];4041 return (42 <div className="grid gap-8">43 <PageHeader44 title="Plan & access"45 description="Fetcha is a private platform. There is a single plan, nothing to upgrade and nothing to pay: no invoices, no payment method, no meters. Usage below is informational."46 />4748 <div className="grid gap-6 lg:grid-cols-[1fr_360px]">49 <Card>50 <CardHeader>51 <div className="flex items-center justify-between gap-3">52 <CardTitle>Unlimited — private platform</CardTitle>53 <Badge variant="solid" dot>54 {limits.label}55 </Badge>56 </div>57 <CardDescription>Every organization on this platform gets the same limits. They are enforced by the API and shown here from the same configuration the API uses.</CardDescription>58 </CardHeader>59 <CardContent>60 <ul className="grid gap-3 text-[13.5px] sm:grid-cols-2">61 {included.map((line) => (62 <li key={line.label} className="flex items-start gap-2">63 <Check className="mt-0.5 size-4 shrink-0 text-success" aria-hidden />64 <span>65 <span className="font-medium tabular">{line.label}</span>66 {line.detail ? <span className="block text-[12.5px] text-fg-muted">{line.detail}</span> : null}67 </span>68 </li>69 ))}70 </ul>71 </CardContent>72 </Card>7374 <Card>75 <CardHeader>76 <CardTitle>Usage this month</CardTitle>77 <CardDescription>78 Across all projects of {ws.organization.name}, since {formatDateOnly(monthStart)}. Resets {formatDateOnly(nextMonth)}.79 </CardDescription>80 </CardHeader>81 <CardContent className="grid gap-3 text-[13px]">82 <div className="flex items-baseline justify-between">83 <span className="text-fg-muted">Requests</span>84 <span className="font-mono tabular">85 {formatNumber(usage.requests)} <span className="text-fg-subtle">/ ∞</span>86 </span>87 </div>88 <div className="flex items-baseline justify-between">89 <span className="text-fg-muted">Successful</span>90 <span className="font-mono tabular">{formatNumber(usage.successful)}</span>91 </div>92 <div className="flex items-baseline justify-between">93 <span className="text-fg-muted">Bandwidth</span>94 <span className="font-mono tabular">{formatBytes(usage.bytes)}</span>95 </div>96 <div className="flex items-baseline justify-between border-t border-border pt-3">97 <span className="text-fg-muted">Metered spend</span>98 <span className="font-mono tabular">{formatUsd(usage.spendUsd)}</span>99 </div>100 <p className="text-[11.5px] leading-snug text-fg-subtle">Spend is an internal cost estimate used only for your optional spending limits. Nothing is invoiced.</p>101 <Link href="/dashboard/usage" className="inline-flex items-center gap-1 text-[12.5px] text-fg-muted underline-offset-4 hover:text-fg hover:underline">102 Detailed usage <ArrowRight className="size-3.5" />103 </Link>104 </CardContent>105 </Card>106 </div>107108 <div className="grid gap-6 lg:grid-cols-2">109 <Alert110 variant="info"111 title="Spending limits"112 action={113 <Button asChild variant="outline" size="xs">114 <Link href="/dashboard/settings">Settings</Link>115 </Button>116 }117 >118 Optional guard-rails, not billing: cap the estimated monthly spend for the whole organization{ws.organization.hardLimitUsd ? ` (currently ${formatUsd(ws.organization.hardLimitUsd)} hard limit)` : ""} or per project. Soft limits email you; hard limits stop requests with USAGE_LIMIT_REACHED.119 </Alert>120 <Alert121 variant="success"122 title="Invitation-only access"123 action={124 ws.isAdmin ? (125 <Button asChild variant="outline" size="xs">126 <Link href="/admin/access">127 <KeyRound className="size-3.5" /> Manage access128 </Link>129 </Button>130 ) : (131 <Button asChild variant="outline" size="xs">132 <a href="mailto:hello@fetcha.co?subject=Fetcha%20access">133 <ShieldCheck className="size-3.5" /> Contact134 </a>135 </Button>136 )137 }138 >139 Accounts are created by invitation. To bring a teammate in, ask your Fetcha administrator to add their email to the access list; they will receive an invitation to sign up.140 </Alert>141 </div>142 </div>143 );144}145