@cybermem/dashboard 0.9.12 → 0.13.4

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.
Files changed (43) hide show
  1. package/Dockerfile +3 -3
  2. package/app/api/audit-logs/route.ts +12 -6
  3. package/app/api/health/route.ts +2 -1
  4. package/app/api/mcp-config/route.ts +128 -0
  5. package/app/api/metrics/route.ts +22 -70
  6. package/app/api/settings/route.ts +125 -30
  7. package/app/page.tsx +105 -127
  8. package/components/dashboard/{chart-card.tsx → charts/chart-card.tsx} +13 -19
  9. package/components/dashboard/{metrics-chart.tsx → charts/memory-chart.tsx} +1 -1
  10. package/components/dashboard/charts-section.tsx +3 -3
  11. package/components/dashboard/header.tsx +177 -176
  12. package/components/dashboard/{audit-log-table.tsx → logs/log-viewer.tsx} +12 -7
  13. package/components/dashboard/mcp/config-preview.tsx +246 -0
  14. package/components/dashboard/mcp/platform-selector.tsx +96 -0
  15. package/components/dashboard/mcp-config-modal.tsx +97 -503
  16. package/components/dashboard/{metric-card.tsx → metrics/stat-card.tsx} +4 -2
  17. package/components/dashboard/metrics-grid.tsx +10 -2
  18. package/components/dashboard/settings/access-token-section.tsx +131 -0
  19. package/components/dashboard/settings/data-management-section.tsx +122 -0
  20. package/components/dashboard/settings/system-info-section.tsx +98 -0
  21. package/components/dashboard/settings-modal.tsx +55 -299
  22. package/e2e/api.spec.ts +219 -0
  23. package/e2e/routing.spec.ts +39 -0
  24. package/e2e/ui.spec.ts +373 -0
  25. package/lib/data/dashboard-context.tsx +96 -29
  26. package/lib/data/types.ts +32 -38
  27. package/middleware.ts +31 -13
  28. package/package.json +6 -1
  29. package/playwright.config.ts +23 -58
  30. package/public/clients.json +5 -3
  31. package/release-reports/assets/local/1_dashboard.png +0 -0
  32. package/release-reports/assets/local/2_audit_logs.png +0 -0
  33. package/release-reports/assets/local/3_charts.png +0 -0
  34. package/release-reports/assets/local/4_mcp_modal.png +0 -0
  35. package/release-reports/assets/local/5_settings_modal.png +0 -0
  36. package/lib/data/demo-strategy.ts +0 -110
  37. package/lib/data/production-strategy.ts +0 -191
  38. package/lib/prometheus/client.ts +0 -58
  39. package/lib/prometheus/index.ts +0 -6
  40. package/lib/prometheus/metrics.ts +0 -234
  41. package/lib/prometheus/sparklines.ts +0 -71
  42. package/lib/prometheus/timeseries.ts +0 -305
  43. package/lib/prometheus/utils.ts +0 -176
@@ -1,248 +1,102 @@
1
1
  "use client";
2
2
 
3
3
  import { Button } from "@/components/ui/button";
4
- import { Input } from "@/components/ui/input";
5
- import { Label } from "@/components/ui/label";
6
4
  import { useDashboard } from "@/lib/data/dashboard-context";
7
- import {
8
- Check,
9
- Copy,
10
- Eye,
11
- EyeOff,
12
- FileCode,
13
- Info,
14
- Monitor,
15
- X,
16
- } from "lucide-react";
17
- import Image from "next/image";
5
+ import { X } from "lucide-react";
18
6
  import { useEffect, useState } from "react";
19
- import { toast } from "sonner";
7
+ import ConfigPreview from "./mcp/config-preview";
8
+ import PlatformSelector from "./mcp/platform-selector";
20
9
 
21
10
  export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
22
11
  const { clientConfigs } = useDashboard();
23
12
  const clients = clientConfigs;
13
+
14
+ // State
24
15
  const [selectedClient, setSelectedClient] = useState("claude");
25
- const [copiedId, setCopiedId] = useState<string | null>(null);
26
16
  const [apiKey, setApiKey] = useState("");
27
17
  const [baseUrl, setBaseUrl] = useState("http://localhost:8080");
28
- const [isLoading, setIsLoading] = useState(true);
18
+ const [isManaged, setIsManaged] = useState(false);
29
19
  const [isKeyVisible, setIsKeyVisible] = useState(false);
