TypeScript 97.5%
SQL 1.4%
Python 0.8%
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: "auto"</code> candidates are grouped by class in the order datacenter → isp → residential → mobile (limited to the plan'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 > 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'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