TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ArrowDown, ArrowUp, Globe } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { listDomains, pageNum, str, DOMAIN_SORTS } from "@/lib/queries/admin";5import { formatMs, formatNumber, formatPercent } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Badge } from "@/components/ui/badge";8import { EmptyState } from "@/components/ui/empty-state";9import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";10import { DateCell, NetworkBadge, Pagination, Panel, ProviderBadge, SearchForm, numCell } from "@/components/admin/primitives";1112export const dynamic = "force-dynamic";1314const COLS: Array<{ key: (typeof DOMAIN_SORTS)[number]; label: string; right?: boolean }> = [15 { key: "domain", label: "Domain" },16 { key: "requests", label: "Requests", right: true },17 { key: "successRate", label: "Success", right: true },18 { key: "blockRate", label: "Blocked", right: true },19 { key: "captchaRate", label: "Captcha", right: true },20 { key: "avgLatencyMs", label: "Avg latency", right: true },21];2223export default async function AdminDomainsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {24 await requireAdmin();25 const sp = await searchParams;26 const q = str(sp.q);27 const sort = str(sp.sort);28 const dir = str(sp.dir);29 const page = pageNum(sp.page);30 const data = await listDomains({ q: q || undefined, sort: sort || undefined, dir: dir || undefined, page });3132 const sortHref = (key: string) => {33 const nextDir = data.sort === key && data.dir === "desc" ? "asc" : "desc";34 const p = new URLSearchParams();35 if (q) p.set("q", q);36 p.set("sort", key);37 p.set("dir", nextDir);38 return `/admin/domains?${p.toString()}`;39 };4041 return (42 <>43 <PageHeader eyebrow="Intelligence" title="Domains" description={`${formatNumber(data.total)} learned domain profiles. Click a column to sort.`} actions={<SearchForm action="/admin/domains" q={q} placeholder="domain contains…" hidden={{ sort: data.sort, dir: data.dir }} />} />44 <Panel flush>45 {data.total === 0 ? (46 <div className="p-4">47 <EmptyState icon={Globe} title={q ? "No domains match" : "No domain profiles yet"} description="Profiles are created automatically after the first request to a domain." compact />48 </div>49 ) : (50 <>51 <Table>52 <TableHeader>53 <TableRow>54 {COLS.map((c) => (55 <TableHead key={c.key} className={c.right ? "text-right" : undefined} aria-sort={data.sort === c.key ? (data.dir === "asc" ? "ascending" : "descending") : "none"}>56 <Link href={sortHref(c.key)} className="inline-flex items-center gap-1 hover:text-fg">57 {c.label}58 {data.sort === c.key ? data.dir === "asc" ? <ArrowUp className="size-3" /> : <ArrowDown className="size-3" /> : null}59 </Link>60 </TableHead>61 ))}62 <TableHead>Preferred route</TableHead>63 <TableHead>Policy</TableHead>64 <TableHead className="text-right" aria-sort={data.sort === "lastSeenAt" ? (data.dir === "asc" ? "ascending" : "descending") : "none"}>65 <Link href={sortHref("lastSeenAt")} className="inline-flex items-center gap-1 hover:text-fg">66 Last seen67 {data.sort === "lastSeenAt" ? data.dir === "asc" ? <ArrowUp className="size-3" /> : <ArrowDown className="size-3" /> : null}68 </Link>69 </TableHead>70 </TableRow>71 </TableHeader>72 <TableBody>73 {data.rows.map((d) => (74 <TableRow key={d.domain}>75 <TableCell>76 <Link href={`/admin/domains/${encodeURIComponent(d.domain)}`} className="font-medium hover:underline">77 {d.domain}78 </Link>79 </TableCell>80 <TableCell className={numCell}>{formatNumber(d.requests)}</TableCell>81 <TableCell className={numCell}>{formatPercent(d.successRate)}</TableCell>82 <TableCell className={numCell}>{formatPercent(d.blockRate)}</TableCell>83 <TableCell className={numCell}>{formatPercent(d.captchaRate)}</TableCell>84 <TableCell className={numCell}>{formatMs(d.avgLatencyMs)}</TableCell>85 <TableCell>86 <span className="inline-flex items-center gap-1.5">87 <ProviderBadge id={d.preferredProvider} />88 <NetworkBadge network={d.preferredNetwork} />89 </span>90 </TableCell>91 <TableCell>92 {d.policy?.force_network ? <Badge variant="warning">force {d.policy.force_network}</Badge> : null}93 {d.policy?.order?.length ? <Badge variant="info" className="ml-1">pinned {d.policy.order.length}</Badge> : null}94 {!d.policy?.force_network && !d.policy?.order?.length ? <span className="text-fg-subtle">—</span> : null}95 </TableCell>96 <TableCell className="text-right">97 <DateCell value={d.lastSeenAt} />98 </TableCell>99 </TableRow>100 ))}101 </TableBody>102 </Table>103 <Pagination page={data.page} pages={data.pages} total={data.total} pageSize={data.pageSize} basePath="/admin/domains" params={{ q, sort: data.sort, dir: data.dir }} />104 </>105 )}106 </Panel>107 </>108 );109}110