@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.
- package/app/api/health/route.ts +60 -46
- package/components/dashboard/audit-log-table.tsx +209 -130
- package/components/dashboard/chart-card.tsx +151 -76
- package/components/dashboard/header.tsx +131 -50
- package/components/dashboard/mcp-config-modal.tsx +1 -1
- package/components/dashboard/metric-card.tsx +46 -10
- package/components/dashboard/metrics-chart.tsx +137 -110
- package/components/dashboard/metrics-grid.tsx +138 -73
- package/components/dashboard/settings-modal.tsx +548 -114
- package/e2e/ui-elements.spec.ts +267 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
8
|
-
|
|
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(
|
|
15
|
-
|
|
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
|
-
|
|
71
|
+
setApiKey(localKey);
|
|
44
72
|
} else {
|
|
45
|
-
|
|
73
|
+
setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
let srvEndpoint = data.endpoint
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
91
|
-
|
|
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
|
|
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
|
|
119
|
-
|
|
120
|
-
{
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
</
|
|
166
|
-
|
|
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
|
-
<
|
|
171
|
-
|
|
465
|
+
<Database className="w-5 h-5" />
|
|
466
|
+
Data Management
|
|
172
467
|
</h3>
|
|
173
|
-
<div className="
|
|
174
|
-
<
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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-
|
|
192
|
-
<div className="grid grid-cols-2 gap-
|
|
193
|
-
<div
|
|
194
|
-
|
|
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">
|
|
203
|
-
|
|
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
|
}
|