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%
5.7 KB · 110 lines tsx
Raw Blame History
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