@cybermem/dashboard 0.5.10 → 0.5.14

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.
@@ -1,95 +1,226 @@
1
- "use client"
1
+ "use client";
2
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, Server, Settings, Shield, X } from "lucide-react"
8
- import { useEffect, useState } from "react"
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 {
8
+ Check,
9
+ Copy,
10
+ Database,
11
+ Download,
12
+ Eye,
13
+ EyeOff,
14
+ Key,
15
+ Loader2,
16
+ RotateCcw,
17
+ Server,
18
+ Settings,
19
+ Shield,
20
+ Trash2,
21
+ Upload,
22
+ X,
23
+ } from "lucide-react";
24
+ import { useEffect, useState } from "react";
9
25
 
10
26
  export default function SettingsModal({ onClose }: { onClose: () => void }) {
11
- const [apiKey, setApiKey] = useState("")
12
- const [endpoint, setEndpoint] = useState("")
13
- const [isManaged, setIsManaged] = useState(false)
14
- const [adminPassword, setAdminPassword] = useState(localStorage.getItem("adminPassword") || "admin")
15
- const [isLoading, setIsLoading] = useState(true)
27
+ const [apiKey, setApiKey] = useState("");
28
+ const [endpoint, setEndpoint] = useState("");
29
+ const [isManaged, setIsManaged] = useState(false);
30
+ const [adminPassword, setAdminPassword] = useState(
31
+ localStorage.getItem("adminPassword") || "admin",
32
+ );
33
+ const [isLoading, setIsLoading] = useState(true);
16
34
 
17
- const { isDemo, toggleDemo } = useDashboard()
18
- const [showApiKey, setShowApiKey] = useState(false)
19
- const [showAdminPassword, setShowAdminPassword] = useState(false)
35
+ const { isDemo, toggleDemo } = useDashboard();
36
+ const [showApiKey, setShowApiKey] = useState(false);
37
+ const [showAdminPassword, setShowAdminPassword] = useState(false);
20
38
 
21
- const [showRegenConfirm, setShowRegenConfirm] = useState(false)
22
- const [regenInputValue, setRegenInputValue] = useState("")
23
- const [copiedId, setCopiedId] = useState<string | null>(null)
39
+ const [showRegenConfirm, setShowRegenConfirm] = useState(false);
40
+ const [regenInputValue, setRegenInputValue] = useState("");
41
+ const [copiedId, setCopiedId] = useState<string | null>(null);
42
+ const [isBackingUp, setIsBackingUp] = useState(false);
43
+ const [isRestoring, setIsRestoring] = useState(false);
44
+ const [isRestarting, setIsRestarting] = useState(false);
45
+ const [isResetting, setIsResetting] = useState(false);
46
+ const [resetConfirmText, setResetConfirmText] = useState("");
47
+ const [showResetConfirm, setShowResetConfirm] = useState(false);
48
+ const [operationStatus, setOperationStatus] = useState<{
49
+ type: "success" | "error";
50
+ message: string;
51
+ } | null>(null);
24
52
 
25
53
  const copyToClipboard = (text: string, id: string) => {
26
- navigator.clipboard.writeText(text)
27
- setCopiedId(id)
28
- setTimeout(() => setCopiedId(null), 2000)
29
- }
54
+ navigator.clipboard.writeText(text);
55
+ setCopiedId(id);
56
+ setTimeout(() => setCopiedId(null), 2000);
57
+ };
30
58
 
31
59
  // Fetch settings from server
