@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
@@ -0,0 +1,246 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Check, Copy, Eye, EyeOff, Terminal } from "lucide-react";
5
+ import Image from "next/image";
6
+ import { useState } from "react";
7
+
8
+ interface ConfigPreviewProps {
9
+ selectedConfig: any | undefined;
10
+ configContent: string;
11
+ configType: string;
12
+ isManaged: boolean;
13
+ isKeyVisible: boolean;
14
+ onToggleKeyVisibility: () => void;
15
+ onClose: () => void;
16
+ }
17
+
18
+ export default function ConfigPreview({
19
+ selectedConfig,
20
+ configContent,
21
+ configType,
22
+ isManaged,
23
+ isKeyVisible,
24
+ onToggleKeyVisibility,
25
+ onClose,
26
+ }: ConfigPreviewProps) {
27
+ const [copiedId, setCopiedId] = useState<string | null>(null);
28
+
29
+ const copyToClipboard = (text: string, id: string) => {
30
+ navigator.clipboard.writeText(text);
31
+ setCopiedId(id);
32
+ setTimeout(() => setCopiedId(null), 2000);
33
+ };
34
+
35
+ const highlightJSON = (jsonStr: string) => {
36
+ try {
37
+ const obj = JSON.parse(jsonStr);
38
+ const json = JSON.stringify(obj, null, 2);
39
+ return json.replace(
40
+ /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
41
+ (match) => {
42
+ let cls = "text-orange-300";
43
+ if (/^"/.test(match)) {
44
+ if (/:$/.test(match)) {
45
+ cls = "text-emerald-300";
46
+ } else {
47
+ cls = "text-yellow-200";
48
+ }
49
+ } else if (/true|false/.test(match)) {
50
+ cls = "text-blue-300";
51
+ } else if (/null/.test(match)) {
52
+ cls = "text-gray-400";
53
+ }
54
+ return `<span class="${cls}">${match}</span>`;
55
+ },
56
+ );
57
+ } catch (e) {
58
+ return jsonStr;
59
+ }
60
+ };
61
+
62
+ if (!selectedConfig) return null;
63
+
64
+ return (
65
+ <div className="flex-1 flex flex-col bg-white/[0.032]">
66
+ <div className="flex-1 overflow-y-auto overscroll-none p-8 space-y-4">
67
+ {/* Client Header */}
68
+ <div className="flex items-center gap-4">
69
+ {selectedConfig?.icon && (
70
+ <div className="w-14 h-14 flex-shrink-0 relative overflow-hidden">
71
+ <Image
72
+ src={selectedConfig.icon}
73
+ alt={selectedConfig.name}
74
+ fill
75
+ className={
76
+ [
77
+ "claude-desktop",
78
+ "claude-code",
79
+ "chatgpt",
80
+ "gemini-cli",
81
+ "perplexity",
82
+ ].includes(selectedConfig.id)
83
+ ? "object-cover"
84
+ : "object-contain"
85
+ }
86
+ />
87
+ </div>
88
+ )}
89
+ <div>
90
+ <h3 className="text-2xl font-bold text-white">
91
+ {selectedConfig?.name}
92
+ </h3>
93
+ <p className="text-sm text-neutral-400 uppercase tracking-wider">
94
+ Integrate MCP Client
95
+ </p>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="space-y-0 pt-2">
100
+ {selectedConfig?.steps?.map((step: string, index: number) => (
101
+ <div
102
+ key={index}
103
+ className="relative flex items-start gap-4 pb-4 group/step"
104
+ >
105
+ {/* Vertical Line */}
106
+ {index < (selectedConfig?.steps?.length || 0) - 1 && (
107
+ <div className="absolute top-6 bottom-0 left-[11.5px] w-px bg-emerald-500/20" />
108
+ )}
109
+
110
+ {/* Step Number */}
111
+ <div className="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/20 border border-emerald-500/50 text-emerald-400 text-xs font-bold z-10">
112
+ {index + 1}
113
+ </div>
114
+
115
+ {/* Step Text */}
116
+ <div
117
+ className="flex-1 text-sm text-neutral-300 leading-relaxed"
118
+ dangerouslySetInnerHTML={{
119
+ __html: step
120
+ .replace(
121
+ /\*\*(.+?)\*\*/g,
122
+ '<strong class="text-white font-semibold">$1</strong>',
123
+ )
124
+ .replace(
125
+ /`(.+?)`/g,
126
+ '<code class="px-1.5 py-0.5 rounded bg-white/10 text-emerald-400 font-mono text-xs">$1</code>',
127
+ ),
128
+ }}
129
+ />
130
+ </div>
131
+ ))}
132
+ </div>
133
+
134
+ {/* Config Code Block */}
135
+ <div className="relative group w-full overflow-hidden border border-white/10 rounded-lg shadow-[0_20px_50px_rgba(0,0,0,0.5)]">
136
+ <div className="relative rounded-t-lg bg-[#05100F] px-4 py-4 flex items-center justify-between border-b border-transparent">
137
+ <div className="flex items-center gap-4">
138
+ <div className="text-sm text-white font-semibold pl-2">
139
+ {(() => {
140
+ if (["claude-code", "gemini-cli"].includes(selectedConfig.id))
141
+ return "Terminal";
142
+ if (selectedConfig.filename) return selectedConfig.filename;
143
+ if (configType === "command" || configType === "cmd")
144
+ return "Terminal";
145
+ return "mcp.json";
146
+ })()}
147
+ </div>
148
+ </div>
149
+
150
+ <div className="flex items-center gap-2">
151
+ {!isManaged && (
152
+ <button
153
+ type="button"
154
+ onClick={onToggleKeyVisibility}
155
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-xs font-medium text-neutral-300 hover:text-white"
156
+ title="Unhide Token"
157
+ >
158
+ {isKeyVisible ? (
159
+ <EyeOff className="w-3.5 h-3.5" />
160
+ ) : (
161
+ <Eye className="w-3.5 h-3.5" />
162
+ )}
163
+ <span>Unhide Token</span>
164
+ <div
165
+ className={`relative inline-flex h-3.5 w-6.5 items-center rounded-full transition-colors ml-1 ${
166
+ isKeyVisible ? "bg-emerald-500" : "bg-neutral-600"
167
+ }`}
168
+ >
169
+ <span
170
+ className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white transition-transform ${
171
+ isKeyVisible ? "translate-x-3.5" : "translate-x-0.5"
172
+ }`}
173
+ />
174
+ </div>
175
+ </button>
176
+ )}
177
+
178
+ <button
179
+ onClick={() => copyToClipboard(configContent, "config")}
180
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-xs font-medium text-neutral-300 hover:text-white"
181
+ title="Copy"
182
+ >
183
+ {copiedId === "config" ? (
184
+ <>
185
+ <Check className="w-3.5 h-3.5 text-emerald-400" />
186
+ <span className="text-emerald-400">Copied</span>
187
+ </>
188
+ ) : (
189
+ <>
190
+ <Copy className="w-3.5 h-3.5" />
191
+ <span>Copy</span>
192
+ </>
193
+ )}
194
+ </button>
195
+ </div>
196
+ </div>
197
+
198
+ <div className="relative">
199
+ <div
200
+ className={`relative rounded-b-lg bg-[#05100F] font-mono text-xs md:text-sm text-white overflow-x-auto ${
201
+ configType === "command" || configType === "cmd"
202
+ ? "px-4 py-3"
203
+ : "pl-5 py-5 pr-5"
204
+ }`}
205
+ >
206
+ <pre className="text-shadow-sm">
207
+ {configType === "json" ? (
208
+ <code
209
+ dangerouslySetInnerHTML={{
210
+ __html: highlightJSON(configContent),
211
+ }}
212
+ />
213
+ ) : (
214
+ <code className="whitespace-pre-wrap break-all flex items-start gap-2">
215
+ {(configType === "command" || configType === "cmd") && (
216
+ <Terminal className="w-4 h-4 mt-0.5 flex-shrink-0 text-emerald-400" />
217
+ )}
218
+ <span>{configContent}</span>
219
+ </code>
220
+ )}
221
+ </pre>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <div className="border-t-[0.5px] border-white/10 px-8 py-4 flex justify-end gap-3 flex-none">
228
+ <Button
229
+ asChild
230
+ variant="ghost"
231
+ className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
232
+ >
233
+ <a href="https://docs.cybermem.dev" target="_blank">
234
+ Read Documentation
235
+ </a>
236
+ </Button>
237
+ <Button
238
+ onClick={onClose}
239
+ className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
240
+ >
241
+ Close
242
+ </Button>
243
+ </div>
244
+ </div>
245
+ );
246
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import Image from "next/image";
4
+
5
+ interface PlatformSelectorProps {
6
+ clients: any[];
7
+ selectedClient: string;
8
+ onSelect: (id: string) => void;
9
+ }
10
+
11
+ export default function PlatformSelector({
12
+ clients,
13
+ selectedClient,
14
+ onSelect,
15
+ }: PlatformSelectorProps) {
16
+ return (
17
+ <div className="w-80 border-r-[0.5px] border-white/10 flex-none overflow-y-auto overscroll-none bg-white/[0.015] rounded-l-3xl">
18
+ <div className="p-6">
19
+ <div className="flex items-center gap-2 mb-3">
20
+ <div className="h-2 w-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
21
+ <h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider">
22
+ Clients
23
+ </h3>
24
+ </div>
25
+ <h2 className="text-2xl font-bold text-white mb-6">Integrations</h2>
26
+ <div className="space-y-2">
27
+ {clients.map((client) => (
28
+ <button
29
+ key={client.id}
30
+ onClick={() => onSelect(client.id)}
31
+ className={`
32
+ w-full flex items-center gap-4 px-4 py-3.5 rounded-xl transition-all duration-200 relative
33
+ ${
34
+ selectedClient === client.id
35
+ ? "bg-white/[0.03] border-[0.5px] border-white/[0.05]"
36
+ : "hover:bg-white/5"
37
+ }
38
+ `}
39
+ >
40
+ {selectedClient === client.id && (
41
+ <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-5 bg-emerald-500 rounded-full shadow-[0_0_8px_rgba(16,185,129,0.6)]" />
42
+ )}
43
+ {client.icon ? (
44
+ <div
45
+ className={`w-10 h-10 flex-shrink-0 relative overflow-hidden rounded-lg border border-white/5 ${
46
+ [
47
+ "claude-desktop",
48
+ "claude-code",
49
+ "chatgpt",
50
+ "gemini-cli",
51
+ "perplexity",
52
+ ].includes(client.id)
53
+ ? ""
54
+ : "bg-white/5 p-1.5"
55
+ }`}
56
+ >
57
+ <Image
58
+ src={client.icon}
59
+ alt={client.name}
60
+ fill
61
+ className={`
62
+ ${
63
+ [
64
+ "claude-desktop",
65
+ "claude-code",
66
+ "chatgpt",
67
+ "gemini-cli",
68
+ "perplexity",
69
+ ].includes(client.id)
70
+ ? "object-cover"
71
+ : "object-contain"
72
+ } ${selectedClient === client.id ? "opacity-100" : "opacity-50 grayscale"}
73
+ `}
74
+ />
75
+ </div>
76
+ ) : (
77
+ <div className="w-7 h-7 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 flex-shrink-0">
78
+ <span className="text-xs font-bold">?</span>
79
+ </div>
80
+ )}
81
+ <span
82
+ className={`text-sm font-medium truncate ${
83
+ selectedClient === client.id
84
+ ? "text-white"
85
+ : "text-neutral-400"
86
+ }`}
87
+ >
88
+ {client.name}
89
+ </span>
90
+ </button>
91
+ ))}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }