@cybermem/dashboard 0.1.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.
Files changed (123) hide show
  1. package/.dockerignore +11 -0
  2. package/.eslintrc.json +3 -0
  3. package/Dockerfile +48 -0
  4. package/app/api/audit-logs/route.ts +60 -0
  5. package/app/api/metrics/route.ts +141 -0
  6. package/app/api/prometheus/route.ts +65 -0
  7. package/app/api/settings/regenerate/route.ts +20 -0
  8. package/app/api/settings/route.ts +25 -0
  9. package/app/api/system/restart/route.ts +18 -0
  10. package/app/globals.css +148 -0
  11. package/app/layout.tsx +37 -0
  12. package/app/page.tsx +150 -0
  13. package/components/dashboard/audit-log-table.tsx +195 -0
  14. package/components/dashboard/chart-card.tsx +196 -0
  15. package/components/dashboard/charts-section.tsx +16 -0
  16. package/components/dashboard/header.tsx +82 -0
  17. package/components/dashboard/login-modal.tsx +87 -0
  18. package/components/dashboard/mcp-config-modal.tsx +397 -0
  19. package/components/dashboard/metric-card.tsx +23 -0
  20. package/components/dashboard/metrics-chart.tsx +134 -0
  21. package/components/dashboard/metrics-grid.tsx +136 -0
  22. package/components/dashboard/settings-modal.tsx +345 -0
  23. package/components/theme-provider.tsx +11 -0
  24. package/components/ui/accordion.tsx +66 -0
  25. package/components/ui/alert-dialog.tsx +157 -0
  26. package/components/ui/alert.tsx +66 -0
  27. package/components/ui/aspect-ratio.tsx +11 -0
  28. package/components/ui/avatar.tsx +53 -0
  29. package/components/ui/badge.tsx +46 -0
  30. package/components/ui/breadcrumb.tsx +109 -0
  31. package/components/ui/button-group.tsx +83 -0
  32. package/components/ui/button.tsx +60 -0
  33. package/components/ui/calendar.tsx +213 -0
  34. package/components/ui/card.tsx +92 -0
  35. package/components/ui/carousel.tsx +241 -0
  36. package/components/ui/chart.tsx +353 -0
  37. package/components/ui/checkbox.tsx +32 -0
  38. package/components/ui/collapsible.tsx +33 -0
  39. package/components/ui/command.tsx +184 -0
  40. package/components/ui/context-menu.tsx +252 -0
  41. package/components/ui/dialog.tsx +143 -0
  42. package/components/ui/drawer.tsx +135 -0
  43. package/components/ui/dropdown-menu.tsx +257 -0
  44. package/components/ui/empty.tsx +104 -0
  45. package/components/ui/field.tsx +244 -0
  46. package/components/ui/form.tsx +167 -0
  47. package/components/ui/hover-card.tsx +44 -0
  48. package/components/ui/input-group.tsx +169 -0
  49. package/components/ui/input-otp.tsx +77 -0
  50. package/components/ui/input.tsx +21 -0
  51. package/components/ui/item.tsx +193 -0
  52. package/components/ui/kbd.tsx +28 -0
  53. package/components/ui/label.tsx +24 -0
  54. package/components/ui/menubar.tsx +276 -0
  55. package/components/ui/navigation-menu.tsx +166 -0
  56. package/components/ui/pagination.tsx +127 -0
  57. package/components/ui/popover.tsx +48 -0
  58. package/components/ui/progress.tsx +31 -0
  59. package/components/ui/radio-group.tsx +45 -0
  60. package/components/ui/resizable.tsx +56 -0
  61. package/components/ui/scroll-area.tsx +58 -0
  62. package/components/ui/select.tsx +185 -0
  63. package/components/ui/separator.tsx +28 -0
  64. package/components/ui/sheet.tsx +139 -0
  65. package/components/ui/sidebar.tsx +726 -0
  66. package/components/ui/skeleton.tsx +13 -0
  67. package/components/ui/slider.tsx +63 -0
  68. package/components/ui/sonner.tsx +25 -0
  69. package/components/ui/spinner.tsx +16 -0
  70. package/components/ui/switch.tsx +31 -0
  71. package/components/ui/table.tsx +116 -0
  72. package/components/ui/tabs.tsx +66 -0
  73. package/components/ui/textarea.tsx +18 -0
  74. package/components/ui/toast.tsx +129 -0
  75. package/components/ui/toaster.tsx +35 -0
  76. package/components/ui/toggle-group.tsx +73 -0
  77. package/components/ui/toggle.tsx +47 -0
  78. package/components/ui/tooltip.tsx +61 -0
  79. package/components/ui/use-mobile.tsx +19 -0
  80. package/components/ui/use-toast.ts +191 -0
  81. package/components.json +21 -0
  82. package/hooks/use-mobile.ts +19 -0
  83. package/hooks/use-toast.ts +191 -0
  84. package/lib/data/dashboard-context.tsx +75 -0
  85. package/lib/data/demo-strategy.ts +110 -0
  86. package/lib/data/production-strategy.ts +152 -0
  87. package/lib/data/types.ts +52 -0
  88. package/lib/prometheus/client.ts +58 -0
  89. package/lib/prometheus/index.ts +6 -0
  90. package/lib/prometheus/metrics.ts +234 -0
  91. package/lib/prometheus/sparklines.ts +71 -0
  92. package/lib/prometheus/timeseries.ts +305 -0
  93. package/lib/prometheus/utils.ts +176 -0
  94. package/lib/utils.ts +6 -0
  95. package/next.config.mjs +36 -0
  96. package/package.json +91 -0
  97. package/postcss.config.mjs +8 -0
  98. package/public/clients.json +165 -0
  99. package/public/favicon-dark.svg +1 -0
  100. package/public/favicon-light.svg +1 -0
  101. package/public/icons/antigravity.png +0 -0
  102. package/public/icons/chatgpt.png +0 -0
  103. package/public/icons/claude-code.png +0 -0
  104. package/public/icons/claude.png +0 -0
  105. package/public/icons/codex.png +0 -0
  106. package/public/icons/cursor.png +0 -0
  107. package/public/icons/gemini.png +0 -0
  108. package/public/icons/images.jpeg +0 -0
  109. package/public/icons/mcp.png +0 -0
  110. package/public/icons/mono.png +0 -0
  111. package/public/icons/perplexity.png +0 -0
  112. package/public/icons/vscode.png +0 -0
  113. package/public/icons/warp.png +0 -0
  114. package/public/icons/windsurf.png +0 -0
  115. package/public/logo.png +0 -0
  116. package/public/logo.svg +7 -0
  117. package/public/manifest.json +21 -0
  118. package/public/site.webmanifest +21 -0
  119. package/public/web-app-manifest-192x192.png +0 -0
  120. package/public/web-app-manifest-512x512.png +0 -0
  121. package/shared.env +0 -0
  122. package/styles/globals.css +125 -0
  123. package/tsconfig.json +41 -0