32
60
  useEffect(() => {
33
- setIsLoading(true)
34
- const localKey = localStorage.getItem("om_api_key")
61
+ setIsLoading(true);
62
+ const localKey = localStorage.getItem("om_api_key");
35
63
 
36
64
  fetch("/api/settings")
37
- .then(res => res.json())
38
- .then(data => {
65
+ .then((res) => res.json())
66
+ .then((data) => {
39
67
  // Enforce Local Mode if server says so
40
- setIsManaged(data.isManaged || false)
68
+ setIsManaged(data.isManaged || false);
41
69
 
42
70
  if (localKey && !data.isManaged) {
43
- setApiKey(localKey)
71
+ setApiKey(localKey);
44
72
  } else {
45
- setApiKey(data.apiKey !== 'not-set' ? data.apiKey : '')
73
+ setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
46
74
  }
47
75
 
48
- let srvEndpoint = data.endpoint
49
- if (srvEndpoint.includes('localhost') && typeof window !== "undefined" && !window.location.hostname.includes('localhost')) {
50
- const port = srvEndpoint.split(':').pop()?.split('/')[0] || '8626'
51
- srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}/mcp`
76
+ let srvEndpoint = data.endpoint;
77
+ if (
78
+ srvEndpoint.includes("localhost") &&
79
+ typeof window !== "undefined" &&
80
+ !window.location.hostname.includes("localhost")
81
+ ) {
82
+ const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
83
+ srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}/mcp`;
52
84
  }
53
- setEndpoint(srvEndpoint)
54
- setIsLoading(false)
85
+ setEndpoint(srvEndpoint);
86
+ setIsLoading(false);
55
87
  })
56
- .catch(err => {
57
- console.error("Failed to fetch settings:", err)
58
- setIsLoading(false)
59
- })
60
- }, [])
88
+ .catch((err) => {
89
+ console.error("Failed to fetch settings:", err);
90
+ setIsLoading(false);
91
+ });
92
+ }, []);
61
93
 
62
94
  const confirmRegenerate = async () => {
63
95
  try {
64
- const res = await fetch('/api/settings/regenerate', { method: 'POST' })
65
- if (!res.ok) throw new Error('Failed to regenerate key')
66
- const data = await res.json()
67
-
68
- const newKey = data.apiKey
69
- setApiKey(newKey)
70
- localStorage.setItem("om_api_key", newKey)
71
- setShowApiKey(true)
72
- setShowRegenConfirm(false)
73
- setRegenInputValue("")
96
+ const res = await fetch("/api/settings/regenerate", { method: "POST" });
97
+ if (!res.ok) throw new Error("Failed to regenerate key");
98
+ const data = await res.json();
99
+
100
+ const newKey = data.apiKey;
101
+ setApiKey(newKey);
102
+ localStorage.setItem("om_api_key", newKey);
103
+ setShowApiKey(true);
104
+ setShowRegenConfirm(false);
105
+ setRegenInputValue("");
74
106
  } catch (e) {
75
- console.error(e)
76
- alert("Failed to regenerate key on server.")
107
+ console.error(e);
108
+ alert("Failed to regenerate key on server.");
77
109
  }
78
- }
110
+ };
79
111
 
80
- const [saved, setSaved] = useState(false)
112
+ const [saved, setSaved] = useState(false);
81
113
 