30
- const [showRegenConfirm, setShowRegenConfirm] = useState(false);
31
- const [regenInputValue, setRegenInputValue] = useState("");
32
- const [isManaged, setIsManaged] = useState(false); // true = local mode, no API key needed
33
20
 
34
- useEffect(() => {
35
- // Try to get key from local storage first (simulating persistence)
36
- const localKey = localStorage.getItem("om_api_key");
37
- if (localKey) {
38
- setApiKey(localKey);
39
- // We still fetch settings for the endpoint
40
- fetch("/api/settings")
41
- .then((res) => res.json())
42
- .then((data) => {
43
- let srvEndpoint = data.endpoint;
44
- if (
45
- srvEndpoint.includes("localhost") &&
46
- typeof window !== "undefined" &&
47
- !window.location.hostname.includes("localhost")
48
- ) {
49
- const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
50
- srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
51
- }
52
- setBaseUrl(srvEndpoint);
53
- setIsLoading(false);
54
- })
55
- .catch((err) => setIsLoading(false));
56
- } else {
57
- fetch("/api/settings")
58
- .then((res) => res.json())
59
- .then((data) => {
60
- setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
61
- setIsManaged(data.isManaged || false);
62
- let srvEndpoint = data.endpoint;
63
- if (
64
- srvEndpoint.includes("localhost") &&
65
- typeof window !== "undefined" &&
66
- !window.location.hostname.includes("localhost")
67
- ) {
68
- const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
69
- srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
70
- }
71
- setBaseUrl(srvEndpoint);
72
- setIsLoading(false);
73
- })
74
- .catch((err) => {
75
- console.error("Failed to fetch settings:", err);
76
- setIsLoading(false);
77
- });
78
- }
79
- }, []);
21
+ // Config State (Fetched from API)
22
+ const [configContent, setConfigContent] = useState("");
23
+ const [configType, setConfigType] = useState("json");
80
24
 
81
- const generateApiKey = () => {
82
- // Legacy - redirected to confirmRegenerate logic via UI state
83
- };
25
+ // selectedConfig object
26
+ const selectedConfig = (clients as any[]).find(
27
+ (c) => c.id === selectedClient,
28
+ );
84
29
 
