@cybermem/dashboard 0.1.0 → 0.5.1
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/README.md +5 -0
- package/app/api/audit-logs/route.ts +4 -1
- package/app/api/health/route.ts +94 -0
- package/app/api/settings/regenerate/route.ts +32 -3
- package/app/api/settings/route.ts +30 -4
- package/app/client-connect/page.tsx +45 -0
- package/app/page.tsx +28 -1
- package/components/dashboard/audit-log-table.tsx +102 -9
- package/components/dashboard/chart-card.tsx +5 -4
- package/components/dashboard/header.tsx +79 -5
- package/components/dashboard/login-modal.tsx +0 -4
- package/components/dashboard/mcp-config-modal.tsx +56 -12
- package/components/dashboard/metrics-grid.tsx +17 -4
- package/components/dashboard/password-alert-modal.tsx +72 -0
- package/components/dashboard/settings-modal.tsx +82 -217
- package/e2e/audit-export.spec.ts +111 -0
- package/e2e/auth.spec.ts +37 -0
- package/e2e/config-ui.spec.ts +98 -0
- package/e2e/crud-happy-path.spec.ts +214 -0
- package/e2e/metrics.spec.ts +28 -0
- package/eslint.config.mjs +15 -0
- package/lib/data/dashboard-context.tsx +44 -1
- package/lib/rate-limit.ts +77 -0
- package/middleware.ts +43 -0
- package/package.json +16 -5
- package/playwright.config.ts +35 -0
- package/public/clients.json +61 -35
- package/.eslintrc.json +0 -3
- package/shared.env +0 -0
|
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"
|
|
|
5
5
|
import { Label } from "@/components/ui/label"
|
|
6
6
|
import { useDashboard } from "@/lib/data/dashboard-context"
|
|
7
7
|
import { Check, Copy, Eye, EyeOff, FileCode, Info, Monitor, X } from "lucide-react"
|
|
8
|
+
import Image from "next/image"
|
|
8
9
|
import { useEffect, useState } from "react"
|
|
9
10
|
|
|
10
11
|
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
@@ -18,6 +19,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
18
19
|
const [isKeyVisible, setIsKeyVisible] = useState(false)
|
|
19
20
|
const [showRegenConfirm, setShowRegenConfirm] = useState(false)
|
|
20
21
|
const [regenInputValue, setRegenInputValue] = useState("")
|
|
22
|
+
const [isManaged, setIsManaged] = useState(false) // true = local mode, no API key needed
|
|
21
23
|
|
|
22
24
|
useEffect(() => {
|
|
23
25
|
// Try to get key from local storage first (simulating persistence)
|
|
@@ -30,7 +32,8 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
30
32
|
.then(data => {
|
|
31
33
|
let srvEndpoint = data.endpoint
|
|
32
34
|
if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
|
|
33
|
-
|
|
35
|
+
const port = srvEndpoint.split(':').pop()?.split('/')[0] || '8626'
|
|
36
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`
|
|
34
37
|
}
|
|
35
38
|
setBaseUrl(srvEndpoint)
|
|
36
39
|
setIsLoading(false)
|
|
@@ -41,9 +44,11 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
41
44
|
.then(res => res.json())
|
|
42
45
|
.then(data => {
|
|
43
46
|
setApiKey(data.apiKey !== 'not-set' ? data.apiKey : '')
|
|
47
|
+
setIsManaged(data.isManaged || false)
|
|
44
48
|
let srvEndpoint = data.endpoint
|
|
45
49
|
if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
|
|
46
|
-
|
|
50
|
+
const port = srvEndpoint.split(':').pop()?.split('/')[0] || '8626'
|
|
51
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`
|
|
47
52
|
}
|
|
48
53
|
setBaseUrl(srvEndpoint)
|
|
49
54
|
setIsLoading(false)
|
|
@@ -81,6 +86,19 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
81
86
|
const isAntigravity = clientId === 'antigravity'
|
|
82
87
|
const urlKey = isAntigravity ? 'serverUrl' : 'url'
|
|
83
88
|
|
|
89
|
+
// Local mode: use stdio (command-based) - no server needed, runs via npx
|
|
90
|
+
if (isManaged) {
|
|
91
|
+
return {
|
|
92
|
+
mcpServers: {
|
|
93
|
+
cybermem: {
|
|
94
|
+
command: "npx",
|
|
95
|
+
args: ["@cybermem/mcp-core"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Remote mode: use SSE URL with API key header
|
|
84
102
|
return {
|
|
85
103
|
mcpServers: {
|
|
86
104
|
cybermem: {
|
|
@@ -100,27 +118,39 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
100
118
|
|
|
101
119
|
// Handle TOML config (Codex)
|
|
102
120
|
if (config?.configType === 'toml') {
|
|
121
|
+
if (isManaged) {
|
|
122
|
+
return `# CyberMem Configuration (Local Mode)\n[mcp]\ncommand = "npx"\nargs = ["@cybermem/mcp-core"]`;
|
|
123
|
+
}
|
|
103
124
|
return `# CyberMem Configuration\n[mcp]\nserver_url = "${baseUrl}/mcp"\napi_key = "${maskKey ? displayKey : actualKey}"`;
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
// Handle command-based configs (Claude Code, Gemini CLI, etc.)
|
|
107
|
-
if ((config?.configType === 'command' || config?.configType === 'cmd')
|
|
108
|
-
|
|
128
|
+
if ((config?.configType === 'command' || config?.configType === 'cmd')) {
|
|
129
|
+
// Select command based on mode
|
|
130
|
+
let cmd = isManaged ? config?.localCommand : config?.remoteCommand;
|
|
131
|
+
|
|
132
|
+
// Fallback to legacy 'command' field if new fields not present
|
|
133
|
+
if (!cmd) {
|
|
134
|
+
cmd = config?.command?.replace("http://localhost:8080", baseUrl) || '';
|
|
135
|
+
}
|
|
109
136
|
|
|
110
|
-
//
|
|
111
|
-
|
|
137
|
+
// Substitute {{ENDPOINT}} placeholder with actual endpoint
|
|
138
|
+
cmd = cmd.replace('{{ENDPOINT}}', `${baseUrl}/mcp`);
|
|
139
|
+
|
|
140
|
+
// Remote mode - inject API key for SSE transport commands
|
|
141
|
+
if (!isManaged && cmd.includes('--transport sse')) {
|
|
112
142
|
const headerPart = `--header "x-api-key: ${maskKey ? displayKey : actualKey}"`;
|
|
113
143
|
if (!cmd.includes('x-api-key')) {
|
|
114
|
-
// Insert before transport or just at the end
|
|
115
144
|
cmd = cmd.replace('mcp add', `mcp add ${headerPart}`);
|
|
116
145
|
}
|
|
117
146
|
}
|
|
147
|
+
|
|
118
148
|
return cmd;
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
// Default to JSON config
|
|
122
152
|
const jsonConfig = getMcpConfig(selectedClient);
|
|
123
|
-
if (maskKey) {
|
|
153
|
+
if (!isManaged && maskKey) {
|
|
124
154
|
(jsonConfig.mcpServers.cybermem as any).headers["x-api-key"] = displayKey;
|
|
125
155
|
}
|
|
126
156
|
return JSON.stringify(jsonConfig, null, 2);
|
|
@@ -193,7 +223,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
193
223
|
<div className="flex items-center justify-between px-6 pt-6 pb-2 flex-none">
|
|
194
224
|
<div className="flex items-center gap-3">
|
|
195
225
|
<div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
|
|
196
|
-
<
|
|
226
|
+
<Image src="/icons/mcp.png" alt="MCP Logo" width={20} height={20} className="drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
|
|
197
227
|
</div>
|
|
198
228
|
<h2 className="text-xl font-semibold text-white text-shadow-sm">Integrate MCP Client</h2>
|
|
199
229
|
</div>
|
|
@@ -231,7 +261,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
231
261
|
>
|
|
232
262
|
<div className="mb-2 transition-transform duration-300 group-hover:scale-110">
|
|
233
263
|
{client.icon ? (
|
|
234
|
-
<
|
|
264
|
+
<Image src={client.icon} alt={client.name} width={32} height={32} className="object-contain drop-shadow-lg" />
|
|
235
265
|
) : (
|
|
236
266
|
<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">
|
|
237
267
|
<span className="text-sm font-bold">?</span>
|
|
@@ -262,7 +292,8 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
262
292
|
|
|
263
293
|
{renderInstructions()}
|
|
264
294
|
|
|
265
|
-
{/* API Key Control Row
|
|
295
|
+
{/* API Key Control Row - Only show in remote mode */}
|
|
296
|
+
{!isManaged ? (
|
|
266
297
|
<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">
|
|
267
298
|
<div className="space-y-2">
|
|
268
299
|
<Label htmlFor="mcp-api-key" className="text-neutral-200">Master API Key</Label>
|
|
@@ -338,6 +369,17 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
338
369
|
</div>
|
|
339
370
|
</div>
|
|
340
371
|
</div>
|
|
372
|
+
) : (
|
|
373
|
+
<div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
374
|
+
<div className="flex items-start gap-3">
|
|
375
|
+
<Info className="h-4 w-4 shrink-0 text-emerald-400 mt-0.5" />
|
|
376
|
+
<div className="space-y-1">
|
|
377
|
+
<p className="text-sm font-medium text-emerald-200">Local Mode Active</p>
|
|
378
|
+
<p className="text-xs text-emerald-200/60">No API key required for connection from your laptop. Just copy the config below.</p>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
341
383
|
|
|
342
384
|
<div className="relative group">
|
|
343
385
|
<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">
|
|
@@ -365,12 +407,14 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
365
407
|
</div>
|
|
366
408
|
</div>
|
|
367
409
|
|
|
368
|
-
|
|
410
|
+
{!isManaged && (
|
|
411
|
+
<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">
|
|
369
412
|
<Info className="h-4 w-4 shrink-0 text-white mt-0.5" />
|
|
370
413
|
<p>
|
|
371
414
|
This configuration includes your generated API key. Keep it secure and do not share it publicly.
|
|
372
415
|
</p>
|
|
373
416
|
</div>
|
|
417
|
+
)}
|
|
374
418
|
</div>
|
|
375
419
|
</section>
|
|
376
420
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent } from "@/components/ui/card"
|
|
4
|
+
import { useDashboard } from "@/lib/data/dashboard-context"
|
|
4
5
|
import MetricCard from "./metric-card"
|
|
5
6
|
|
|
6
7
|
// Types
|
|
@@ -29,7 +30,19 @@ interface MetricsGridProps {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
|
|
32
34
|
export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
|
|
35
|
+
const { clientConfigs } = useDashboard()
|
|
36
|
+
|
|
37
|
+
const getClientDisplayName = (rawName: string) => {
|
|
38
|
+
if (!rawName || rawName === "N/A" || rawName === "unknown") return rawName
|
|
39
|
+
|
|
40
|
+
const nameLower = rawName.toLowerCase()
|
|
41
|
+
// Find matching client config (e.g. "antigravity" matches "antigravity-client")
|
|
42
|
+
const config = clientConfigs.find((c: any) => nameLower.includes(c.match))
|
|
43
|
+
return config ? config.name : rawName
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
const formatTimestamp = (timestamp: number) => {
|
|
34
47
|
if (timestamp <= 0) return "No activity"
|
|
35
48
|
const date = new Date(timestamp)
|
|
@@ -86,7 +99,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
|
|
|
86
99
|
<Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
|
|
87
100
|
<CardContent className="pt-6 pb-6 relative">
|
|
88
101
|
<div className="text-sm font-medium text-slate-400 mb-2">Top Writer</div>
|
|
89
|
-
<div className="text-4xl font-bold text-white mb-1 truncate">{stats.topWriter.name}</div>
|
|
102
|
+
<div className="text-4xl font-bold text-white mb-1 truncate">{getClientDisplayName(stats.topWriter.name)}</div>
|
|
90
103
|
<div className="text-xl text-white/80 whitespace-nowrap">
|
|
91
104
|
{stats.topWriter.count > 0 ? `${stats.topWriter.count.toLocaleString()} writes` : ""}
|
|
92
105
|
</div>
|
|
@@ -98,7 +111,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
|
|
|
98
111
|
<CardContent className="pt-6 pb-6 relative">
|
|
99
112
|
<div className="text-sm font-medium text-slate-400 mb-2">Top Reader</div>
|
|
100
113
|
<div className="text-4xl font-bold text-white mb-1 truncate">
|
|
101
|
-
{stats.topReader.count > 0 ? stats.topReader.name : "N/A"}
|
|
114
|
+
{stats.topReader.count > 0 ? getClientDisplayName(stats.topReader.name) : "N/A"}
|
|
102
115
|
</div>
|
|
103
116
|
<div className="text-xl text-white/80 whitespace-nowrap">
|
|
104
117
|
{stats.topReader.count > 0 ? `${stats.topReader.count.toLocaleString()} reads` : ""}
|
|
@@ -111,7 +124,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
|
|
|
111
124
|
<CardContent className="pt-6 pb-6 relative">
|
|
112
125
|
<div className="text-sm font-medium text-slate-400 mb-2">Last Writer</div>
|
|
113
126
|
<div className="text-4xl font-bold text-white mb-1 truncate">
|
|
114
|
-
{stats.lastWriter.name !== "N/A" ? stats.lastWriter.name : "N/A"}
|
|
127
|
+
{stats.lastWriter.name !== "N/A" ? getClientDisplayName(stats.lastWriter.name) : "N/A"}
|
|
115
128
|
</div>
|
|
116
129
|
<div className="text-xl text-white/80 whitespace-nowrap">
|
|
117
130
|
{stats.lastWriter.timestamp > 0 ? formatTimestamp(stats.lastWriter.timestamp) : "No activity"}
|
|
@@ -124,7 +137,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
|
|
|
124
137
|
<CardContent className="pt-6 pb-6 relative">
|
|
125
138
|
<div className="text-sm font-medium text-slate-400 mb-2">Last Reader</div>
|
|
126
139
|
<div className="text-4xl font-bold text-white mb-1 truncate">
|
|
127
|
-
{stats.lastReader.name !== "N/A" ? stats.lastReader.name : "N/A"}
|
|
140
|
+
{stats.lastReader.name !== "N/A" ? getClientDisplayName(stats.lastReader.name) : "N/A"}
|
|
128
141
|
</div>
|
|
129
142
|
<div className="text-xl text-white/80 whitespace-nowrap">
|
|
130
143
|
{stats.lastReader.timestamp > 0 ? formatTimestamp(stats.lastReader.timestamp) : "No activity"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import { AlertTriangle, Settings, X } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
interface PasswordAlertModalProps {
|
|
7
|
+
onChangePassword: () => void
|
|
8
|
+
onDismiss: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function PasswordAlertModal({
|
|
12
|
+
onChangePassword,
|
|
13
|
+
onDismiss
|
|
14
|
+
}: PasswordAlertModalProps) {
|
|
15
|
+
const handleDontShowAgain = () => {
|
|
16
|
+
localStorage.setItem("hidePasswordWarning", "true")
|
|
17
|
+
onDismiss()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
22
|
+
<div
|
|
23
|
+
className="bg-[#0B1116]/90 backdrop-blur-xl border border-amber-500/30 rounded-2xl shadow-2xl max-w-md w-full overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
|
24
|
+
style={{
|
|
25
|
+
backgroundImage: `
|
|
26
|
+
radial-gradient(circle at 0% 0%, rgba(251, 191, 36, 0.05) 0%, transparent 50%),
|
|
27
|
+
radial-gradient(circle at 100% 100%, rgba(251, 191, 36, 0.05) 0%, transparent 50%)
|
|
28
|
+
`
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{/* Header */}
|
|
32
|
+
<div className="px-6 pt-6 pb-4 flex items-start gap-4">
|
|
33
|
+
<div className="p-3 bg-amber-500/10 rounded-full border border-amber-500/20 shrink-0">
|
|
34
|
+
<AlertTriangle className="w-6 h-6 text-amber-400 drop-shadow-[0_0_10px_rgba(251,191,36,0.5)]" />
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex-1">
|
|
37
|
+
<h2 className="text-lg font-semibold text-white">Default Password Detected</h2>
|
|
38
|
+
<p className="text-neutral-400 text-sm mt-1">
|
|
39
|
+
You're using the default admin password. For security, we recommend changing it now.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<Button
|
|
43
|
+
variant="ghost"
|
|
44
|
+
size="icon"
|
|
45
|
+
onClick={onDismiss}
|
|
46
|
+
className="text-neutral-400 hover:text-white hover:bg-white/10 rounded-full -mt-1 -mr-2"
|
|
47
|
+
>
|
|
48
|
+
<X className="w-4 h-4" />
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Actions */}
|
|
53
|
+
<div className="px-6 pb-6 flex flex-col gap-3">
|
|
54
|
+
<Button
|
|
55
|
+
onClick={onChangePassword}
|
|
56
|
+
className="w-full bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 font-medium transition-colors gap-2"
|
|
57
|
+
>
|
|
58
|
+
<Settings className="w-4 h-4" />
|
|
59
|
+
Change Password
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
variant="ghost"
|
|
63
|
+
onClick={handleDontShowAgain}
|
|
64
|
+
className="w-full text-neutral-400 hover:text-white hover:bg-white/10"
|
|
65
|
+
>
|
|
66
|
+
Don't show again
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|