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%
8.4 KB · 137 lines tsx
Raw Blame History
1import Link from "next/link";2import { requireAdmin } from "@/lib/session";3import { getRoutingAnalytics, parseRange, rangeLabel } from "@/lib/queries/admin";4import { formatBytes, formatMs, formatNumber, formatPercent, formatUsd } from "@/lib/format";5import { PageHeader } from "@/components/ui/page-header";6import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";7import { NetworkBadge, Panel, ProviderBadge, RangeTabs, numCell } from "@/components/admin/primitives";8import { ProviderSeriesChart } from "@/components/admin/charts";910export const dynamic = "force-dynamic";1112/** Mirrors SCORE_WEIGHTS in packages/routing/src/engine.ts (kept in sync by hand; web does not depend on @fetcha/routing). */13const WEIGHTS: Array<{ key: string; weight: number; what: string; how: string }> = [14  { key: "success", weight: 35, what: "Historical success on this domain × route", how: "Bayesian-smoothed toward the network prior (datacenter 70 %, isp 85 %, residential 93 %, mobile 95 %) with 5 pseudo-observations, so new routes are not punished." },15  { key: "cost", weight: 20, what: "Cost efficiency", how: "1 − price_per_gb / $15. Cheaper networks score higher; the ceiling is the most expensive class." },16  { key: "latency", weight: 15, what: "Observed latency", how: "300 ms → 1.0, 5 s → 0, linear. Uses route stats when present, else 500 ms (datacenter) / 1 200 ms (others)." },17  { key: "health", weight: 15, what: "Provider health", how: "Circuit-breaker health factor for the route key (recent failure window)." },18  { key: "geo", weight: 10, what: "Geography match", how: "1 when the provider supports the requested country (or no country requested), 0 otherwise." },19  { key: "session", weight: 5, what: "Session stability", how: "1 unless a sticky session is required and the provider cannot honour it (direct)." },20];2122export default async function AdminRoutingPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {23  await requireAdmin();24  const sp = await searchParams;25  const range = parseRange(sp.range);26  const a = await getRoutingAnalytics(range);27  const totals = a.matrix.reduce((acc, r) => ({ requests: acc.requests + r.requests, successes: acc.successes + r.successes, cost: acc.cost + r.cost }), { requests: 0, successes: 0, cost: 0 });2829  return (30    <>31      <PageHeader eyebrow="Intelligence" title="Routing" description={`${rangeLabel(range)} · hourly routing_metrics aggregated by provider × network.`} actions={<RangeTabs current={range} basePath="/admin/routing" />} />3233      <Panel title="Provider × network" description={`${formatNumber(totals.requests)} routed attempts · ${formatPercent(totals.requests ? (totals.successes / totals.requests) * 100 : null)} success · ${formatUsd(totals.cost, true)} upstream`} flush>34        <Table>35          <TableHeader>36            <TableRow>37              <TableHead>Provider</TableHead>38              <TableHead>Network</TableHead>39              <TableHead className="text-right">Requests</TableHead>40              <TableHead className="text-right">Successes</TableHead>41              <TableHead className="text-right">Success</TableHead>42              <TableHead className="text-right">Blocked</TableHead>43              <TableHead className="text-right">Errors</TableHead>44              <TableHead className="text-right">Avg latency</TableHead>45              <TableHead className="text-right">GB</TableHead>46              <TableHead className="text-right">Cost</TableHead>47              <TableHead className="text-right">Cost / success</TableHead>48            </TableRow>49          </TableHeader>50          <TableBody>51            {a.matrix.length === 0 ? (52              <TableEmpty colSpan={11}>No routing metrics in this range. Metrics are written hourly by the API as attempts complete.</TableEmpty>53            ) : (54              a.matrix.map((r) => (55                <TableRow key={`${r.provider}:${r.network}`}>56                  <TableCell>57                    <ProviderBadge id={r.provider} />58                  </TableCell>59                  <TableCell>60                    <NetworkBadge network={r.network} />61                  </TableCell>62                  <TableCell className={numCell}>{formatNumber(r.requests)}</TableCell>63                  <TableCell className={numCell}>{formatNumber(r.successes)}</TableCell>64                  <TableCell className={numCell}>{formatPercent(r.successRate)}</TableCell>65                  <TableCell className={numCell}>{formatNumber(r.blocked)}</TableCell>66                  <TableCell className={numCell}>{formatNumber(r.errors)}</TableCell>67                  <TableCell className={numCell}>{formatMs(r.avgLatency)}</TableCell>68                  <TableCell className={numCell}>{formatBytes(r.bytes)}</TableCell>69                  <TableCell className={numCell}>{formatUsd(r.cost, true)}</TableCell>70                  <TableCell className={`${numCell} font-semibold`}>{formatUsd(r.costPerSuccess, true)}</TableCell>71                </TableRow>72              ))73            )}74          </TableBody>75        </Table>76      </Panel>7778      <div className="mt-6 grid gap-6 xl:grid-cols-2">79        <Panel title="Requests over time per provider" bodyClassName="p-2 pt-3">80          <ProviderSeriesChart data={a.series} providers={a.providers} unit={a.unit as "hour" | "day"} metric="requests" />81        </Panel>82        <Panel title="Upstream cost over time per provider" bodyClassName="p-2 pt-3">83          <ProviderSeriesChart data={a.series} providers={a.providers} unit={a.unit as "hour" | "day"} metric="cost" />84        </Panel>85      </div>8687      <Panel title="How the routing score works" description="Each candidate route (provider × network) gets a weighted score in [0, 1]. Weights mirror packages/routing/src/engine.ts." flush className="mt-6">88        <Table>89          <TableHeader>90            <TableRow>91              <TableHead>Factor</TableHead>92              <TableHead className="text-right">Weight</TableHead>93              <TableHead>Measures</TableHead>94              <TableHead>Computation</TableHead>95            </TableRow>96          </TableHeader>97          <TableBody>98            {WEIGHTS.map((w) => (99              <TableRow key={w.key}>100                <TableCell className="font-mono text-[12.5px]">{w.key}</TableCell>101                <TableCell className={numCell}>{w.weight} %</TableCell>102                <TableCell>{w.what}</TableCell>103                <TableCell className="max-w-[520px] text-[12.5px] text-fg-muted">{w.how}</TableCell>104              </TableRow>105            ))}106          </TableBody>107        </Table>108        <div className="grid gap-4 border-t border-border p-4 text-[13px] leading-relaxed text-fg-muted lg:grid-cols-3">109          <div>110            <h3 className="mb-1 font-semibold text-fg">Auto escalation</h3>111            <p>112              For <code className="font-mono text-[12px]">network: &quot;auto&quot;</code> candidates are grouped by class in the order datacenter → isp → residential → mobile (limited to the plan&apos;s networks), then sorted by score inside each class. A class is skipped113              entirely for a domain once its route stats show ≥ 5 attempts with &gt; 60 % blocks (residential is never skipped). Providers are de-duplicated so retries alternate upstreams before repeating one.114            </p>115          </div>116          <div>117            <h3 className="mb-1 font-semibold text-fg">Domain profiles</h3>118            <p>119              <Link href="/admin/domains" className="text-accent hover:underline">120                domain_profiles.route_stats121              </Link>{" "}122              feed the success and latency factors per route key. The admin <em>policy</em> can pin an explicit route order (wins over score, still bounded by circuit state) or force a network for auto requests. Explicit customer network choices are never overridden.123            </p>124          </div>125          <div>126            <h3 className="mb-1 font-semibold text-fg">Circuit breakers</h3>127            <p>128              Each route key has a breaker. An open circuit removes the route from the candidate list entirely; a half-open one lets a trickle through. The breaker&apos;s recent failure ratio is also the 15 % health factor, so a degrading route loses score before it is cut off.129              Reset circuits from <Link href="/admin/providers" className="text-accent hover:underline">Providers</Link>.130            </p>131          </div>132        </div>133      </Panel>134    </>135  );136}137