82
114
  const handleSave = () => {
83
- localStorage.setItem("adminPassword", adminPassword)
84
- setSaved(true)
85
- setTimeout(() => setSaved(false), 2000)
86
- }
115
+ localStorage.setItem("adminPassword", adminPassword);
116
+ setSaved(true);
117
+ setTimeout(() => setSaved(false), 2000);
118
+ };
119
+
120
+ const handleBackup = async () => {
121
+ try {
122
+ setIsBackingUp(true);
123
+ const res = await fetch("/api/backup");
124
+ if (!res.ok) throw new Error("Backup failed");
125
+
126
+ const blob = await res.blob();
127
+ const url = window.URL.createObjectURL(blob);
128
+ const a = document.createElement("a");
129
+ a.href = url;
130
+ a.download = `cybermem-backup-${new Date().toISOString().split("T")[0]}.tar.gz`;
131
+ document.body.appendChild(a);
132
+ a.click();
133
+ window.URL.revokeObjectURL(url);
134
+ setOperationStatus({
135
+ type: "success",
136
+ message: "Backup downloaded successfully",
137
+ });
138
+ } catch (err: any) {
139
+ setOperationStatus({ type: "error", message: err.message });
140
+ } finally {
141
+ setIsBackingUp(false);
142
+ }
143
+ };
144
+
145
+ const handleRestore = async (e: React.ChangeEvent<HTMLInputElement>) => {
146
+ const file = e.target.files?.[0];
147
+ if (!file) return;
148
+
149
+ try {
150
+ setIsRestoring(true);
151
+ const formData = new FormData();
152
+ formData.append("backup", file);
153
+
154
+ const res = await fetch("/api/restore", {
155
+ method: "POST",
156
+ body: formData,
157
+ });
158
+ const data = await res.json();
159
+ if (!res.ok) throw new Error(data.error || "Restore failed");
160
+
161
+ setOperationStatus({
162
+ type: "success",
163
+ message: "Database restored. Restart required.",
164
+ });
165
+ } catch (err: any) {
166
+ setOperationStatus({ type: "error", message: err.message });
167
+ } finally {
168
+ setIsRestoring(false);
169
+ // Reset input
170
+ e.target.value = "";
171
+ }
172
+ };
173
+
174
+ const handleRestart = async () => {
175
+ try {
176
+ setIsRestarting(true);
177
+ const res = await fetch("/api/system/restart", { method: "POST" });
178
+ if (!res.ok) throw new Error("Restart failed");
179
+ setOperationStatus({
180
+ type: "success",
181
+ message: "System is restarting...",
182
+ });
183
+ } catch (err: any) {
184
+ setOperationStatus({ type: "error", message: err.message });
185
+ } finally {
186
+ setIsRestarting(false);
187
+ }
188
+ };
189
+
190
+ const handleReset = async () => {
191
+ if (resetConfirmText !== "RESET") return;
192
+
193
+ try {
194
+ setIsResetting(true);
195
+ const res = await fetch("/api/reset", {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify({ confirm: "RESET" }),
199
+ });
200
+ const data = await res.json();
201
+ if (!res.ok) throw new Error(data.error || "Reset failed");
202
+
203
+ setShowResetConfirm(false);
204
+ setResetConfirmText("");
205
+ setOperationStatus({
206
+ type: "success",
207
+ message: "Database wiped successfully.",
208
+ });
209
+ } catch (err: any) {
210
+ setOperationStatus({ type: "error", message: err.message });
211
+ } finally {
212
+ setIsResetting(false);
213
+ }
214
+ };
87
215
 