85
- const confirmRegenerate = async () => {
86
- try {
87
- const res = await fetch("/api/settings/regenerate", { method: "POST" });
88
- if (!res.ok) throw new Error("Failed to regenerate key");
89
- const data = await res.json();
30
+ // 1. Initial Load: Get Settings (API Key, Base URL, Managed Status)
31
+ useEffect(() => {
32
+ const fetchSettings = async () => {
33
+ try {
34
+ const localKey = localStorage.getItem("om_api_key");
35
+ const res = await fetch("/api/settings");
36
+ const data = await res.json();
37
+
38
+ // Resolving Endpoint (Localhost Fix)
39
+ let srvEndpoint = data.endpoint;
40
+ if (
41
+ srvEndpoint.includes("localhost") &&
42
+ typeof window !== "undefined" &&
43
+ !window.location.hostname.includes("localhost")
44
+ ) {
45
+ const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
46
+ srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
47
+ }
90
48
 
91
- const newKey = data.apiKey;
92
- setApiKey(newKey);
93
- localStorage.setItem("om_api_key", newKey);
94
- setIsKeyVisible(true);
95
- setShowRegenConfirm(false);
96
- setRegenInputValue("");
97
- toast.success("Token Regenerated!", {
98
- description: "All existing client connections will need to be updated.",
99
- });
100
- } catch (e) {
101
- console.error(e);
102
- toast.error("Failed to regenerate token", {
103
- description: "Please check if the server is running.",
104
- });
105
- }
106
- };
49
+ setBaseUrl(srvEndpoint);
50
+ setIsManaged(data.isManaged || false);
107
51
 
108
- const getMcpConfig = (clientId: string) => {
109
- // Local mode: use stdio (command-based) - no server needed, runs via npx
110
- if (isManaged) {
111
- return {
112
- mcpServers: {
113
- cybermem: {
114
- command: "npx",
115
- args: ["@cybermem/mcp"],
116
- },
117
- },
118
- };
119
- }
120
-
121
- // Remote mode: use stdio with --url and --token
122
- return {
123
- mcpServers: {
124
- cybermem: {
125
- command: "npx",
126
- args: [
127
- "-y",
128
- "@cybermem/mcp",
129
- "--url",
130
- baseUrl,
131
- "--token",
132
- apiKey || "sk-your-generated-token",
133
- ],
134
- },
135
- },
52
+ if (localKey) {
53
+ setApiKey(localKey);
54
+ } else {
55
+ setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
56
+ }
57
+ } catch (err) {
58
+ console.error("Failed to fetch settings:", err);
59
+ }
136
60
  };
137
- };
138
-
139
- const getConfigContent = (maskKey = false) => {
140
- const config = (clients as any[]).find((c) => c.id === selectedClient);
141
- const displayKey = maskKey
142
- ? "••••••••••••••••"
143
- : apiKey || "sk-your-generated-token";
144
- const actualKey = apiKey || "sk-your-generated-token";
61
+ fetchSettings();
62
+ }, []);
145
63
 
146
- // Handle TOML config (Codex)
147
- if (config?.configType === "toml") {
148
- if (isManaged) {
149
- return `# CyberMem Configuration (Local Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp"]`;
150
- }
151
- return `# CyberMem Configuration (Remote Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp", "--url", "${baseUrl}", "--token", "${maskKey ? displayKey : actualKey}"]`;
152
- }
64
+ // 2. Fetch Config when Dependencies Change
65
+ useEffect(() => {
66
+ const fetchConfig = async () => {
67
+ try {
68
+ const params = new URLSearchParams({
69
+ client: selectedClient,
70
+ mask: (!isKeyVisible).toString(),
71
+ baseUrl: baseUrl,
72
+ });
153
73
 
154
- // Handle command-based configs (Claude Code, Gemini CLI, etc.)
155
- if (config?.configType === "command" || config?.configType === "cmd") {
156
- // Select command based on mode
157
- let cmd = isManaged ? config?.localCommand : config?.remoteCommand;
74
+ const res = await fetch(`/api/mcp-config?${params.toString()}`);
75
+ const data = await res.json();
158
76
 
159
- // Fallback to legacy 'command' field if new fields not present
160
- if (!cmd) {
161
- cmd = config?.command?.replace("http://localhost:8080", baseUrl) || "";
77
+ if (data.config) {
78
+ setConfigContent(
79
+ typeof data.config === "object"
80
+ ? JSON.stringify(data.config, null, 2)
81
+ : data.config,
82
+ );
83
+ setConfigType(data.configType);
84
+ }
85
+ } catch (err) {
86
+ console.error("Failed to fetch MCP config:", err);
87
+ setConfigContent("// Failed to load config");
162
88
  }
89
+ };
163
90
 
164
- // Substitute placeholders with actual values
165
- cmd = cmd.replace("{{ENDPOINT}}", baseUrl);
166
- cmd = cmd.replace("{{API_KEY}}", maskKey ? displayKey : actualKey);
167
- cmd = cmd.replace("{{TOKEN}}", maskKey ? displayKey : actualKey);
168
-
169
- return cmd;
91
+ if (baseUrl) {
92
+ fetchConfig();
170
93
  }
171
-
172
- // Default to JSON config
173
- const jsonConfig = getMcpConfig(selectedClient);
174
- return JSON.stringify(jsonConfig, null, 2);
175
- };
176
-
177
- const copyToClipboard = (text: string, id: string) => {
178
- navigator.clipboard.writeText(text);
179
- setCopiedId(id);
180
- setTimeout(() => setCopiedId(null), 2000);
181
- toast.success("Copied to clipboard!");
182
- };
183
-
184
- const highlightJSON = (obj: any) => {
185
- const json = JSON.stringify(obj, null, 2);
186
- return json.replace(
187
- /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
188
- (match) => {
189
- let cls = "text-orange-300";
190
- if (/^"/.test(match)) {
191
- if (/:$/.test(match)) {
192
- cls = "text-emerald-300";
193
- } else {
194
- cls = "text-yellow-200";
195
- }
196
- } else if (/true|false/.test(match)) {
197
- cls = "text-blue-300";
198
- } else if (/null/.test(match)) {
199
- cls = "text-gray-400";
200
- }
201
- return `<span class="${cls}">${match}</span>`;
202
- },
203
- );
204
- };
205
-
206
- const selectedConfig = (clients as any[]).find(
207
- (c) => c.id === selectedClient,
208
- );
209
-
210
- const renderInstructions = () => {
211
- if (!selectedConfig) return null;
212
-
213
- return (
214
- <div className="space-y-4 text-sm text-neutral-300">
215
- <p>{selectedConfig.description}</p>
216
- {selectedConfig.steps.length > 0 && (
217
- <ol className="list-decimal list-inside space-y-2 ml-2 text-neutral-400">
218
- {selectedConfig.steps.map((step: string, i: number) => (
219
- <li
220
- key={i}
221
- dangerouslySetInnerHTML={{
222
- __html: step
223
- .replace(
224
- /\*\*(.*?)\*\*/g,
225
- '<span class="text-white font-medium">$1</span>',
226
- )
227
- .replace(
228
- /`([^`]+)`/g,
229
- '<code class="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">$1</code>',
230
- ),
231
- }}
232
- />
233
- ))}
234
- </ol>
235
- )}
236
- </div>
237
- );
238
- };
239
-
240
- const configContent = getConfigContent();
94
+ }, [selectedClient, isKeyVisible, baseUrl]);
241
95
 
242
96
  return (
243
97
  <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
244
98
  <div
245
- className="w-full max-w-4xl bg-[#0B1116]/80 backdrop-blur-xl border border-emerald-500/20 rounded-2xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[85vh] relative overflow-hidden"
99
+ className="w-full max-w-6xl bg-[#05100F] backdrop-blur-xl border-[0.5px] border-white/10 rounded-3xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[90vh] relative overflow-hidden"
246
100
  style={{
247
101
  backgroundImage: `
248
102
  radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%),
@@ -252,293 +106,33 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
252
106
  `,
253
107
  }}
254
108
  >
255
- {/* Header */}
256
- <div className="flex items-center justify-between px-6 pt-6 pb-2 flex-none">
257
- <div className="flex items-center gap-3">
258
- <div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
259
- <Image
260
- src="/icons/mcp.png"
261
- alt="MCP Logo"
262
- width={20}
263
- height={20}
264
- className="drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]"
265
- />
266
- </div>
267
- <h2 className="text-xl font-semibold text-white text-shadow-sm">
268
- Integrate MCP Client
269
- </h2>
270
- </div>
271
- <Button
272
- variant="ghost"
273
- size="icon"
274
- onClick={onClose}
275
- className="text-neutral-400 hover:text-white hover:bg-white/10 rounded-full"
276
- >
277
- <X className="w-5 h-5" />
278
- </Button>
279
- </div>
280
-
281
- {/* Content */}
282
- <div className="p-6 space-y-6 overflow-y-auto flex-1 min-h-0">
283
- {/* Client Selector */}
284
- <section>
285
- <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
286
- <Monitor className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
287
- Select Client
288
- </h3>
289
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
290
- {clients.map((client) => (
291
- <button
292
- key={client.id}
293
- onClick={() => setSelectedClient(client.id)}
294
- className={`
295
- relative group flex flex-col items-center justify-center p-3 rounded-xl transition-all duration-300 border
296
- ${
297
- selectedClient === client.id
298
- ? "bg-emerald-500/10 border-emerald-500/50 shadow-[0_0_15px_rgba(16,185,129,0.1)] backdrop-blur-sm"
299
- : "bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/20 backdrop-blur-sm"
300
- }
301
- `}
302
- >
303
- <div className="mb-2 transition-transform duration-300 group-hover:scale-110">
304
- {client.icon ? (
305
- <Image
306
- src={client.icon}
307
- alt={client.name}
308
- width={32}
309
- height={32}
310
- className="object-contain drop-shadow-lg"
311
- />
312
- ) : (
313
- <div className="w-8 h-8 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 transition-transform duration-300">
314
- <span className="text-sm font-bold">?</span>
315
- </div>
316
- )}
317
- </div>
318
- <span
319
- className={`text-[10px] font-medium text-center transition-colors ${selectedClient === client.id ? "text-emerald-400 text-shadow-emerald" : "text-neutral-400 group-hover:text-white"}`}
320
- >
321
- {client.name}
322
- </span>
323
- </button>
324
- ))}
325
- </div>
326
- </section>
327
-
328
- {/* Instructions */}
329
- <section>
330
- <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
331
- <FileCode className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
332
- {selectedClient === "codex"
333
- ? "Configuration"
334
- : selectedClient === "other"
335
- ? "Configuration JSON"
336
- : "Integration Instructions"}
337
- </h3>
338
-
339
- <div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
340
- {selectedClient === "chatgpt" && (
341
- <div className="px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-200">
342
- <p>
343
- Requires Developer Mode.{" "}
344
- <a
345
- href="https://platform.openai.com/docs/guides/developer-mode"
346
- target="_blank"
347
- rel="noopener noreferrer"
348
- className="underline hover:text-white"
349
- >
350
- Read OpenAI Documentation
351
- </a>
352
- </p>
353
- </div>
354
- )}
355
-
356
- {renderInstructions()}
357
-
358
- {/* API Key Control Row - Only show in remote mode */}
359
- {!isManaged ? (
360
- <div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm mb-4">
361
- <div className="space-y-2">
362
- <Label htmlFor="mcp-api-key" className="text-neutral-200">
363
- Security Token
364
- </Label>
365
- <div className="flex gap-2">
366
- <div className="relative flex-1">
367
- <Input
368
- id="mcp-api-key"
369
- value={apiKey || "sk-not-generated-yet"}
370
- readOnly
371
- className="bg-black/40 border-white/10 text-white focus-visible:border-emerald-500/30 focus-visible:ring-emerald-500/10 placeholder:text-neutral-600 shadow-inner pr-10 font-mono"
372
- type={isKeyVisible ? "text" : "password"}
373
- />
374
- <button
375
- type="button"
376
- onClick={() => setIsKeyVisible(!isKeyVisible)}
377
- className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
378
- >
379
- {isKeyVisible ? (
380
- <EyeOff className="w-4 h-4" />
381
- ) : (
382
- <Eye className="w-4 h-4" />
383
- )}
384
- </button>
385
- </div>
386
- <Button
387
- size="icon"
388
- variant="ghost"
389
- className="h-10 w-10 border border-white/10 bg-white/5 hover:bg-white/10 text-neutral-400 hover:text-white"
390
- onClick={() => copyToClipboard(apiKey, "apikey")}
391
- title="Copy API Key"
392
- >
393
- {copiedId === "apikey" ? (
394
- <Check className="h-4 w-4 text-emerald-400" />
395
- ) : (
396
- <Copy className="h-4 w-4" />
397
- )}
398
- </Button>
399
- </div>
400
-
401
- {/* Regeneration Controls */}
402
- <div className="flex justify-end pt-2">
403
- {showRegenConfirm ? (
404
- <div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-4 duration-200">
405
- <span className="text-xs text-red-400 font-medium">
406
- Warning: Disconnects clients.
407
- </span>
408
- <Input
409
- value={regenInputValue}
410
- onChange={(e) => setRegenInputValue(e.target.value)}
411
- placeholder="Type 'agree'"
412
- className="h-8 w-28 bg-red-500/10 border-red-500/30 text-red-200 text-xs placeholder:text-red-500/30 focus-visible:border-red-500/50"
413
- />
414
- <Button
415
- size="sm"
416
- variant="ghost"
417
- className="h-8 px-3 text-neutral-400 hover:text-white hover:bg-white/10"
418
- onClick={() => {
419
- setShowRegenConfirm(false);
420
- setRegenInputValue("");
421
- }}
422
- >
423
- Cancel
424
- </Button>
425
- <Button
426
- size="sm"
427
- disabled={regenInputValue !== "agree"}
428
- className="h-8 px-3 bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
429
- onClick={confirmRegenerate}
430
- >
431
- Confirm
432
- </Button>
433
- </div>
434
- ) : (
435
- <Button
436
- size="sm"
437
- variant="ghost"
438
- className="h-8 px-2 text-neutral-400 hover:text-white hover:bg-white/10"
439
- onClick={() => setShowRegenConfirm(true)}
440
- >
441
- Regenerate Key
442
- </Button>
443
- )}
444
- </div>
445
- </div>
446
- </div>
447
- ) : (
448
- <div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-4 mb-4">
449
- <div className="flex items-start gap-3">
450
- <Info className="h-4 w-4 shrink-0 text-emerald-400 mt-0.5" />
451
- <div className="space-y-1">
452
- <p className="text-sm font-medium text-emerald-200">
453
- Local Mode Active
454
- </p>
455
- <p className="text-xs text-emerald-200/60">
456
- No token required for connection from your laptop. Just
457
- copy the config below.
458
- </p>
459
- </div>
460
- </div>
461
- </div>
462
- )}
463
-
464
- <div className="relative group">
465
- <div className="relative pl-5 py-5 pr-24 rounded-lg bg-[#0F161C] border border-white/10 font-mono text-xs md:text-sm text-white overflow-x-auto shadow-[0_0_20px_rgba(0,0,0,0.3)] inset-shadow">
466
- <pre className="text-shadow-sm">
467
- {(() => {
468
- const config = (clients as any[]).find(
469
- (c) => c.id === selectedClient,
470
- );
471
- if (config?.configType === "json") {
472
- return (
473
- <code
474
- dangerouslySetInnerHTML={{
475
- __html: highlightJSON(
476
- JSON.parse(getConfigContent(!isKeyVisible)),
477
- ),
478
- }}
479
- />
480
- );
481
- } else {
482
- return getConfigContent(!isKeyVisible);
483
- }
484
- })()}
485
- </pre>
486
- </div>
487
- <div className="absolute top-5 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
488
- <Button
489
- size="sm"
490
- variant="ghost"
491
- className="h-8 px-3 text-white bg-black/40 backdrop-blur border border-white/5 shadow-[0_0_10px_rgba(255,255,255,0.05)] hover:bg-white/10 hover:text-white font-medium"
492
- onClick={() =>
493
- copyToClipboard(getConfigContent(false), "config")
494
- }
495
- >
496
- {copiedId === "config" ? (
497
- <Check className="h-4 w-4 stroke-[2.5] text-emerald-400 mr-2 drop-shadow-[0_0_5px_rgba(52,211,153,0.5)]" />
498
- ) : (
499
- <Copy className="h-4 w-4 stroke-[2.5] mr-2 text-white drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
500
- )}
501
- {copiedId === "config" ? (
502
- <span className="text-emerald-400 text-shadow-sm">
503
- Copied
504
- </span>
505
- ) : (
506
- <span className="text-white text-shadow-sm">Copy</span>
507
- )}
508
- </Button>
509
- </div>
510
- </div>
511
-
512
- {!isManaged && (
513
- <div className="flex items-start gap-3 p-3 rounded-lg bg-emerald-500/5 border border-emerald-500/10 text-emerald-200/70 text-xs mt-4">
514
- <Info className="h-4 w-4 shrink-0 text-white mt-0.5" />
515
- <p>
516
- This configuration includes your generated Security Token.
517
- Keep it secure and do not share it publicly.
518
- </p>
519
- </div>
520
- )}
521
- </div>
522
- </section>
523
- </div>
524
-
525
- {/* Footer */}
526
- <div className="border-t border-emerald-500/20 px-6 py-4 flex justify-end gap-3 flex-none bg-[#0B1116]/30">
527
- <Button
528
- asChild
529
- variant="ghost"
530
- className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
531
- >
532
- <a href="https://docs.cybermem.dev" target="_blank">
533
- Read Documentation
534
- </a>
535
- </Button>
536
- <Button
537
- onClick={onClose}
538
- className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
539
- >
540
- Close
541
- </Button>
109
+ {/* Close Button */}
110
+ <Button
111
+ variant="ghost"
112
+ size="icon"
113
+ onClick={onClose}
114
+ className="absolute top-6 right-6 z-10 text-neutral-400 hover:text-white hover:bg-white/10 rounded-full"
115
+ >
116
+ <X className="w-5 h-5" />
117
+ </Button>
118
+
119
+ {/* Layout */}
120
+ <div className="flex flex-1 min-h-0">
121
+ <PlatformSelector
122
+ clients={clients}
123
+ selectedClient={selectedClient}
124
+ onSelect={setSelectedClient}
125
+ />
126
+
127
+ <ConfigPreview
128
+ selectedConfig={selectedConfig}
129
+ configContent={configContent}
130
+ configType={configType}
131
+ isManaged={isManaged}
132
+ isKeyVisible={isKeyVisible}
133
+ onToggleKeyVisibility={() => setIsKeyVisible(!isKeyVisible)}
134
+ onClose={onClose}
135
+ />
542
136
  </div>
543
137
  </div>
544
138
  </div>
@@ -12,7 +12,7 @@ interface MetricCardProps {
12
12
  loading?: boolean;
13
13
  }
14
14
 
15
- export default function MetricCard({
15
+ export default function StatCard({
16
16
  label,
17
17
  value,
18
18
  loading = false,
@@ -49,7 +49,9 @@ export default function MetricCard({
49
49
  <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative group hover:bg-white/[0.07] transition-colors">
50
50
  <CardContent className="p-6 relative z-10">
51
51
  <div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
52
- <div className="text-4xl font-bold text-white">{value}</div>
52
+ <div className="text-4xl font-bold text-white" data-testid="stat-value">
53
+ {value}
54
+ </div>
53
55
  </CardContent>
54
56
  </Card>
55
57
  );