@wopr-network/platform-ui-core 1.26.0 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -408,8 +408,8 @@ describe("Payment page", () => {
408
408
  const amountCells = await screen.findAllByText("$29.00");
409
409
  // inv-001 has no downloadUrl and no hostedUrl — the row should render no <a> element at all
410
410
  // The 3rd amount cell belongs to inv-001 (oldest invoice, no URLs)
411
- const inv001Row = amountCells[2].closest("tr")!;
412
- expect(within(inv001Row).queryByRole("link")).toBeNull();
411
+ const inv001Row = amountCells[2].closest("tr");
412
+ expect(inv001Row ? within(inv001Row).queryByRole("link") : null).toBeNull();
413
413
  });
414
414
 
415
415
  it("renders BYOK messaging", async () => {
@@ -32,7 +32,6 @@ export default function OnboardingPage() {
32
32
  const [botName, setBotName] = useState("");
33
33
  const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
34
34
 
35
- // biome-ignore lint/correctness/useExhaustiveDependencies: router.push is stable; using [router] causes infinite re-renders
36
35
  useEffect(() => {
37
36
  if (isOnboardingComplete()) {
38
37
  router.push(homePath());
@@ -253,6 +253,7 @@ export default function ProfilePage() {
253
253
  disabled={uploading}
254
254
  >
255
255
  {profile.avatarUrl ? (
256
+ // biome-ignore lint/performance/noImgElement: external avatar URL — domain not configured for next/image
256
257
  <img
257
258
  src={profile.avatarUrl}
258
259
  alt={profile.name}
@@ -0,0 +1,5 @@
1
+ import { PoolConfigDashboard } from "@/components/admin/pool-config-dashboard";
2
+
3
+ export default function PoolConfigPage() {
4
+ return <PoolConfigDashboard />;
5
+ }
@@ -23,6 +23,7 @@ const adminNavItems = [
23
23
  { label: "Migrations", href: "/admin/migrations" },
24
24
  { label: "GPU", href: "/admin/gpu" },
25
25
  { label: "Fleet Updates", href: "/admin/fleet-updates" },
26
+ { label: "Pool", href: "/admin/pool-config" },
26
27
  { label: "Incidents", href: "/admin/incidents" },
27
28
  ];
28
29
 
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { getPoolConfig, type PoolConfig, setPoolSize } from "@/lib/admin-pool-api";
9
+
10
+ export function PoolConfigDashboard() {
11
+ const [config, setConfig] = useState<PoolConfig | null>(null);
12
+ const [loading, setLoading] = useState(true);
13
+ const [saving, setSaving] = useState(false);
14
+ const [sizeInput, setSizeInput] = useState("");
15
+
16
+ const load = useCallback(async () => {
17
+ try {
18
+ const data = await getPoolConfig();
19
+ setConfig(data);
20
+ setSizeInput(String(data.poolSize));
21
+ } catch {
22
+ toast.error("Failed to load pool config");
23
+ } finally {
24
+ setLoading(false);
25
+ }
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ load();
30
+ }, [load]);
31
+
32
+ const handleSave = async () => {
33
+ const size = Number.parseInt(sizeInput, 10);
34
+ if (Number.isNaN(size) || size < 0 || size > 50) {
35
+ toast.error("Pool size must be between 0 and 50");
36
+ return;
37
+ }
38
+ setSaving(true);
39
+ try {
40
+ const result = await setPoolSize(size);
41
+ setConfig((prev) => (prev ? { ...prev, poolSize: result.poolSize } : prev));
42
+ toast.success(`Pool size updated to ${result.poolSize}`);
43
+ } catch {
44
+ toast.error("Failed to update pool size");
45
+ } finally {
46
+ setSaving(false);
47
+ }
48
+ };
49
+
50
+ if (loading) {
51
+ return (
52
+ <div className="space-y-6 p-6">
53
+ <div className="h-8 w-48 animate-pulse rounded bg-muted" />
54
+ <div className="grid grid-cols-3 gap-4">
55
+ {[1, 2, 3].map((i) => (
56
+ <div key={i} className="h-24 animate-pulse rounded-lg bg-muted" />
57
+ ))}
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ if (!config) {
64
+ return <div className="p-6 text-muted-foreground">Failed to load pool configuration.</div>;
65
+ }
66
+
67
+ if (!config.enabled) {
68
+ return (
69
+ <div className="p-6">
70
+ <h2 className="text-lg font-semibold mb-2">Hot Pool</h2>
71
+ <p className="text-muted-foreground">
72
+ Hot pool is not enabled for this product. Enable the{" "}
73
+ <code className="text-xs bg-muted px-1 py-0.5 rounded">hotPool</code> feature flag in the
74
+ boot config to use pre-provisioned instances.
75
+ </p>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <div className="space-y-6 p-6">
82
+ <div>
83
+ <h2 className="text-lg font-semibold">Hot Pool</h2>
84
+ <p className="text-sm text-muted-foreground">
85
+ Pre-provisioned warm containers for instant instance creation.
86
+ </p>
87
+ </div>
88
+
89
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
90
+ <div className="rounded-lg border border-border bg-card p-4">
91
+ <div className="text-sm text-muted-foreground">Target Size</div>
92
+ <div className="mt-1 text-2xl font-bold text-terminal">{config.poolSize}</div>
93
+ </div>
94
+
95
+ <div className="rounded-lg border border-border bg-card p-4">
96
+ <div className="text-sm text-muted-foreground">Warm Containers</div>
97
+ <div className="mt-1 text-2xl font-bold text-green-400">{config.warmCount}</div>
98
+ </div>
99
+
100
+ <div className="rounded-lg border border-border bg-card p-4">
101
+ <div className="text-sm text-muted-foreground">Status</div>
102
+ <div className="mt-1 text-2xl font-bold">
103
+ {config.warmCount >= config.poolSize ? (
104
+ <span className="text-green-400">Full</span>
105
+ ) : config.warmCount > 0 ? (
106
+ <span className="text-amber-400">Filling</span>
107
+ ) : (
108
+ <span className="text-red-400">Empty</span>
109
+ )}
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <div className="rounded-lg border border-border bg-card p-4">
115
+ <div className="text-sm font-medium mb-3">Adjust Pool Size</div>
116
+ <div className="flex items-center gap-3">
117
+ <Input
118
+ type="number"
119
+ min={0}
120
+ max={50}
121
+ value={sizeInput}
122
+ onChange={(e) => setSizeInput(e.target.value)}
123
+ className="w-24"
124
+ />
125
+ <Button
126
+ onClick={handleSave}
127
+ disabled={saving || sizeInput === String(config.poolSize)}
128
+ size="sm"
129
+ >
130
+ {saving ? "Saving..." : "Update"}
131
+ </Button>
132
+ <span className="text-xs text-muted-foreground">
133
+ The pool manager will replenish to this target within 60 seconds.
134
+ </span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
@@ -3,102 +3,90 @@
3
3
  import { Check } from "lucide-react";
4
4
 
5
5
  interface ConfirmationTrackerProps {
6
- confirmations: number;
7
- confirmationsRequired: number;
8
- displayAmount: string;
9
- credited: boolean;
10
- txHash?: string;
6
+ confirmations: number;
7
+ confirmationsRequired: number;
8
+ displayAmount: string;
9
+ credited: boolean;
10
+ txHash?: string;
11
11
  }
12
12
 
13
13
  export function ConfirmationTracker({
14
- confirmations,
15
- confirmationsRequired,
16
- displayAmount,
17
- credited,
18
- txHash,
14
+ confirmations,
15
+ confirmationsRequired,
16
+ displayAmount,
17
+ credited,
18
+ txHash,
19
19
  }: ConfirmationTrackerProps) {
20
- const pct =
21
- confirmationsRequired > 0
22
- ? Math.min(100, Math.round((confirmations / confirmationsRequired) * 100))
23
- : 0;
24
- const detected = confirmations > 0 || credited;
20
+ const pct =
21
+ confirmationsRequired > 0
22
+ ? Math.min(100, Math.round((confirmations / confirmationsRequired) * 100))
23
+ : 0;
24
+ const detected = confirmations > 0 || credited;
25
25
 
26
- return (
27
- <div className="space-y-4 text-center">
28
- <p className="text-sm text-muted-foreground">
29
- {credited ? "Payment complete!" : "Payment received!"}
30
- </p>
31
- <p className="text-xl font-semibold">{displayAmount}</p>
26
+ return (
27
+ <div className="space-y-4 text-center">
28
+ <p className="text-sm text-muted-foreground">
29
+ {credited ? "Payment complete!" : "Payment received!"}
30
+ </p>
31
+ <p className="text-xl font-semibold">{displayAmount}</p>
32
32
 
33
- <div className="rounded-lg border border-border p-3 space-y-2">
34
- <div className="flex justify-between text-xs">
35
- <span className="text-muted-foreground">Confirmations</span>
36
- <span>
37
- {confirmations} / {confirmationsRequired}
38
- </span>
39
- </div>
40
- <div
41
- className="h-1.5 rounded-full bg-muted overflow-hidden"
42
- role="progressbar"
43
- aria-valuenow={pct}
44
- aria-valuemin={0}
45
- aria-valuemax={100}
46
- >
47
- <div
48
- className="h-full rounded-full bg-primary transition-all duration-500"
49
- style={{ width: `${pct}%` }}
50
- />
51
- </div>
52
- </div>
33
+ <div className="rounded-lg border border-border p-3 space-y-2">
34
+ <div className="flex justify-between text-xs">
35
+ <span className="text-muted-foreground">Confirmations</span>
36
+ <span>
37
+ {confirmations} / {confirmationsRequired}
38
+ </span>
39
+ </div>
40
+ <div
41
+ className="h-1.5 rounded-full bg-muted overflow-hidden"
42
+ role="progressbar"
43
+ aria-valuenow={pct}
44
+ aria-valuemin={0}
45
+ aria-valuemax={100}
46
+ >
47
+ <div
48
+ className="h-full rounded-full bg-primary transition-all duration-500"
49
+ style={{ width: `${pct}%` }}
50
+ />
51
+ </div>
52
+ </div>
53
53
 
54
- <div className="space-y-2 text-left">
55
- <div className="flex items-center gap-2">
56
- <div
57
- className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${detected ? "bg-green-500 text-white" : "bg-muted"}`}
58
- >
59
- {detected && <Check className="h-2.5 w-2.5" />}
60
- </div>
61
- <span
62
- className={`text-xs ${detected ? "text-muted-foreground" : "text-muted-foreground/50"}`}
63
- >
64
- Payment detected
65
- </span>
66
- </div>
67
- <div className="flex items-center gap-2">
68
- <div
69
- className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : detected ? "bg-primary text-white animate-pulse" : "bg-muted"}`}
70
- >
71
- {credited ? (
72
- <Check className="h-2.5 w-2.5" />
73
- ) : detected ? (
74
- <span>&middot;</span>
75
- ) : null}
76
- </div>
77
- <span
78
- className={`text-xs ${detected ? "text-foreground" : "text-muted-foreground/50"}`}
79
- >
80
- {credited ? "Confirmed" : "Confirming on chain"}
81
- </span>
82
- </div>
83
- <div className="flex items-center gap-2">
84
- <div
85
- className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : "bg-muted"}`}
86
- >
87
- {credited && <Check className="h-2.5 w-2.5" />}
88
- </div>
89
- <span
90
- className={`text-xs ${credited ? "text-foreground" : "text-muted-foreground/50"}`}
91
- >
92
- Credits applied
93
- </span>
94
- </div>
95
- </div>
54
+ <div className="space-y-2 text-left">
55
+ <div className="flex items-center gap-2">
56
+ <div
57
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${detected ? "bg-green-500 text-white" : "bg-muted"}`}
58
+ >
59
+ {detected && <Check className="h-2.5 w-2.5" />}
60
+ </div>
61
+ <span
62
+ className={`text-xs ${detected ? "text-muted-foreground" : "text-muted-foreground/50"}`}
63
+ >
64
+ Payment detected
65
+ </span>
66
+ </div>
67
+ <div className="flex items-center gap-2">
68
+ <div
69
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : detected ? "bg-primary text-white animate-pulse" : "bg-muted"}`}
70
+ >
71
+ {credited ? <Check className="h-2.5 w-2.5" /> : detected ? <span>&middot;</span> : null}
72
+ </div>
73
+ <span className={`text-xs ${detected ? "text-foreground" : "text-muted-foreground/50"}`}>
74
+ {credited ? "Confirmed" : "Confirming on chain"}
75
+ </span>
76
+ </div>
77
+ <div className="flex items-center gap-2">
78
+ <div
79
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : "bg-muted"}`}
80
+ >
81
+ {credited && <Check className="h-2.5 w-2.5" />}
82
+ </div>
83
+ <span className={`text-xs ${credited ? "text-foreground" : "text-muted-foreground/50"}`}>
84
+ Credits applied
85
+ </span>
86
+ </div>
87
+ </div>
96
88
 
97
- {txHash && (
98
- <p className="text-xs text-muted-foreground font-mono truncate">
99
- tx: {txHash}
100
- </p>
101
- )}
102
- </div>
103
- );
89
+ {txHash && <p className="text-xs text-muted-foreground font-mono truncate">tx: {txHash}</p>}
90
+ </div>
91
+ );
104
92
  }
@@ -32,7 +32,9 @@ export function CryptoCheckout() {
32
32
  useEffect(() => {
33
33
  getSupportedPaymentMethods()
34
34
  .then(setMethods)
35
- .catch(() => {});
35
+ .catch(() => {
36
+ // silently fall back to empty methods list
37
+ });
36
38
  }, []);
37
39
 
38
40
  useEffect(() => {
@@ -45,8 +45,16 @@ export function DepositView({ checkout, status, onBack }: DepositViewProps) {
45
45
  <p className="text-xs text-muted-foreground">
46
46
  on {checkout.chain} &middot; ${checkout.amountUsd.toFixed(2)} USD
47
47
  </p>
48
- <div className="mx-auto w-fit rounded-lg border border-border bg-background p-3" aria-hidden="true">
49
- <QRCodeSVG value={checkout.depositAddress} size={140} bgColor="hsl(var(--background))" fgColor="hsl(var(--foreground))" />
48
+ <div
49
+ className="mx-auto w-fit rounded-lg border border-border bg-background p-3"
50
+ aria-hidden="true"
51
+ >
52
+ <QRCodeSVG
53
+ value={checkout.depositAddress}
54
+ size={140}
55
+ bgColor="hsl(var(--background))"
56
+ fgColor="hsl(var(--foreground))"
57
+ />
50
58
  </div>
51
59
  <div className="flex items-center gap-2 rounded-lg border border-border bg-muted/50 px-3 py-2">
52
60
  <code className="flex-1 truncate text-xs font-mono">{checkout.depositAddress}</code>
@@ -24,11 +24,7 @@ interface PaymentMethodPickerProps {
24
24
  onBack?: () => void;
25
25
  }
26
26
 
27
- export function PaymentMethodPicker({
28
- methods,
29
- onSelect,
30
- onBack,
31
- }: PaymentMethodPickerProps) {
27
+ export function PaymentMethodPicker({ methods, onSelect, onBack }: PaymentMethodPickerProps) {
32
28
  const [search, setSearch] = useState("");
33
29
  const [filter, setFilter] = useState<Filter>("popular");
34
30
 
@@ -0,0 +1,15 @@
1
+ import { trpcVanilla } from "./trpc";
2
+
3
+ export interface PoolConfig {
4
+ enabled: boolean;
5
+ poolSize: number;
6
+ warmCount: number;
7
+ }
8
+
9
+ export async function getPoolConfig(): Promise<PoolConfig> {
10
+ return trpcVanilla.admin.getPoolConfig.query({});
11
+ }
12
+
13
+ export async function setPoolSize(size: number): Promise<{ poolSize: number }> {
14
+ return trpcVanilla.admin.setPoolSize.mutate({ size });
15
+ }
@@ -80,6 +80,8 @@ type AppRouterRecord = {
80
80
  listAllOrgs: AnyTRPCQueryProcedure;
81
81
  billingOverview: AnyTRPCQueryProcedure;
82
82
  listAvailableModels: AnyTRPCQueryProcedure;
83
+ getPoolConfig: AnyTRPCQueryProcedure;
84
+ setPoolSize: AnyTRPCMutationProcedure;
83
85
  };
84
86
  promotions: {
85
87
  list: AnyTRPCQueryProcedure;