88
216
  return (
89
217
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
90
- <div 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"
91
- style={{ backgroundImage: `radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)` }}>
92
-
218
+ <div
219
+ 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"
220
+ style={{
221
+ backgroundImage: `radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%), radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)`,
222
+ }}
223
+ >
93
224
  {/* Header */}
94
225
  <div className="flex items-center justify-between px-6 pt-6 pb-2">
95
226
  <div className="flex items-center gap-3">
@@ -98,7 +229,12 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
98
229
  </div>
99
230
  <h2 className="text-xl font-semibold text-white">Settings</h2>
100
231
  </div>
101
- <Button variant="ghost" size="icon" onClick={onClose} className="text-neutral-400 hover:text-white rounded-full">
232
+ <Button
233
+ variant="ghost"
234
+ size="icon"
235
+ onClick={onClose}
236
+ className="text-neutral-400 hover:text-white rounded-full"
237
+ >
102
238
  <X className="w-5 h-5" />
103
239
  </Button>
104
240
  </div>
@@ -115,72 +251,335 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
115
251
  <div className="space-y-2">
116
252
  <Label htmlFor="admin-password">Admin Password</Label>
117
253
  <div className="relative">
118
- <Input id="admin-password" value={adminPassword} onChange={(e) => setAdminPassword(e.target.value)} className="bg-black/40 border-white/10 text-white" type={showAdminPassword ? "text" : "password"} />
119
- <button onClick={() => setShowAdminPassword(!showAdminPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white">
120
- {showAdminPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
254
+ <Input
255
+ id="admin-password"
256
+ value={adminPassword}
257
+ onChange={(e) => setAdminPassword(e.target.value)}
258
+ className="bg-black/40 border-white/10 text-white"
259
+ type={showAdminPassword ? "text" : "password"}
260
+ />
261
+ <button
262
+ onClick={() => setShowAdminPassword(!showAdminPassword)}
263
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
264
+ >
265
+ {showAdminPassword ? (
266
+ <EyeOff className="w-4 h-4" />
267
+ ) : (
268
+ <Eye className="w-4 h-4" />
269
+ )}
121
270
  </button>
122
271
  </div>
123
272
  </div>
273
+
274
+ {/* Authentication Status */}
275
+ <div className="space-y-2 pt-4 border-t border-white/10">
276
+ <Label>Authentication Method</Label>
277
+ <div className="flex items-center gap-3 p-3 bg-black/20 rounded-lg border border-white/5">
278
+ {/* Show different states based on auth method */}
279
+ {isManaged ? (
280
+ <>
281
+ <div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse" />
282
+ <div className="flex-1">
283
+ <p className="text-sm text-emerald-300 font-medium">
284
+ Local Mode
285
+ </p>
286
+ <p className="text-xs text-neutral-500">
287
+ No authentication required for localhost
288
+ </p>
289
+ </div>
290
+ </>
291
+ ) : apiKey ? (
292
+ <>
293
+ <div className="w-2 h-2 bg-yellow-400 rounded-full" />
294
+ <div className="flex-1">
295
+ <p className="text-sm text-yellow-300 font-medium">
296
+ API Key
297
+ </p>
298
+ <p className="text-xs text-neutral-500">
299
+ Using legacy API key authentication
300
+ <span className="text-yellow-500 ml-1">
301
+ (deprecated)
302
+ </span>
303
+ </p>
304
+ </div>
305
+ <a
306
+ href="https://cybermem.dev/auth/signin"
307
+ target="_blank"
308
+ rel="noopener noreferrer"
309
+ className="px-3 py-1.5 text-xs bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
310
+ >
311
+ Upgrade to OAuth
312
+ </a>
313
+ </>
314
+ ) : (
315
+ <>
316
+ <div className="w-2 h-2 bg-neutral-400 rounded-full" />
317
+ <div className="flex-1">
318
+ <p className="text-sm text-neutral-300 font-medium">
319
+ Not Configured
320
+ </p>
321
+ <p className="text-xs text-neutral-500">
322
+ Connect with GitHub for secure access
323
+ </p>
324
+ </div>
325
+ <a
326
+ href="https://cybermem.dev/auth/signin"
327
+ target="_blank"
328
+ rel="noopener noreferrer"
329
+ className="px-3 py-1.5 text-xs bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 rounded-lg text-emerald-400 transition-colors"
330
+ >
331
+ Connect GitHub
332
+ </a>
333
+ </>
334
+ )}
335
+ </div>
336
+ </div>
124
337
  </div>
125
338
  </section>
126
339
 
127
340
  {/* API Configuration */}
128
341
  {!isManaged ? (
129
- <section>
130
- <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
131
- <Key className="w-5 h-5" />
132
- API Configuration
133
- </h3>
134
- <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">
135
- <div className="space-y-2">
136
- <Label htmlFor="api-key">Master API Key</Label>
137
- <div className="flex gap-2">
138
- <div className="relative flex-1">
139
- <Input id="api-key" value={apiKey || "sk-not-generated-yet"} readOnly className="bg-black/40 border-white/10 text-white font-mono" type={showApiKey ? "text" : "password"} />
140
- <button onClick={() => setShowApiKey(!showApiKey)} className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white">
141
- {showApiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
142
- </button>
342
+ <section>
343
+ <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
344
+ <Key className="w-5 h-5" />
345
+ API Configuration
346
+ </h3>
347
+ <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">
348
+ <div className="space-y-2">
349
+ <Label htmlFor="api-key">Master API Key</Label>
350
+ <div className="flex gap-2">
351
+ <div className="relative flex-1">
352
+ <Input
353
+ id="api-key"
354
+ value={apiKey || "sk-not-generated-yet"}
355
+ readOnly
356
+ className="bg-black/40 border-white/10 text-white font-mono"
357
+ type={showApiKey ? "text" : "password"}
358
+ />
359
+ <button
360
+ onClick={() => setShowApiKey(!showApiKey)}
361
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
362
+ >
363
+ {showApiKey ? (
364
+ <EyeOff className="w-4 h-4" />
365
+ ) : (
366
+ <Eye className="w-4 h-4" />
367
+ )}
368
+ </button>
369
+ </div>
370
+ <Button
371
+ size="icon"
372
+ variant="ghost"
373
+ onClick={() => copyToClipboard(apiKey, "apikey")}
374
+ >
375
+ {copiedId === "apikey" ? (
376
+ <Check className="h-4 w-4 text-emerald-400" />
377
+ ) : (
378
+ <Copy className="h-4 w-4" />
379
+ )}
380
+ </Button>
381
+ </div>
382
+ <div className="flex justify-end pt-2">
383
+ {showRegenConfirm ? (
384
+ <div className="flex items-center gap-2">
385
+ <Input
386
+ value={regenInputValue}
387
+ onChange={(e) => setRegenInputValue(e.target.value)}
388
+ placeholder="Type 'agree'"
389
+ className="h-8 w-24 text-xs"
390
+ />
391
+ <Button
392
+ size="sm"
393
+ variant="ghost"
394
+ onClick={() => setShowRegenConfirm(false)}
395
+ >
396
+ Cancel
397
+ </Button>
398
+ <Button
399
+ size="sm"
400
+ disabled={regenInputValue !== "agree"}
401
+ onClick={confirmRegenerate}
402
+ >
403
+ Confirm
404
+ </Button>
405
+ </div>
406
+ ) : (
407
+ <Button
408
+ size="sm"
409
+ variant="ghost"
410
+ onClick={() => setShowRegenConfirm(true)}
411
+ >
412
+ Regenerate Key
413
+ </Button>
414
+ )}
143
415
  </div>
144
- <Button size="icon" variant="ghost" onClick={() => copyToClipboard(apiKey, "apikey")}>
145
- {copiedId === "apikey" ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
146
- </Button>
147
416
  </div>
148
- <div className="flex justify-end pt-2">
149
- {showRegenConfirm ? (
150
- <div className="flex items-center gap-2">
151
- <Input value={regenInputValue} onChange={(e) => setRegenInputValue(e.target.value)} placeholder="Type 'agree'" className="h-8 w-24 text-xs" />
152
- <Button size="sm" variant="ghost" onClick={() => setShowRegenConfirm(false)}>Cancel</Button>
153
- <Button size="sm" disabled={regenInputValue !== 'agree'} onClick={confirmRegenerate}>Confirm</Button>
154
- </div>
155
- ) : (
156
- <Button size="sm" variant="ghost" onClick={() => setShowRegenConfirm(true)}>Regenerate Key</Button>
157
- )}
417
+
418
+ <div className="space-y-2">
419
+ <Label htmlFor="endpoint">Server Endpoint</Label>
420
+ <Input
421
+ id="endpoint"
422
+ value={endpoint}
423
+ onChange={(e) => setEndpoint(e.target.value)}
424
+ className="bg-black/40 border-white/10 text-white"
425
+ />
158
426
  </div>
159
427
  </div>
428
+ </section>
429
+ ) : (
430
+ <section>
431
+ <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
432
+ <Shield className="w-5 h-5 text-emerald-400" />
433
+ API Security
434
+ </h3>
435
+ <div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-5 space-y-2 backdrop-blur-sm">
436
+ <p className="text-sm font-medium text-emerald-300">
437
+ Local Mode Active
438
+ </p>
439
+ <p className="text-xs text-emerald-200/60">
440
+ No API key required for connection from your laptop. Key
441
+ management is hidden.
442
+ </p>
160
443
 
161
- <div className="space-y-2">
162
- <Label htmlFor="endpoint">Server Endpoint</Label>
163
- <Input id="endpoint" value={endpoint} onChange={(e) => setEndpoint(e.target.value)} className="bg-black/40 border-white/10 text-white" />
444
+ <div className="mt-4 pt-4 border-t border-white/10">
445
+ <Label
446
+ htmlFor="endpoint"
447
+ className="text-xs text-neutral-500 mb-2 block"
448
+ >
449
+ System Endpoint
450
+ </Label>
451
+ <Input
452
+ id="endpoint"
453
+ value={endpoint}
454
+ readOnly
455
+ className="h-9 bg-black/40 border-white/10 text-neutral-400 text-sm"
456
+ />
457
+ </div>
164
458
  </div>
165
- </div>
166
- </section>
167
- ) : (
459
+ </section>
460
+ )}
461
+
462
+ {/* Data Management */}
168
463
  <section>
169
464
  <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
170
- <Shield className="w-5 h-5 text-emerald-400" />
171
- API Security
465
+ <Database className="w-5 h-5" />
466
+ Data Management
172
467
  </h3>
173
- <div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-5 space-y-2 backdrop-blur-sm">
174
- <p className="text-sm font-medium text-emerald-300">Local Mode Active</p>
175
- <p className="text-xs text-emerald-200/60">No API key required for connection from your laptop. Key management is hidden.</p>
468
+ <div className="flex flex-col gap-3">
469
+ <div className="flex items-center gap-3">
470
+ <Button
471
+ variant="outline"
472
+ className="flex-1 justify-center bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 text-white h-11 px-6 transition-all"
473
+ onClick={handleBackup}
474
+ disabled={isBackingUp}
475
+ >
476
+ {isBackingUp ? (
477
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
478
+ ) : (
479
+ <Download className="w-4 h-4 mr-2 opacity-70" />
480
+ )}
481
+ <span className="font-medium">Backup</span>
482
+ </Button>
483
+
484
+ <div className="flex-1 relative">
485
+ <input
486
+ type="file"
487
+ id="restore-file"
488
+ className="hidden"
489
+ accept=".tar.gz,.tgz"
490
+ onChange={handleRestore}
491
+ disabled={isRestoring}
492
+ />
493
+ <Button
494
+ variant="outline"
495
+ className="w-full justify-center bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 text-white h-11 px-6 transition-all"
496
+ onClick={() =>
497
+ document.getElementById("restore-file")?.click()
498
+ }
499
+ disabled={isRestoring}
500
+ >
501
+ {isRestoring ? (
502
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
503
+ ) : (
504
+ <Upload className="w-4 h-4 mr-2 opacity-70" />
505
+ )}
506
+ <span className="font-medium">Restore</span>
507
+ </Button>
508
+ </div>
176
509
 
177
- <div className="mt-4 pt-4 border-t border-white/10">
178
- <Label htmlFor="endpoint" className="text-xs text-neutral-500 mb-2 block">System Endpoint</Label>
179
- <Input id="endpoint" value={endpoint} readOnly className="h-9 bg-black/40 border-white/10 text-neutral-400 text-sm" />
510
+ <Button
511
+ variant="outline"
512
+ className="flex-1 justify-center bg-white/5 border-red-500/10 hover:bg-red-500/10 hover:border-red-500/30 text-red-400 h-11 px-6 transition-all"
513
+ onClick={() => setShowResetConfirm(true)}
514
+ disabled={isResetting}
515
+ >
516
+ <Trash2 className="w-4 h-4 mr-2 opacity-70" />
517
+ <span className="font-medium">Reset DB</span>
518
+ </Button>
180
519
  </div>
520
+
521
+ {showResetConfirm && (
522
+ <div className="p-5 bg-red-500/5 border border-red-500/20 rounded-xl space-y-4 shadow-inner">
523
+ <p className="text-xs text-red-400/80 font-bold uppercase tracking-widest text-center">
524
+ Danger Zone: This will permanently delete all memories!
525
+ </p>
526
+ <div className="flex flex-col gap-3">
527
+ <Input
528
+ value={resetConfirmText}
529
+ onChange={(e) => setResetConfirmText(e.target.value)}
530
+ placeholder="Type 'RESET' to confirm"
531
+ className="h-10 bg-black/40 border-red-500/20 text-white placeholder:text-red-500/20 text-center font-mono focus:border-red-500/40"
532
+ />
533
+ <div className="flex gap-2">
534
+ <Button
535
+ className="flex-1 text-neutral-400 hover:text-white hover:bg-white/5"
536
+ variant="ghost"
537
+ onClick={() => {
538
+ setShowResetConfirm(false);
539
+ setResetConfirmText("");
540
+ }}
541
+ >
542
+ Cancel
543
+ </Button>
544
+ <Button
545
+ className="flex-1 bg-red-500/80 hover:bg-red-500 text-white shadow-lg active:scale-[0.98] transition-transform"
546
+ disabled={resetConfirmText !== "RESET" || isResetting}
547
+ onClick={handleReset}
548
+ >
549
+ {isResetting && (
550
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
551
+ )}
552
+ Confirm Reset
553
+ </Button>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ )}
558
+
559
+ {operationStatus && (
560
+ <div
561
+ className={`p-3 rounded-xl text-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-1 ${
562
+ operationStatus.type === "success"
563
+ ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
564
+ : "bg-red-500/10 text-red-400 border border-red-500/20"
565
+ }`}
566
+ >
567
+ {operationStatus.type === "success" ? (
568
+ <Check className="w-4 h-4" />
569
+ ) : (
570
+ <X className="w-4 h-4" />
571
+ )}
572
+ <span className="flex-1">{operationStatus.message}</span>
573
+ <button
574
+ onClick={() => setOperationStatus(null)}
575
+ className="opacity-50 hover:opacity-100 p-1"
576
+ >
577
+ <X className="w-4 h-4" />
578
+ </button>
579
+ </div>
580
+ )}
181
581
  </div>
182
582
  </section>
183
- )}
184
583
 
185
584
  {/* System Info */}
186
585
  <section>
@@ -188,10 +587,40 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
188
587
  <Server className="w-5 h-5" />
189
588
  System
190
589
  </h3>
191
- <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">
192
- <div className="grid grid-cols-2 gap-4">
193
- <div><span className="text-xs uppercase text-neutral-500 font-semibold">Version</span><p className="text-neutral-200 font-mono mt-1">v0.2.0</p></div>
194
- <div><span className="text-xs uppercase text-neutral-500 font-semibold">Environment</span><p className="text-emerald-400 font-mono mt-1">Production</p></div>
590
+ <div className="bg-white/5 border border-white/10 rounded-xl p-6 shadow-[inset_0_0_30px_rgba(255,255,255,0.01)] backdrop-blur-md space-y-6">
591
+ <div className="grid grid-cols-2 gap-8">
592
+ <div>
593
+ <span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em]">
594
+ Version
595
+ </span>
596
+ <p className="text-neutral-200 font-mono text-base mt-2 tracking-tight">
597
+ v0.2.0
598
+ </p>
599
+ </div>
600
+ <div>
601
+ <span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em]">
602
+ Environment
603
+ </span>
604
+ <p className="text-emerald-400 font-mono text-base mt-2 tracking-tight">
605
+ Production
606
+ </p>
607
+ </div>
608
+ </div>
609
+
610
+ <div className="pt-2 border-t border-white/5">
611
+ <Button
612
+ variant="outline"
613
+ className="w-full bg-sky-500/5 border-sky-500/10 hover:bg-sky-500/10 hover:border-sky-500/30 text-sky-400 h-10 transition-all font-medium flex items-center justify-center gap-2 group"
614
+ onClick={handleRestart}
615
+ disabled={isRestarting}
616
+ >
617
+ {isRestarting ? (
618
+ <Loader2 className="w-4 h-4 animate-spin" />
619
+ ) : (
620
+ <RotateCcw className="w-4 h-4 opacity-70 group-hover:rotate-45 transition-transform" />
621
+ )}
622
+ {isRestarting ? "Restarting..." : "Restart Service"}
623
+ </Button>
195
624
  </div>
196
625
  </div>
197
626
  </section>
@@ -199,12 +628,17 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
199
628
 
200
629
  {/* Footer */}
201
630
  <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">
202
- <Button onClick={onClose} variant="ghost">Cancel</Button>
203
- <Button onClick={handleSave} className="bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-400 border border-emerald-500/20">
631
+ <Button onClick={onClose} variant="ghost">
632
+ Cancel
633
+ </Button>
634
+ <Button
635
+ onClick={handleSave}
636
+ className="bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-400 border border-emerald-500/20"
637
+ >
204
638
  {saved ? "Saved!" : "Save Changes"}
205
639
  </Button>
206
640
  </div>
207
641
  </div>
208
642
  </div>
209
- )
643
+ );
210
644
  }