@@ -0,0 +1,136 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent } from "@/components/ui/card"
4
+ import MetricCard from "./metric-card"
5
+
6
+ // Types
7
+ interface TrendState {
8
+ change: string
9
+ trend: "up" | "down" | "neutral"
10
+ hasData: boolean
11
+ }
12
+
13
+ interface MetricsGridProps {
14
+ stats: {
15
+ memoryRecords: number
16
+ totalClients: number
17
+ successRate: number
18
+ totalRequests: number
19
+ topWriter: { name: string; count: number }
20
+ topReader: { name: string; count: number }
21
+ lastWriter: { name: string; timestamp: number }
22
+ lastReader: { name: string; timestamp: number }
23
+ }
24
+ trends: {
25
+ memory: TrendState
26
+ clients: TrendState
27
+ success: TrendState
28
+ requests: TrendState
29
+ }
30
+ }
31
+
32
+ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
33
+ const formatTimestamp = (timestamp: number) => {
34
+ if (timestamp <= 0) return "No activity"
35
+ const date = new Date(timestamp)
36
+ const now = new Date()
37
+ const isToday = date.getDate() === now.getDate() &&
38
+ date.getMonth() === now.getMonth() &&
39
+ date.getFullYear() === now.getFullYear()
40
+
41
+ if (isToday) {
42
+ return date.toLocaleTimeString()
43
+ }
44
+ return date.toLocaleString()
45
+ }
46
+
47
+ return (
48
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
49
+ {/* 1. Memory Records */}
50
+ <MetricCard
51
+ label="Memory Records"
52
+ value={stats.memoryRecords.toLocaleString()}
53
+ change={trends.memory.change}
54
+ trend={trends.memory.trend}
55
+ hasData={trends.memory.hasData}
56
+ />
57
+
58
+ {/* 2. Total Clients */}
59
+ <MetricCard
60
+ label="Total Clients"
61
+ value={stats.totalClients.toString()}
62
+ change={trends.clients.change}
63
+ trend={trends.clients.trend}
64
+ hasData={trends.clients.hasData}
65
+ />
66
+
67
+ {/* 3. Success Rate */}
68
+ <MetricCard
69
+ label="Success Rate"
70
+ value={`${stats.successRate.toFixed(1)}%`}
71
+ change={trends.success.change}
72
+ trend={trends.success.trend} // Trend UP is good (green) for success rate
73
+ hasData={trends.success.hasData}
74
+ />
75
+
76
+ {/* 4. Total Requests */}
77
+ <MetricCard
78
+ label="Total Requests"
79
+ value={stats.totalRequests.toLocaleString()}
80
+ change={trends.requests.change}
81
+ trend={trends.requests.trend}
82
+ hasData={trends.requests.hasData}
83
+ />
84
+
85
+ {/* 5. Top Writer */}
86
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
87
+ <CardContent className="pt-6 pb-6 relative">
88
+ <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>
90
+ <div className="text-xl text-white/80 whitespace-nowrap">
91
+ {stats.topWriter.count > 0 ? `${stats.topWriter.count.toLocaleString()} writes` : ""}
92
+ </div>
93
+ </CardContent>
94
+ </Card>
95
+
96
+ {/* 6. Top Reader */}
97
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
98
+ <CardContent className="pt-6 pb-6 relative">
99
+ <div className="text-sm font-medium text-slate-400 mb-2">Top Reader</div>
100
+ <div className="text-4xl font-bold text-white mb-1 truncate">
101
+ {stats.topReader.count > 0 ? stats.topReader.name : "N/A"}
102
+ </div>
103
+ <div className="text-xl text-white/80 whitespace-nowrap">
104
+ {stats.topReader.count > 0 ? `${stats.topReader.count.toLocaleString()} reads` : ""}
105
+ </div>
106
+ </CardContent>
107
+ </Card>
108
+
109
+ {/* 7. Last Writer */}
110
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
111
+ <CardContent className="pt-6 pb-6 relative">
112
+ <div className="text-sm font-medium text-slate-400 mb-2">Last Writer</div>
113
+ <div className="text-4xl font-bold text-white mb-1 truncate">
114
+ {stats.lastWriter.name !== "N/A" ? stats.lastWriter.name : "N/A"}
115
+ </div>
116
+ <div className="text-xl text-white/80 whitespace-nowrap">
117
+ {stats.lastWriter.timestamp > 0 ? formatTimestamp(stats.lastWriter.timestamp) : "No activity"}
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+
122
+ {/* 8. Last Reader */}
123
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
124
+ <CardContent className="pt-6 pb-6 relative">
125
+ <div className="text-sm font-medium text-slate-400 mb-2">Last Reader</div>
126
+ <div className="text-4xl font-bold text-white mb-1 truncate">
127
+ {stats.lastReader.name !== "N/A" ? stats.lastReader.name : "N/A"}
128
+ </div>
129
+ <div className="text-xl text-white/80 whitespace-nowrap">
130
+ {stats.lastReader.timestamp > 0 ? formatTimestamp(stats.lastReader.timestamp) : "No activity"}
131
+ </div>
132
+ </CardContent>
133
+ </Card>
134
+ </div>
135
+ )
136
+ }
@@ -0,0 +1,345 @@
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Input } from "@/components/ui/input"
5
+ import { Label } from "@/components/ui/label"
6
+ import { useDashboard } from "@/lib/data/dashboard-context"
7
+ import { Check, Copy, Eye, EyeOff, Key, Save, Server, Settings, Shield, X } from "lucide-react"
8
+ import { useEffect, useState } from "react"
9
+
10
+ export default function SettingsModal({ onClose }: { onClose: () => void }) {
11
+ const [apiKey, setApiKey] = useState("")
12
+ const [endpoint, setEndpoint] = useState("")
13
+ const [adminPassword, setAdminPassword] = useState(localStorage.getItem("adminPassword") || "admin")
14
+ const [isLoading, setIsLoading] = useState(true)
15
+
16
+ const { isDemo, toggleDemo } = useDashboard()
17
+ const [showApiKey, setShowApiKey] = useState(false)
18
+ const [showAdminPassword, setShowAdminPassword] = useState(false)
19
+ /* Demo mode controlled by context now */
20
+
21
+ const [showRegenConfirm, setShowRegenConfirm] = useState(false)
22
+ const [regenInputValue, setRegenInputValue] = useState("")
23
+ const [copiedId, setCopiedId] = useState<string | null>(null)
24
+
25
+ const copyToClipboard = (text: string, id: string) => {
26
+ navigator.clipboard.writeText(text)
27
+ setCopiedId(id)
28
+ setTimeout(() => setCopiedId(null), 2000)
29
+ }
30
+
31
+ // Fetch settings from server
32
+ useEffect(() => {
33
+ // Try to get key from local storage first (simulating persistence)
34
+ const localKey = localStorage.getItem("om_api_key")
35
+ if (localKey) {
36
+ setApiKey(localKey)
37
+ fetch("/api/settings")
38
+ .then(res => res.json())
39
+ .then(data => {
40
+ let srvEndpoint = data.endpoint
41
+ if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
42
+ srvEndpoint = `${window.location.protocol}//${window.location.hostname}:8080`
43
+ }
44
+ setEndpoint(srvEndpoint)
45
+ setIsLoading(false)
46
+ })
47
+ .catch(err => setIsLoading(false))
48
+ } else {
49
+ fetch("/api/settings")
50
+ .then(res => res.json())
51
+ .then(data => {
52
+ setApiKey(data.apiKey !== 'not-set' ? data.apiKey : '')
53
+ let srvEndpoint = data.endpoint
54
+ if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
55
+ srvEndpoint = `${window.location.protocol}//${window.location.hostname}:8080`
56
+ }
57
+ setEndpoint(srvEndpoint)
58
+ setIsLoading(false)
59
+ })
60
+ .catch(err => {
61
+ console.error("Failed to fetch settings:", err)
62
+ setIsLoading(false)
63
+ })
64
+ }
65
+ }, [])
66
+
67
+ const generateApiKey = () => {
68
+ // Legacy direct generation - now just triggers the confirmation flow if button linked here
69
+ // But the button in UI calls setShowRegenConfirm(true) directly.
70
+ // We can keep this empty or redirect.
71
+ }
72
+
73
+ const confirmRegenerate = async () => {
74
+ try {
75
+ const res = await fetch('/api/settings/regenerate', { method: 'POST' })
76
+ if (!res.ok) throw new Error('Failed to regenerate key')
77
+ const data = await res.json()
78
+
79
+ const newKey = data.apiKey
80
+ setApiKey(newKey)
81
+ localStorage.setItem("om_api_key", newKey)
82
+ setShowApiKey(true)
83
+ setShowRegenConfirm(false)
84
+ setRegenInputValue("")
85
+ } catch (e) {
86
+ console.error(e)
87
+ alert("Failed to regenerate key on server.")
88
+ }
89
+ }
90
+
91
+ const [saved, setSaved] = useState(false)
92
+
93
+ const handleSave = () => {
94
+ // Save admin password to localStorage
95
+ localStorage.setItem("adminPassword", adminPassword)
96
+ setSaved(true)
97
+ setTimeout(() => setSaved(false), 2000)
98
+ }
99
+
100
+ return (
101
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
102
+ <div
103
+ className="bg-[#0B1116]/80 backdrop-blur-xl border border-emerald-500/20 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto relative overflow-hidden"
104
+ style={{
105
+ backgroundImage: `
106
+ radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%),
107
+ radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
108
+ radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
109
+ radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
110
+ `
111
+ }}
112
+ >
113
+ {/* Header */}
114
+ <div className="flex items-center justify-between px-6 pt-6 pb-2">
115
+ <div className="flex items-center gap-3">
116
+ <div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
117
+ <Settings className="w-5 h-5 text-white drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
118
+ </div>
119
+ <h2 className="text-xl font-semibold text-white text-shadow-sm">Settings</h2>
120
+ </div>
121
+ <Button
122
+ variant="ghost"
123
+ size="icon"
124
+ onClick={onClose}
125
+ className="text-neutral-400 hover:text-white hover:bg-white/10 rounded-full"
126
+ >
127
+ <X className="w-5 h-5" />
128
+ </Button>
129
+ </div>
130
+
131
+ {/* Content */}
132
+ <div className="p-6 space-y-6">
133
+ {/* Security */}
134
+ <section>
135
+ <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
136
+ <Shield className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
137
+ Security
138
+ </h3>
139
+ <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">
140
+ <div className="space-y-2">
141
+ <Label htmlFor="admin-password" className="text-neutral-200">Admin Password</Label>
142
+ <div className="flex gap-2">
143
+ <div className="relative flex-1">
144
+ <Input
145
+ id="admin-password"
146
+ value={adminPassword}
147
+ onChange={(e) => setAdminPassword(e.target.value)}
148
+ 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"
149
+ type={showAdminPassword ? "text" : "password"}
150
+ />
151
+ <button
152
+ type="button"
153
+ onClick={() => setShowAdminPassword(!showAdminPassword)}
154
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
155
+ >
156
+ {showAdminPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
157
+ </button>
158
+ </div>
159
+ </div>
160
+ <p className="text-xs text-neutral-500">Used to access the dashboard</p>
161
+ </div>
162
+ </div>
163
+ </section>
164
+
165
+ {/* API Configuration */}
166
+ <section>
167
+ <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
168
+ <Key className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
169
+ API Configuration
170
+ </h3>
171
+ <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">
172
+ <div className="space-y-2">
173
+ <Label htmlFor="api-key" className="text-neutral-200">Master API Key</Label>
174
+ <div className="flex gap-2">
175
+ <div className="relative flex-1">
176
+ <Input
177
+ id="api-key"
178
+ value={apiKey || "sk-not-generated-yet"}
179
+ readOnly
180
+ 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"
181
+ type={showApiKey ? "text" : "password"}
182
+ />
183
+ <button
184
+ type="button"
185
+ onClick={() => setShowApiKey(!showApiKey)}
186
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
187
+ >
188
+ {showApiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
189
+ </button>
190
+ </div>
191
+ <Button
192
+ size="icon"
193
+ variant="ghost"
194
+ className="h-10 w-10 border border-white/10 bg-white/5 hover:bg-white/10 text-neutral-400 hover:text-white"
195
+ onClick={() => copyToClipboard(apiKey, "apikey")}
196
+ title="Copy API Key"
197
+ >
198
+ {copiedId === "apikey" ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
199
+ </Button>
200
+ </div>
201
+
202
+ {/* Regeneration Controls */}
203
+ <div className="flex justify-end pt-2">
204
+ {showRegenConfirm ? (
205
+ <div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-4 duration-200">
206
+ <span className="text-xs text-red-400 font-medium">Warning: This will disconnect all existing clients.</span>
207
+ <Input
208
+ value={regenInputValue}
209
+ onChange={(e) => setRegenInputValue(e.target.value)}
210
+ placeholder="Type 'agree'"
211
+ 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"
212
+ />
213
+ <Button
214
+ size="sm"
215
+ variant="ghost"
216
+ className="h-8 px-3 text-neutral-400 hover:text-white hover:bg-white/10"
217
+ onClick={() => {
218
+ setShowRegenConfirm(false)
219
+ setRegenInputValue("")
220
+ }}
221
+ >
222
+ Cancel
223
+ </Button>
224
+ <Button
225
+ size="sm"
226
+ disabled={regenInputValue !== 'agree'}
227
+ 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"
228
+ onClick={confirmRegenerate}
229
+ >
230
+ Confirm
231
+ </Button>
232
+ </div>
233
+ ) : (
234
+ <Button
235
+ size="sm"
236
+ variant="ghost"
237
+ className="h-8 px-2 text-neutral-400 hover:text-white hover:bg-white/10"
238
+ onClick={() => setShowRegenConfirm(true)}
239
+ >
240
+ Regenerate Key
241
+ </Button>
242
+ )}
243
+ </div>
244
+ <p className="text-xs text-neutral-500">Your master secret key for all MCP clients</p>
245
+ </div>
246
+
247
+ <div className="space-y-2">
248
+ <Label htmlFor="endpoint" className="text-neutral-200">Server Endpoint</Label>
249
+ <div className="flex gap-2">
250
+ <Input
251
+ id="endpoint"
252
+ value={endpoint}
253
+ onChange={(e) => setEndpoint(e.target.value)}
254
+ 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"
255
+ />
256
+ </div>
257
+ <p className="text-xs text-neutral-500">URL of the OpenMemory backend instance</p>
258
+ </div>
259
+
260
+ <div className="pt-2 border-t border-white/10">
261
+ <Button
262
+ variant="destructive"
263
+ className="w-full bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20"
264
+ onClick={async () => {
265
+ if(!confirm("Are you sure you want to restart the OpenMemory server?")) return;
266
+ try {
267
+ const res = await fetch('/api/system/restart', { method: 'POST' })
268
+ if(res.ok) alert("Server restarting... Please wait a moment.")
269
+ else alert("Failed to restart server")
270
+ } catch(e) {
271
+ alert("Error triggering restart")
272
+ }
273
+ }}
274
+ >
275
+ Restart Server
276
+ </Button>
277
+ </div>
278
+ </div>
279
+ </section>
280
+
281
+ {/* System Info */}
282
+ <section>
283
+ <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
284
+ <Server className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
285
+ System
286
+ </h3>
287
+ <div className="bg-white/5 border border-white/10 rounded-lg p-5 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
288
+ <div className="grid grid-cols-2 gap-4">
289
+ <div>
290
+ <span className="text-xs uppercase tracking-wider text-neutral-500 font-semibold">Version</span>
291
+ <p className="text-neutral-200 font-mono mt-1">v0.1.0-beta</p>
292
+ </div>
293
+ <div>
294
+ <span className="text-xs uppercase tracking-wider text-neutral-500 font-semibold">Environment</span>
295
+ <p className="text-emerald-400 font-mono mt-1 text-shadow-emerald">Production</p>
296
+ </div>
297
+ </div>
298
+ <div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
299
+ <div>
300
+ <label htmlFor="demo-mode" className="text-neutral-200 font-medium block">Demo Mode</label>
301
+ <p className="text-xs text-neutral-500 mt-1">Generate simulated traffic for demonstration</p>
302
+ </div>
303
+ <div className="relative inline-block w-12 h-6 transition duration-200 ease-in-out rounded-full border border-white/10">
304
+ <input
305
+ type="checkbox"
306
+ id="demo-mode"
307
+ className="peer absolute w-0 h-0 opacity-0"
308
+ checked={isDemo}
309
+ onChange={toggleDemo}
310
+ />
311
+ <label
312
+ htmlFor="demo-mode"
313
+ className={`block w-full h-full rounded-full cursor-pointer transition-colors duration-300 ${isDemo ? "bg-emerald-500/30" : "bg-black/40"}`}
314
+ ></label>
315
+ <div className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-md transform transition-transform duration-300 ${isDemo ? "translate-x-6 bg-emerald-100" : "translate-x-0 bg-neutral-400"}`}></div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </section>
320
+ </div>
321
+
322
+ {/* Footer */}
323
+ <div className="sticky bottom-0 bg-[#0B1116]/80 backdrop-blur-md border-t border-emerald-500/20 px-6 py-4 flex justify-end gap-3 z-10">
324
+ <Button
325
+ onClick={onClose}
326
+ className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
327
+ >
328
+ Cancel
329
+ </Button>
330
+ <Button
331
+ onClick={handleSave}
332
+ className={`border font-medium transition-colors gap-2 ${
333
+ saved
334
+ ? "bg-emerald-500/30 border-emerald-500/30 text-emerald-300"
335
+ : "bg-emerald-500/20 hover:bg-emerald-500/30 border-emerald-500/20 text-emerald-400"
336
+ }`}
337
+ >
338
+ <Save className="w-4 h-4" />
339
+ {saved ? "Saved!" : "Save Changes"}
340
+ </Button>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ )
345
+ }
@@ -0,0 +1,11 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ ThemeProvider as NextThemesProvider,
6
+ type ThemeProviderProps,
7
+ } from 'next-themes'
8
+
9
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as AccordionPrimitive from '@radix-ui/react-accordion'
5
+ import { ChevronDownIcon } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn('border-b last:border-b-0', className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ 'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn('pt-0 pb-4', className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }