TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { ArrowDown, ArrowUp, Eraser, Plus, Save, X } from "lucide-react";5import { resetDomainStats, updateDomainPolicy, type AdminActionResult } from "@/actions/admin";6import { Button } from "@/components/ui/button";7import { NativeSelect } from "@/components/ui/input";8import { Field, Hint, Label } from "@/components/ui/label";9import { ActionButton, ResultMessage } from "./action-button";1011const NETWORKS = ["datacenter", "residential", "isp", "mobile"] as const;1213export function DomainPolicyEditor({ domain, policy, availableKeys }: { domain: string; policy: { order?: string[]; force_network?: string } | null; availableKeys: string[] }) {14 const router = useRouter();15 const [pending, start] = React.useTransition();16 const [force, setForce] = React.useState(policy?.force_network ?? "");17 const [order, setOrder] = React.useState<string[]>(policy?.order ?? []);18 const [pick, setPick] = React.useState("");19 const [result, setResult] = React.useState<AdminActionResult | null>(null);20 const candidates = availableKeys.filter((k) => !order.includes(k));2122 const move = (i: number, d: -1 | 1) =>23 setOrder((cur) => {24 const next = [...cur];25 const j = i + d;26 if (j < 0 || j >= next.length) return cur;27 [next[i], next[j]] = [next[j]!, next[i]!];28 return next;29 });3031 return (32 <form33 className="grid gap-4"34 onSubmit={(e) => {35 e.preventDefault();36 start(async () => {37 const r = await updateDomainPolicy(domain, { force_network: force || null, order });38 setResult(r);39 if (r.ok) router.refresh();40 });41 }}42 >43 <Field>44 <Label htmlFor="force-network">Force network</Label>45 <NativeSelect id="force-network" value={force} onChange={(e) => setForce(e.target.value)} className="w-[220px]">46 <option value="">None (auto escalation)</option>47 {NETWORKS.map((n) => (48 <option key={n} value={n}>49 {n}50 </option>51 ))}52 </NativeSelect>53 <Hint>Applies only when the customer asks for `network: "auto"`. Explicit network requests are never overridden.</Hint>54 </Field>5556 <div className="grid gap-1.5">57 <Label>Pinned route order</Label>58 <Hint>Ordered list of `provider:network` keys. Pinned routes are tried first (in this order); unpinned candidates follow by score.</Hint>59 {order.length ? (60 <ol className="divide-y divide-border rounded-md border border-border">61 {order.map((k, i) => (62 <li key={k} className="flex items-center gap-2 px-2.5 py-1.5 text-[13px]">63 <span className="w-5 text-right tabular font-mono text-fg-subtle">{i + 1}.</span>64 <span className="font-mono">{k}</span>65 <span className="ml-auto flex items-center gap-0.5">66 <Button type="button" variant="ghost" size="icon-sm" onClick={() => move(i, -1)} disabled={i === 0} aria-label="Move up">67 <ArrowUp className="size-3.5" />68 </Button>69 <Button type="button" variant="ghost" size="icon-sm" onClick={() => move(i, 1)} disabled={i === order.length - 1} aria-label="Move down">70 <ArrowDown className="size-3.5" />71 </Button>72 <Button type="button" variant="ghost" size="icon-sm" onClick={() => setOrder((cur) => cur.filter((x) => x !== k))} aria-label={`Remove ${k}`}>73 <X className="size-3.5" />74 </Button>75 </span>76 </li>77 ))}78 </ol>79 ) : (80 <p className="rounded-md border border-dashed border-border px-3 py-2 text-[12.5px] text-fg-muted">No pinned routes — the engine orders candidates by score.</p>81 )}82 <div className="flex items-center gap-2">83 <NativeSelect value={pick} onChange={(e) => setPick(e.target.value)} className="w-[260px]" aria-label="Route to pin">84 <option value="">Add a route…</option>85 {candidates.map((k) => (86 <option key={k} value={k}>87 {k}88 </option>89 ))}90 </NativeSelect>91 <Button92 type="button"93 variant="outline"94 size="sm"95 disabled={!pick}96 onClick={() => {97 if (pick) setOrder((cur) => [...cur, pick]);98 setPick("");99 }}100 >101 <Plus className="size-3.5" /> Pin102 </Button>103 </div>104 </div>105106 <div className="flex flex-wrap items-center gap-3">107 <Button type="submit" size="sm" loading={pending}>108 <Save className="size-3.5" /> Save policy109 </Button>110 <Button111 type="button"112 variant="ghost"113 size="sm"114 onClick={() => {115 setForce("");116 setOrder([]);117 }}118 >119 Clear120 </Button>121 <ResultMessage result={result} />122 </div>123 </form>124 );125}126127export function ResetDomainStatsButton({ domain }: { domain: string }) {128 return (129 <ActionButton130 variant="outline"131 size="sm"132 action={() => resetDomainStats(domain)}133 confirm={{ title: `Reset learned stats for ${domain}?`, description: "Route statistics, block/captcha counters and the preferred route are cleared. The policy is kept. The router falls back to network priors until new data accumulates.", confirmLabel: "Reset stats" }}134 >135 <Eraser className="size-3.5" /> Reset stats136 </ActionButton>137 );138}139