TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { RefreshCw, RotateCcw, Save } from "lucide-react";5import { resetCircuit, runProviderProbe, updateProviderConfig, type AdminActionResult, type ProviderConfigInput } from "@/actions/admin";6import { Button } from "@/components/ui/button";7import { Input, Textarea } from "@/components/ui/input";8import { Field, Hint, Label } from "@/components/ui/label";9import { Switch } from "@/components/ui/switch";10import { Badge, StatusBadge } from "@/components/ui/badge";11import { ActionButton, ResultMessage } from "./action-button";1213const ALL_NETWORKS = ["datacenter", "residential", "isp", "mobile"] as const;1415export interface ProviderConfigFormProps {16 id: string;17 label: string;18 enabled: boolean;19 networks: string[];20 pricePerGbUsd: Record<string, number>;21 weight: number;22 maxConcurrency: number;23 notes: string | null;24 /** Prices reported live by the API (fallback when no config row exists). */25 livePrices: Record<string, number>;26 liveNetworks: string[];27 hasConfigRow: boolean;28}2930export function ProviderConfigForm(p: ProviderConfigFormProps) {31 const router = useRouter();32 const [pending, start] = React.useTransition();33 const [result, setResult] = React.useState<AdminActionResult | null>(null);34 const [enabled, setEnabled] = React.useState(p.enabled);35 const [networks, setNetworks] = React.useState<string[]>(p.networks.length ? p.networks : p.liveNetworks);36 const [prices, setPrices] = React.useState<Record<string, string>>(() => {37 const out: Record<string, string> = {};38 for (const n of ALL_NETWORKS) {39 const v = p.pricePerGbUsd[n] ?? p.livePrices[n];40 if (v !== undefined) out[n] = String(v);41 }42 return out;43 });44 const [weight, setWeight] = React.useState(String(p.weight));45 const [maxConcurrency, setMaxConcurrency] = React.useState(String(p.maxConcurrency));46 const [notes, setNotes] = React.useState(p.notes ?? "");4748 const submit = (e: React.FormEvent) => {49 e.preventDefault();50 start(async () => {51 const pricePerGbUsd: Record<string, number> = {};52 for (const [k, v] of Object.entries(prices)) if (v.trim() !== "" && networks.includes(k)) pricePerGbUsd[k] = Number(v);53 const input: ProviderConfigInput = {54 label: p.label,55 enabled,56 networks: networks as ProviderConfigInput["networks"],57 pricePerGbUsd,58 weight: Number(weight),59 maxConcurrency: Number(maxConcurrency),60 notes: notes.trim() || null,61 };62 const r = await updateProviderConfig(p.id, input);63 setResult(r);64 if (r.ok) router.refresh();65 });66 };6768 return (69 <form onSubmit={submit} className="grid gap-4">70 <div className="flex items-center gap-3">71 <Switch id={`en-${p.id}`} checked={enabled} onCheckedChange={setEnabled} aria-label="Enabled" />72 <Label htmlFor={`en-${p.id}`}>Enabled in routing</Label>73 {!p.hasConfigRow ? <Badge variant="outline">no config row yet — defaults shown</Badge> : null}74 </div>75 <div>76 <div className="mb-1.5 text-[13px] font-medium">Networks and price per GB (USD)</div>77 <div className="grid gap-2 sm:grid-cols-2">78 {ALL_NETWORKS.map((n) => {79 const on = networks.includes(n);80 return (81 <div key={n} className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5">82 <input83 type="checkbox"84 id={`${p.id}-${n}`}85 className="size-3.5 accent-[var(--accent)]"86 checked={on}87 onChange={(e) => setNetworks((cur) => (e.target.checked ? [...cur, n] : cur.filter((x) => x !== n)))}88 />89 <label htmlFor={`${p.id}-${n}`} className="w-24 text-[13px]">90 {n}91 </label>92 <span className="text-[12px] text-fg-subtle">$</span>93 <Input94 aria-label={`${n} price per GB`}95 type="number"96 min={0}97 step="0.01"98 inputMode="decimal"99 disabled={!on}100 value={prices[n] ?? ""}101 onChange={(e) => setPrices((cur) => ({ ...cur, [n]: e.target.value }))}102 className="h-7 w-24 font-mono text-[12.5px]"103 placeholder="—"104 />105 <span className="text-[12px] text-fg-subtle">/GB</span>106 </div>107 );108 })}109 </div>110 </div>111 <div className="grid gap-3 sm:grid-cols-2">112 <Field>113 <Label htmlFor={`w-${p.id}`}>Weight</Label>114 <Input id={`w-${p.id}`} type="number" min={0} max={10} step="0.1" value={weight} onChange={(e) => setWeight(e.target.value)} className="font-mono" />115 <Hint>Relative preference multiplier (1 = neutral).</Hint>116 </Field>117 <Field>118 <Label htmlFor={`mc-${p.id}`}>Max concurrency</Label>119 <Input id={`mc-${p.id}`} type="number" min={1} step={1} value={maxConcurrency} onChange={(e) => setMaxConcurrency(e.target.value)} className="font-mono" />120 <Hint>Upstream connection ceiling for this provider.</Hint>121 </Field>122 </div>123 <Field>124 <Label htmlFor={`notes-${p.id}`}>Notes</Label>125 <Textarea id={`notes-${p.id}`} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="Contract terms, account manager, quotas…" className="min-h-[64px]" />126 </Field>127 <div className="flex items-center gap-3">128 <Button type="submit" size="sm" loading={pending}>129 <Save className="size-3.5" /> Save & reload providers130 </Button>131 <ResultMessage result={result} />132 </div>133 </form>134 );135}136137export function CircuitList({ circuits }: { circuits: Array<{ key: string; state: string; failures: number; successes: number; openedAt: number | null }> }) {138 if (!circuits.length) return <p className="text-[12.5px] text-fg-muted">No circuit entries yet — circuits are created on the first attempt per route.</p>;139 return (140 <ul className="divide-y divide-border">141 {circuits.map((c) => (142 <li key={c.key} className="flex flex-wrap items-center gap-3 py-2 text-[12.5px]">143 <span className="font-mono">{c.key}</span>144 <StatusBadge status={c.state === "closed" ? "healthy" : c.state === "open" ? "down" : c.state} />145 <span className="tabular font-mono text-fg-muted">146 {c.successes} ok / {c.failures} fail147 </span>148 {c.openedAt ? <span className="text-fg-subtle">opened {new Date(c.openedAt).toLocaleTimeString()}</span> : null}149 <span className="ml-auto">150 <ActionButton variant="outline" size="xs" action={() => resetCircuit(c.key)}>151 <RotateCcw className="size-3" /> Reset circuit152 </ActionButton>153 </span>154 </li>155 ))}156 </ul>157 );158}159160export function ProbeButton() {161 return (162 <ActionButton variant="primary" size="sm" action={() => runProviderProbe()}>163 <RefreshCw className="size-3.5" /> Run health probe now164 </ActionButton>165 );166}167168export function ResetAllCircuitsButton() {169 return (170 <ActionButton variant="outline" size="sm" action={() => resetCircuit()} confirm={{ title: "Reset every circuit breaker?", description: "All routes become eligible again immediately, including ones that were failing.", confirmLabel: "Reset all" }}>171 <RotateCcw className="size-3.5" /> Reset all circuits172 </ActionButton>173 );174}175