SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
7.1 KB · 145 lines tsx
Raw Blame History
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