@cybermem/dashboard 0.8.5 → 0.8.7
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/environment/route.ts +70 -0
- package/app/api/settings/route.ts +53 -13
- package/app/layout.tsx +2 -0
- package/app/page.tsx +2 -37
- package/components/dashboard/login-modal.tsx +108 -36
- package/components/dashboard/mcp-config-modal.tsx +17 -18
- package/components/dashboard/settings-modal.tsx +142 -239
- package/components/ui/confirmation-modal.tsx +116 -0
- package/components/ui/tint-button.tsx +91 -0
- package/package.json +1 -1
- package/components/dashboard/password-alert-modal.tsx +0 -72
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment detection API for dynamic MCP URL configuration
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. Tailscale hostname (if TAILSCALE_HOSTNAME env var is set)
|
|
8
|
+
* 2. LAN .local domain (if raspberrypi.local or similar)
|
|
9
|
+
* 3. VPS public URL (if CYBERMEM_PUBLIC_URL env var is set)
|
|
10
|
+
* 4. localhost:8626 (fallback)
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(request: Request) {
|
|
13
|
+
const MCP_PORT = process.env.MCP_PORT || "8626";
|
|
14
|
+
|
|
15
|
+
// Check for Tailscale hostname first (highest priority for remote access)
|
|
16
|
+
const tailscaleHostname = process.env.TAILSCALE_HOSTNAME;
|
|
17
|
+
if (tailscaleHostname) {
|
|
18
|
+
return NextResponse.json({
|
|
19
|
+
url: `https://${tailscaleHostname}`,
|
|
20
|
+
type: "tailscale",
|
|
21
|
+
editable: false,
|
|
22
|
+
hint: "Using Tailscale Funnel for secure remote access",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for VPS public URL
|
|
27
|
+
const publicUrl = process.env.CYBERMEM_PUBLIC_URL;
|
|
28
|
+
if (publicUrl) {
|
|
29
|
+
return NextResponse.json({
|
|
30
|
+
url: publicUrl,
|
|
31
|
+
type: "vps",
|
|
32
|
+
editable: true,
|
|
33
|
+
hint: "Configure CYBERMEM_PUBLIC_URL to change this URL",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Detect from request headers (for LAN access)
|
|
38
|
+
const host = request.headers.get("host") || "";
|
|
39
|
+
|
|
40
|
+
// Check if accessing via .local domain (Raspberry Pi LAN)
|
|
41
|
+
if (host.includes(".local")) {
|
|
42
|
+
const hostname = host.split(":")[0];
|
|
43
|
+
return NextResponse.json({
|
|
44
|
+
url: `http://${hostname}:${MCP_PORT}`,
|
|
45
|
+
type: "lan",
|
|
46
|
+
editable: false,
|
|
47
|
+
hint: "Detected LAN access via mDNS (.local)",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if accessing via IP address (likely LAN or VPS)
|
|
52
|
+
const ipMatch = host.match(/^(\d+\.\d+\.\d+\.\d+)/);
|
|
53
|
+
if (ipMatch) {
|
|
54
|
+
const protocol = request.headers.get("x-forwarded-proto") || "http";
|
|
55
|
+
return NextResponse.json({
|
|
56
|
+
url: `${protocol}://${ipMatch[1]}:${MCP_PORT}`,
|
|
57
|
+
type: "ip",
|
|
58
|
+
editable: true,
|
|
59
|
+
hint: "Detected IP-based access",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Default to localhost
|
|
64
|
+
return NextResponse.json({
|
|
65
|
+
url: `http://localhost:${MCP_PORT}`,
|
|
66
|
+
type: "local",
|
|
67
|
+
editable: false,
|
|
68
|
+
hint: "Running locally",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -6,6 +6,51 @@ export const dynamic = "force-dynamic";
|
|
|
6
6
|
|
|
7
7
|
const CONFIG_PATH = "/data/config.json";
|
|
8
8
|
|
|
9
|
+
// Detect the correct MCP endpoint based on request host
|
|
10
|
+
function getMcpEndpoint(request: NextRequest): {
|
|
11
|
+
endpoint: string;
|
|
12
|
+
isLocal: boolean;
|
|
13
|
+
} {
|
|
14
|
+
const host = request.headers.get("host") || "localhost:3000";
|
|
15
|
+
const hostname = host.split(":")[0];
|
|
16
|
+
|
|
17
|
+
// Priority 1: Explicit env var
|
|
18
|
+
if (process.env.CYBERMEM_URL) {
|
|
19
|
+
return { endpoint: process.env.CYBERMEM_URL, isLocal: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Priority 2: Tailscale domain (from env or detect)
|
|
23
|
+
const tailscaleDomain = process.env.TAILSCALE_DOMAIN;
|
|
24
|
+
|
|
25
|
+
// Priority 3: Based on request host
|
|
26
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
27
|
+
// Local development
|
|
28
|
+
return { endpoint: "http://localhost:8626/mcp", isLocal: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hostname.endsWith(".local")) {
|
|
32
|
+
// LAN access (raspberrypi.local)
|
|
33
|
+
// If Tailscale domain is available, prefer it for remote config
|
|
34
|
+
if (tailscaleDomain) {
|
|
35
|
+
return {
|
|
36
|
+
endpoint: `https://${tailscaleDomain}/cybermem/mcp`,
|
|
37
|
+
isLocal: false,
|
|
38
|
+
// Also provide LAN endpoint as fallback
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { endpoint: `http://${hostname}:8626/mcp`, isLocal: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (hostname.includes(".ts.net")) {
|
|
45
|
+
// Tailscale Funnel access
|
|
46
|
+
return { endpoint: `https://${hostname}/cybermem/mcp`, isLocal: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback: use request host
|
|
50
|
+
const protocol = request.headers.get("x-forwarded-proto") || "http";
|
|
51
|
+
return { endpoint: `${protocol}://${hostname}:8626/mcp`, isLocal: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
9
54
|
export async function GET(request: NextRequest) {
|
|
10
55
|
// Rate limiting check
|
|
11
56
|
const rateLimit = checkRateLimit(request);
|
|
@@ -32,17 +77,11 @@ export async function GET(request: NextRequest) {
|
|
|
32
77
|
// ignore
|
|
33
78
|
}
|
|
34
79
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
// isManaged = Local Mode (No Auth). Only if NO URL and NO API KEY.
|
|
41
|
-
// If API Key is present (RPi), we are in "Secure/Legacy" mode, not Local.
|
|
42
|
-
// In local development, rawEndpoint might be unset, but we still want to not be "managed" if we want to test auth flows.
|
|
43
|
-
const isManaged =
|
|
44
|
-
!rawEndpoint &&
|
|
45
|
-
(!process.env.OM_API_KEY || process.env.OM_API_KEY === "dev-secret-key");
|
|
80
|
+
// Get dynamic endpoint based on request host
|
|
81
|
+
const { endpoint, isLocal } = getMcpEndpoint(request);
|
|
82
|
+
|
|
83
|
+
// isManaged = Local Mode (localhost auto-login)
|
|
84
|
+
const isManaged = isLocal;
|
|
46
85
|
|
|
47
86
|
return NextResponse.json(
|
|
48
87
|
{
|
|
@@ -50,8 +89,9 @@ export async function GET(request: NextRequest) {
|
|
|
50
89
|
apiKey: apiKey,
|
|
51
90
|
endpoint,
|
|
52
91
|
isManaged,
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
isLocal,
|
|
93
|
+
dashboardVersion: "v0.8.0",
|
|
94
|
+
mcpVersion: "v0.8.0",
|
|
55
95
|
},
|
|
56
96
|
{
|
|
57
97
|
headers: {
|
package/app/layout.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Analytics } from "@vercel/analytics/next";
|
|
|
3
3
|
import type { Metadata } from "next";
|
|
4
4
|
import { Exo_2, Geist, Geist_Mono } from "next/font/google";
|
|
5
5
|
import type React from "react";
|
|
6
|
+
import { Toaster } from "sonner";
|
|
6
7
|
import "./globals.css";
|
|
7
8
|
|
|
8
9
|
const _geist = Geist({ subsets: ["latin"] });
|
|
@@ -38,6 +39,7 @@ export default async function RootLayout({
|
|
|
38
39
|
<DashboardProvider initialAuth={initialAuth}>
|
|
39
40
|
{children}
|
|
40
41
|
</DashboardProvider>
|
|
42
|
+
<Toaster richColors theme="dark" position="bottom-right" />
|
|
41
43
|
<Analytics />
|
|
42
44
|
</body>
|
|
43
45
|
</html>
|
package/app/page.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import DashboardHeader from "@/components/dashboard/header";
|
|
|
6
6
|
import LoginModal from "@/components/dashboard/login-modal";
|
|
7
7
|
import MCPConfigModal from "@/components/dashboard/mcp-config-modal";
|
|
8
8
|
import MetricsGrid from "@/components/dashboard/metrics-grid";
|
|
9
|
-
import PasswordAlertModal from "@/components/dashboard/password-alert-modal";
|
|
10
9
|
import SettingsModal from "@/components/dashboard/settings-modal";
|
|
11
10
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
12
11
|
import { DashboardData } from "@/lib/data/types";
|
|
@@ -27,8 +26,6 @@ export default function Dashboard() {
|
|
|
27
26
|
|
|
28
27
|
const [showMCPConfig, setShowMCPConfig] = useState(false);
|
|
29
28
|
const [showSettings, setShowSettings] = useState(false);
|
|
30
|
-
const [showPasswordAlert, setShowPasswordAlert] = useState(false);
|
|
31
|
-
const [focusPasswordField, setFocusPasswordField] = useState(false);
|
|
32
29
|
|
|
33
30
|
// Data State
|
|
34
31
|
const [data, setData] = useState<DashboardData>({
|
|
@@ -53,27 +50,8 @@ export default function Dashboard() {
|
|
|
53
50
|
|
|
54
51
|
// Auth is handled by context (Layout passes initial state from headers)
|
|
55
52
|
|
|
56
|
-
const handleLogin = (
|
|
53
|
+
const handleLogin = (token: string) => {
|
|
57
54
|
login();
|
|
58
|
-
|
|
59
|
-
// Check if using default password and warning not dismissed
|
|
60
|
-
const hasCustomPassword = localStorage.getItem("adminPassword") !== null;
|
|
61
|
-
const warningDismissed =
|
|
62
|
-
localStorage.getItem("hidePasswordWarning") === "true";
|
|
63
|
-
|
|
64
|
-
if (!hasCustomPassword && !warningDismissed) {
|
|
65
|
-
setShowPasswordAlert(true);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const handlePasswordAlertChange = () => {
|
|
70
|
-
setShowPasswordAlert(false);
|
|
71
|
-
setFocusPasswordField(true);
|
|
72
|
-
setShowSettings(true);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const handlePasswordAlertDismiss = () => {
|
|
76
|
-
setShowPasswordAlert(false);
|
|
77
55
|
};
|
|
78
56
|
|
|
79
57
|
// Fetch Data Effect - Reacts to strategy change or refresh signal
|
|
@@ -183,20 +161,7 @@ export default function Dashboard() {
|
|
|
183
161
|
{showMCPConfig && (
|
|
184
162
|
<MCPConfigModal onClose={() => setShowMCPConfig(false)} />
|
|
185
163
|
)}
|
|
186
|
-
{showSettings && (
|
|
187
|
-
<SettingsModal
|
|
188
|
-
onClose={() => {
|
|
189
|
-
setShowSettings(false);
|
|
190
|
-
setFocusPasswordField(false);
|
|
191
|
-
}}
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
194
|
-
{showPasswordAlert && (
|
|
195
|
-
<PasswordAlertModal
|
|
196
|
-
onChangePassword={handlePasswordAlertChange}
|
|
197
|
-
onDismiss={handlePasswordAlertDismiss}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
164
|
+
{showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
|
|
200
165
|
</div>
|
|
201
166
|
);
|
|
202
167
|
}
|
|
@@ -1,32 +1,62 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Label } from "@/components/ui/label";
|
|
5
|
+
import { TintButton } from "@/components/ui/tint-button";
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipTrigger,
|
|
10
|
+
} from "@/components/ui/tooltip";
|
|
11
|
+
import { HelpCircle, Key, LogIn } from "lucide-react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
8
13
|
|
|
9
14
|
interface LoginModalProps {
|
|
10
|
-
onLogin: (
|
|
15
|
+
onLogin: (token: string) => void;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export default function LoginModal({ onLogin }: LoginModalProps) {
|
|
14
|
-
const [
|
|
15
|
-
const [error, setError] = useState("")
|
|
19
|
+
const [token, setToken] = useState("");
|
|
20
|
+
const [error, setError] = useState("");
|
|
21
|
+
const [isLocal, setIsLocal] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Auto-login on localhost
|
|
25
|
+
const hostname = window.location.hostname;
|
|
26
|
+
const local =
|
|
27
|
+
hostname === "localhost" ||
|
|
28
|
+
hostname === "127.0.0.1" ||
|
|
29
|
+
hostname.includes("raspberrypi.local") ||
|
|
30
|
+
hostname.endsWith(".local");
|
|
31
|
+
setIsLocal(local);
|
|
32
|
+
|
|
33
|
+
if (local) {
|
|
34
|
+
// Auto-login for local access
|
|
35
|
+
onLogin("local-bypass");
|
|
36
|
+
}
|
|
37
|
+
}, [onLogin]);
|
|
16
38
|
|
|
17
39
|
const handleSubmit = (e: React.FormEvent) => {
|
|
18
|
-
e.preventDefault()
|
|
40
|
+
e.preventDefault();
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
if (!token.trim()) {
|
|
43
|
+
setError("Please enter your API token");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
22
46
|
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} else {
|
|
27
|
-
setError("Incorrect password")
|
|
28
|
-
setPassword("")
|
|
47
|
+
if (!token.startsWith("sk-")) {
|
|
48
|
+
setError("Token should start with 'sk-'");
|
|
49
|
+
return;
|
|
29
50
|
}
|
|
51
|
+
|
|
52
|
+
// For remote access, use the token
|
|
53
|
+
onLogin(token);
|
|
54
|
+
setError("");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Don't show modal for local access (auto-login)
|
|
58
|
+
if (isLocal) {
|
|
59
|
+
return null;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
return (
|
|
@@ -39,45 +69,87 @@ export default function LoginModal({ onLogin }: LoginModalProps) {
|
|
|
39
69
|
radial-gradient(circle at 100% 0%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%),
|
|
40
70
|
radial-gradient(circle at 100% 100%, oklch(0.65 0 0 / 0.05) 0%, transparent 50%),
|
|
41
71
|
radial-gradient(circle at 0% 100%, oklch(0.6 0 0 / 0.05) 0%, transparent 50%)
|
|
42
|
-
|
|
72
|
+
`,
|
|
43
73
|
}}
|
|
44
74
|
>
|
|
45
75
|
{/* Header */}
|
|
46
76
|
<div className="px-6 pt-8 pb-6 text-center">
|
|
47
77
|
<div className="inline-flex p-3 bg-emerald-500/10 rounded-full border border-emerald-500/20 mb-4">
|
|
48
|
-
<
|
|
78
|
+
<Key className="w-8 h-8 text-emerald-400 drop-shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
|
49
79
|
</div>
|
|
50
|
-
<h2 className="text-2xl font-bold text-white text-shadow-sm mb-2">
|
|
51
|
-
|
|
80
|
+
<h2 className="text-2xl font-bold text-white text-shadow-sm mb-2">
|
|
81
|
+
CyberMem Dashboard
|
|
82
|
+
</h2>
|
|
83
|
+
<p className="text-neutral-400 text-sm">
|
|
84
|
+
Remote access requires API token authentication
|
|
85
|
+
</p>
|
|
52
86
|
</div>
|
|
53
87
|
|
|
54
88
|
{/* Content */}
|
|
55
89
|
<form onSubmit={handleSubmit} className="px-6 pb-6 space-y-4">
|
|
56
90
|
<div className="space-y-2">
|
|
57
|
-
<
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<Label htmlFor="token" className="text-neutral-200">
|
|
93
|
+
API Token
|
|
94
|
+
</Label>
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<TooltipTrigger asChild>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
className="text-neutral-500 hover:text-emerald-400 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
<HelpCircle className="w-4 h-4" />
|
|
102
|
+
</button>
|
|
103
|
+
</TooltipTrigger>
|
|
104
|
+
<TooltipContent className="max-w-xs bg-neutral-900 border-neutral-700 text-neutral-200">
|
|
105
|
+
<p className="text-xs">
|
|
106
|
+
Find your token in{" "}
|
|
107
|
+
<code className="text-emerald-400">~/.cybermem/.env</code>{" "}
|
|
108
|
+
or check the terminal output from{" "}
|
|
109
|
+
<code className="text-emerald-400">cybermem up</code>
|
|
110
|
+
</p>
|
|
111
|
+
</TooltipContent>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
</div>
|
|
58
114
|
<Input
|
|
59
|
-
id="
|
|
115
|
+
id="token"
|
|
60
116
|
type="password"
|
|
61
|
-
value={
|
|
62
|
-
onChange={(e) =>
|
|
63
|
-
placeholder="
|
|
64
|
-
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"
|
|
117
|
+
value={token}
|
|
118
|
+
onChange={(e) => setToken(e.target.value)}
|
|
119
|
+
placeholder="sk-xxxxxxxxxxxxxxxx"
|
|
120
|
+
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 font-mono"
|
|
65
121
|
autoFocus
|
|
66
122
|
/>
|
|
67
|
-
{error &&
|
|
68
|
-
<p className="text-red-400 text-sm mt-2">{error}</p>
|
|
69
|
-
)}
|
|
123
|
+
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
|
|
70
124
|
</div>
|
|
71
125
|
|
|
72
|
-
<
|
|
126
|
+
<TintButton
|
|
73
127
|
type="submit"
|
|
74
|
-
|
|
128
|
+
tint="emerald"
|
|
129
|
+
variant="solid"
|
|
130
|
+
className="w-full h-11"
|
|
75
131
|
>
|
|
76
132
|
<LogIn className="w-4 h-4" />
|
|
77
|
-
Login
|
|
78
|
-
</
|
|
133
|
+
Login with Token
|
|
134
|
+
</TintButton>
|
|
135
|
+
|
|
136
|
+
<div className="text-xs text-neutral-500 text-center pt-2 space-y-1">
|
|
137
|
+
<p>
|
|
138
|
+
<strong className="text-neutral-400">
|
|
139
|
+
Where to find your token:
|
|
140
|
+
</strong>
|
|
141
|
+
</p>
|
|
142
|
+
<p>
|
|
143
|
+
1. Open Dashboard from{" "}
|
|
144
|
+
<code className="text-emerald-400/80">localhost:3000</code> or{" "}
|
|
145
|
+
<code className="text-emerald-400/80">
|
|
146
|
+
raspberrypi.local:3000
|
|
147
|
+
</code>
|
|
148
|
+
</p>
|
|
149
|
+
<p>2. Go to Settings → Access Token → Copy</p>
|
|
150
|
+
</div>
|
|
79
151
|
</form>
|
|
80
152
|
</div>
|
|
81
153
|
</div>
|
|
82
|
-
)
|
|
154
|
+
);
|
|
83
155
|
}
|
|
@@ -5,17 +5,18 @@ 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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
Check,
|
|
9
|
+
Copy,
|
|
10
|
+
Eye,
|
|
11
|
+
EyeOff,
|
|
12
|
+
FileCode,
|
|
13
|
+
Info,
|
|
14
|
+
Monitor,
|
|
15
|
+
X,
|
|
16
16
|
} from "lucide-react";
|
|
17
17
|
import Image from "next/image";
|
|
18
18
|
import { useEffect, useState } from "react";
|
|
19
|
+
import { toast } from "sonner";
|
|
19
20
|
|
|
20
21
|
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
21
22
|
const { clientConfigs } = useDashboard();
|
|
@@ -93,9 +94,14 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
93
94
|
setIsKeyVisible(true);
|
|
94
95
|
setShowRegenConfirm(false);
|
|
95
96
|
setRegenInputValue("");
|
|
97
|
+
toast.success("Token Regenerated!", {
|
|
98
|
+
description: "All existing client connections will need to be updated.",
|
|
99
|
+
});
|
|
96
100
|
} catch (e) {
|
|
97
101
|
console.error(e);
|
|
98
|
-
|
|
102
|
+
toast.error("Failed to regenerate token", {
|
|
103
|
+
description: "Please check if the server is running.",
|
|
104
|
+
});
|
|
99
105
|
}
|
|
100
106
|
};
|
|
101
107
|
|
|
@@ -112,7 +118,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
// Remote mode: use stdio with --url and --token
|
|
121
|
+
// Remote mode: use stdio with --url and --token
|
|
116
122
|
return {
|
|
117
123
|
mcpServers: {
|
|
118
124
|
cybermem: {
|
|
@@ -165,14 +171,6 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
165
171
|
|
|
166
172
|
// Default to JSON config
|
|
167
173
|
const jsonConfig = getMcpConfig(selectedClient);
|
|
168
|
-
if (!isManaged && maskKey) {
|
|
169
|
-
// Mask the token in args array
|
|
170
|
-
const args = (jsonConfig.mcpServers.cybermem as any).args;
|
|
171
|
-
const tokenIdx = args.indexOf("--token");
|
|
172
|
-
if (tokenIdx !== -1 && args[tokenIdx + 1]) {
|
|
173
|
-
args[tokenIdx + 1] = displayKey;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
174
|
return JSON.stringify(jsonConfig, null, 2);
|
|
177
175
|
};
|
|
178
176
|
|
|
@@ -180,6 +178,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
180
178
|
navigator.clipboard.writeText(text);
|
|
181
179
|
setCopiedId(id);
|
|
182
180
|
setTimeout(() => setCopiedId(null), 2000);
|
|
181
|
+
toast.success("Copied to clipboard!");
|
|
183
182
|
};
|
|
184
183
|
|
|
185
184
|
const highlightJSON = (obj: any) => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { ConfirmationModal } from "@/components/ui/confirmation-modal";
|
|
4
5
|
import { Input } from "@/components/ui/input";
|
|
5
6
|
import { Label } from "@/components/ui/label";
|
|
7
|
+
import { TintButton } from "@/components/ui/tint-button";
|
|
6
8
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
7
9
|
import {
|
|
8
10
|
Check,
|
|
@@ -11,7 +13,6 @@ import {
|
|
|
11
13
|
Download,
|
|
12
14
|
Eye,
|
|
13
15
|
EyeOff,
|
|
14
|
-
Key,
|
|
15
16
|
Loader2,
|
|
16
17
|
RotateCcw,
|
|
17
18
|
Server,
|
|
@@ -22,20 +23,17 @@ import {
|
|
|
22
23
|
X,
|
|
23
24
|
} from "lucide-react";
|
|
24
25
|
import { useEffect, useState } from "react";
|
|
26
|
+
import { toast } from "sonner";
|
|
25
27
|
|
|
26
28
|
export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
27
29
|
const [apiKey, setApiKey] = useState("");
|
|
28
30
|
const [endpoint, setEndpoint] = useState("");
|
|
29
31
|
const [isManaged, setIsManaged] = useState(false);
|
|
30
|
-
const [adminPassword, setAdminPassword] = useState(
|
|
31
|
-
localStorage.getItem("adminPassword") || "admin",
|
|
32
|
-
);
|
|
33
32
|
const [settings, setSettings] = useState<any>(null);
|
|
34
33
|
const [isLoading, setIsLoading] = useState(true);
|
|
35
34
|
|
|
36
35
|
const { isDemo, toggleDemo } = useDashboard();
|
|
37
36
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
38
|
-
const [showAdminPassword, setShowAdminPassword] = useState(false);
|
|
39
37
|
|
|
40
38
|
const [showRegenConfirm, setShowRegenConfirm] = useState(false);
|
|
41
39
|
const [regenInputValue, setRegenInputValue] = useState("");
|
|
@@ -55,6 +53,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
55
53
|
navigator.clipboard.writeText(text);
|
|
56
54
|
setCopiedId(id);
|
|
57
55
|
setTimeout(() => setCopiedId(null), 2000);
|
|
56
|
+
toast.success("Copied to clipboard!");
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
// Fetch settings from server
|
|
@@ -105,16 +104,19 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
105
104
|
setShowApiKey(true);
|
|
106
105
|
setShowRegenConfirm(false);
|
|
107
106
|
setRegenInputValue("");
|
|
107
|
+
toast.success("Token Regenerated!", {
|
|
108
|
+
description: "All connected clients will need to be updated.",
|
|
109
|
+
});
|
|
108
110
|
} catch (e) {
|
|
109
111
|
console.error(e);
|
|
110
|
-
|
|
112
|
+
toast.error("Failed to regenerate token");
|
|
111
113
|
}
|
|
112
114
|
};
|
|
113
115
|
|
|
114
116
|
const [saved, setSaved] = useState(false);
|
|
115
117
|
|
|
116
118
|
const handleSave = () => {
|
|
117
|
-
|
|
119
|
+
// Settings saved
|
|
118
120
|
setSaved(true);
|
|
119
121
|
setTimeout(() => setSaved(false), 2000);
|
|
120
122
|
};
|
|
@@ -243,92 +245,90 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
243
245
|
|
|
244
246
|
{/* Content */}
|
|
245
247
|
<div className="p-6 space-y-6">
|
|
246
|
-
{/*
|
|
248
|
+
{/* Access Token */}
|
|
247
249
|
<section>
|
|
248
250
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
249
251
|
<Shield className="w-5 h-5" />
|
|
250
|
-
|
|
252
|
+
Access Token
|
|
251
253
|
</h3>
|
|
252
254
|
<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">
|
|
255
|
+
{/* Token Display with inline regenerate */}
|
|
253
256
|
<div className="space-y-2">
|
|
254
|
-
<Label htmlFor="
|
|
255
|
-
<div className="
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
<Label htmlFor="access-token">Your Access Token</Label>
|
|
258
|
+
<div className="flex gap-2">
|
|
259
|
+
<div className="relative flex-1">
|
|
260
|
+
<Input
|
|
261
|
+
id="access-token"
|
|
262
|
+
value={apiKey || "Token not generated yet"}
|
|
263
|
+
readOnly
|
|
264
|
+
className="bg-black/40 border-white/10 text-white font-mono text-sm pr-10"
|
|
265
|
+
type={showApiKey ? "text" : "password"}
|
|
266
|
+
/>
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
269
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
|
270
|
+
>
|
|
271
|
+
{showApiKey ? (
|
|
272
|
+
<EyeOff className="w-4 h-4" />
|
|
273
|
+
) : (
|
|
274
|
+
<Eye className="w-4 h-4" />
|
|
275
|
+
)}
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
<TintButton
|
|
279
|
+
tint="neutral"
|
|
280
|
+
variant="ghost"
|
|
281
|
+
size="icon"
|
|
282
|
+
onClick={() => copyToClipboard(apiKey, "accesstoken")}
|
|
283
|
+
title="Copy token"
|
|
266
284
|
>
|
|
267
|
-
{
|
|
268
|
-
<
|
|
285
|
+
{copiedId === "accesstoken" ? (
|
|
286
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
269
287
|
) : (
|
|
270
|
-
<
|
|
288
|
+
<Copy className="h-4 w-4" />
|
|
271
289
|
)}
|
|
272
|
-
</
|
|
290
|
+
</TintButton>
|
|
291
|
+
<TintButton
|
|
292
|
+
tint="yellow"
|
|
293
|
+
variant="ghost"
|
|
294
|
+
size="icon"
|
|
295
|
+
onClick={() => setShowRegenConfirm(true)}
|
|
296
|
+
title="Regenerate token"
|
|
297
|
+
>
|
|
298
|
+
<RotateCcw className="w-4 h-4" />
|
|
299
|
+
</TintButton>
|
|
273
300
|
</div>
|
|
301
|
+
<p className="text-xs text-neutral-500">
|
|
302
|
+
Use this token to connect MCP clients from other devices
|
|
303
|
+
</p>
|
|
274
304
|
</div>
|
|
275
305
|
|
|
276
|
-
{/*
|
|
277
|
-
<div className="
|
|
278
|
-
<Label>Authentication Method</Label>
|
|
306
|
+
{/* Auth Status */}
|
|
307
|
+
<div className="pt-4 border-t border-white/10">
|
|
279
308
|
<div className="flex items-center gap-3 p-3 bg-black/20 rounded-lg border border-white/5">
|
|
280
|
-
{/* Show different states based on auth method */}
|
|
281
309
|
{isManaged ? (
|
|
282
310
|
<>
|
|
283
311
|
<div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse" />
|
|
284
312
|
<div className="flex-1">
|
|
285
313
|
<p className="text-sm text-emerald-300 font-medium">
|
|
286
|
-
Local Mode
|
|
314
|
+
Local Mode Active
|
|
287
315
|
</p>
|
|
288
316
|
<p className="text-xs text-neutral-500">
|
|
289
|
-
|
|
317
|
+
No token needed for local connections
|
|
290
318
|
</p>
|
|
291
319
|
</div>
|
|
292
320
|
</>
|
|
293
|
-
) :
|
|
321
|
+
) : (
|
|
294
322
|
<>
|
|
295
323
|
<div className="w-2 h-2 bg-yellow-400 rounded-full" />
|
|
296
324
|
<div className="flex-1">
|
|
297
325
|
<p className="text-sm text-yellow-300 font-medium">
|
|
298
|
-
|
|
326
|
+
Remote Mode
|
|
299
327
|
</p>
|
|
300
328
|
<p className="text-xs text-neutral-500">
|
|
301
|
-
|
|
329
|
+
Token required for MCP client connections
|
|
302
330
|
</p>
|
|
303
331
|
</div>
|
|
304
|
-
<a
|
|
305
|
-
href="https://cybermem.dev/auth/signin"
|
|
306
|
-
target="_blank"
|
|
307
|
-
rel="noopener noreferrer"
|
|
308
|
-
className="px-3 py-1.5 text-xs bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
|
|
309
|
-
>
|
|
310
|
-
Upgrade to OAuth
|
|
311
|
-
</a>
|
|
312
|
-
</>
|
|
313
|
-
) : (
|
|
314
|
-
<>
|
|
315
|
-
<div className="w-2 h-2 bg-neutral-400 rounded-full" />
|
|
316
|
-
<div className="flex-1">
|
|
317
|
-
<p className="text-sm text-neutral-300 font-medium">
|
|
318
|
-
Not Configured
|
|
319
|
-
</p>
|
|
320
|
-
<p className="text-xs text-neutral-500">
|
|
321
|
-
Connect with GitHub for secure access
|
|
322
|
-
</p>
|
|
323
|
-
</div>
|
|
324
|
-
<a
|
|
325
|
-
href="https://cybermem.dev/auth/signin"
|
|
326
|
-
target="_blank"
|
|
327
|
-
rel="noopener noreferrer"
|
|
328
|
-
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"
|
|
329
|
-
>
|
|
330
|
-
Connect GitHub
|
|
331
|
-
</a>
|
|
332
332
|
</>
|
|
333
333
|
)}
|
|
334
334
|
</div>
|
|
@@ -336,98 +336,6 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
336
336
|
</div>
|
|
337
337
|
</section>
|
|
338
338
|
|
|
339
|
-
{/* API Configuration */}
|
|
340
|
-
{!isManaged && (
|
|
341
|
-
<section>
|
|
342
|
-
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
343
|
-
<Key className="w-5 h-5" />
|
|
344
|
-
Token Configuration
|
|
345
|
-
</h3>
|
|
346
|
-
<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">
|
|
347
|
-
<div className="space-y-2">
|
|
348
|
-
<Label htmlFor="api-key">Security Token</Label>
|
|
349
|
-
<div className="flex gap-2">
|
|
350
|
-
<div className="relative flex-1">
|
|
351
|
-
<Input
|
|
352
|
-
id="api-key"
|
|
353
|
-
value={apiKey || "not-generated-yet"}
|
|
354
|
-
readOnly
|
|
355
|
-
className="bg-black/40 border-white/10 text-white font-mono"
|
|
356
|
-
type={showApiKey ? "text" : "password"}
|
|
357
|
-
/>
|
|
358
|
-
<button
|
|
359
|
-
onClick={() => setShowApiKey(!showApiKey)}
|
|
360
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
|
361
|
-
>
|
|
362
|
-
{showApiKey ? (
|
|
363
|
-
<EyeOff className="w-4 h-4" />
|
|
364
|
-
) : (
|
|
365
|
-
<Eye className="w-4 h-4" />
|
|
366
|
-
)}
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
<Button
|
|
370
|
-
size="icon"
|
|
371
|
-
variant="ghost"
|
|
372
|
-
onClick={() => copyToClipboard(apiKey, "apikey")}
|
|
373
|
-
>
|
|
374
|
-
{copiedId === "apikey" ? (
|
|
375
|
-
<Check className="h-4 w-4 text-emerald-400" />
|
|
376
|
-
) : (
|
|
377
|
-
<Copy className="h-4 w-4" />
|
|
378
|
-
)}
|
|
379
|
-
</Button>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
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
|
-
)}
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
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
|
-
/>
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
</section>
|
|
429
|
-
)}
|
|
430
|
-
|
|
431
339
|
{/* Data Management */}
|
|
432
340
|
<section>
|
|
433
341
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
@@ -436,19 +344,20 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
436
344
|
</h3>
|
|
437
345
|
<div className="flex flex-col gap-3">
|
|
438
346
|
<div className="flex items-center gap-3">
|
|
439
|
-
<
|
|
440
|
-
|
|
441
|
-
|
|
347
|
+
<TintButton
|
|
348
|
+
tint="neutral"
|
|
349
|
+
variant="solid"
|
|
350
|
+
className="flex-1 h-11"
|
|
442
351
|
onClick={handleBackup}
|
|
443
352
|
disabled={isBackingUp}
|
|
444
353
|
>
|
|
445
354
|
{isBackingUp ? (
|
|
446
|
-
<Loader2 className="w-4 h-4
|
|
355
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
447
356
|
) : (
|
|
448
|
-
<Download className="w-4 h-4
|
|
357
|
+
<Download className="w-4 h-4" />
|
|
449
358
|
)}
|
|
450
|
-
|
|
451
|
-
</
|
|
359
|
+
Backup
|
|
360
|
+
</TintButton>
|
|
452
361
|
|
|
453
362
|
<div className="flex-1 relative">
|
|
454
363
|
<input
|
|
@@ -459,72 +368,36 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
459
368
|
onChange={handleRestore}
|
|
460
369
|
disabled={isRestoring}
|
|
461
370
|
/>
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
371
|
+
<TintButton
|
|
372
|
+
tint="neutral"
|
|
373
|
+
variant="solid"
|
|
374
|
+
className="w-full h-11"
|
|
465
375
|
onClick={() =>
|
|
466
376
|
document.getElementById("restore-file")?.click()
|
|
467
377
|
}
|
|
468
378
|
disabled={isRestoring}
|
|
469
379
|
>
|
|
470
380
|
{isRestoring ? (
|
|
471
|
-
<Loader2 className="w-4 h-4
|
|
381
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
472
382
|
) : (
|
|
473
|
-
<Upload className="w-4 h-4
|
|
383
|
+
<Upload className="w-4 h-4" />
|
|
474
384
|
)}
|
|
475
|
-
|
|
476
|
-
</
|
|
385
|
+
Restore
|
|
386
|
+
</TintButton>
|
|
477
387
|
</div>
|
|
478
388
|
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
389
|
+
<TintButton
|
|
390
|
+
tint="red"
|
|
391
|
+
variant="solid"
|
|
392
|
+
className="flex-1 h-11"
|
|
482
393
|
onClick={() => setShowResetConfirm(true)}
|
|
483
394
|
disabled={isResetting}
|
|
484
395
|
>
|
|
485
|
-
<Trash2 className="w-4 h-4
|
|
486
|
-
|
|
487
|
-
</
|
|
396
|
+
<Trash2 className="w-4 h-4" />
|
|
397
|
+
Reset DB
|
|
398
|
+
</TintButton>
|
|
488
399
|
</div>
|
|
489
400
|
|
|
490
|
-
{showResetConfirm && (
|
|
491
|
-
<div className="p-5 bg-red-500/5 border border-red-500/20 rounded-xl space-y-4 shadow-inner">
|
|
492
|
-
<p className="text-xs text-red-400/80 font-bold uppercase tracking-widest text-center">
|
|
493
|
-
Danger Zone: This will permanently delete all memories!
|
|
494
|
-
</p>
|
|
495
|
-
<div className="flex flex-col gap-3">
|
|
496
|
-
<Input
|
|
497
|
-
value={resetConfirmText}
|
|
498
|
-
onChange={(e) => setResetConfirmText(e.target.value)}
|
|
499
|
-
placeholder="Type 'RESET' to confirm"
|
|
500
|
-
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"
|
|
501
|
-
/>
|
|
502
|
-
<div className="flex gap-2">
|
|
503
|
-
<Button
|
|
504
|
-
className="flex-1 text-neutral-400 hover:text-white hover:bg-white/5"
|
|
505
|
-
variant="ghost"
|
|
506
|
-
onClick={() => {
|
|
507
|
-
setShowResetConfirm(false);
|
|
508
|
-
setResetConfirmText("");
|
|
509
|
-
}}
|
|
510
|
-
>
|
|
511
|
-
Cancel
|
|
512
|
-
</Button>
|
|
513
|
-
<Button
|
|
514
|
-
className="flex-1 bg-red-500/80 hover:bg-red-500 text-white shadow-lg active:scale-[0.98] transition-transform"
|
|
515
|
-
disabled={resetConfirmText !== "RESET" || isResetting}
|
|
516
|
-
onClick={handleReset}
|
|
517
|
-
>
|
|
518
|
-
{isResetting && (
|
|
519
|
-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
520
|
-
)}
|
|
521
|
-
Confirm Reset
|
|
522
|
-
</Button>
|
|
523
|
-
</div>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
526
|
-
)}
|
|
527
|
-
|
|
528
401
|
{operationStatus && (
|
|
529
402
|
<div
|
|
530
403
|
className={`p-3 rounded-xl text-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-1 ${
|
|
@@ -585,33 +458,42 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
585
458
|
<span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
|
|
586
459
|
Environment
|
|
587
460
|
</span>
|
|
588
|
-
<div className="
|
|
589
|
-
<
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
461
|
+
<div className="space-y-3">
|
|
462
|
+
<div className="flex justify-between items-center">
|
|
463
|
+
<span className="text-xs text-neutral-400">Status</span>
|
|
464
|
+
<code className="text-[13px] font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
|
465
|
+
Production
|
|
466
|
+
</code>
|
|
467
|
+
</div>
|
|
468
|
+
<div className="flex justify-between items-center">
|
|
469
|
+
<span className="text-xs text-neutral-400">Instance</span>
|
|
470
|
+
<code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10">
|
|
471
|
+
{settings?.isLocal
|
|
472
|
+
? "Local"
|
|
473
|
+
: settings?.isManaged
|
|
474
|
+
? "RPi"
|
|
475
|
+
: "VPS"}
|
|
476
|
+
</code>
|
|
477
|
+
</div>
|
|
597
478
|
</div>
|
|
598
479
|
</div>
|
|
599
480
|
</div>
|
|
600
481
|
|
|
601
482
|
<div className="pt-2 border-t border-white/5">
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
483
|
+
<TintButton
|
|
484
|
+
tint="sky"
|
|
485
|
+
variant="solid"
|
|
486
|
+
className="w-full h-10"
|
|
605
487
|
onClick={handleRestart}
|
|
606
488
|
disabled={isRestarting}
|
|
607
489
|
>
|
|
608
490
|
{isRestarting ? (
|
|
609
491
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
610
492
|
) : (
|
|
611
|
-
<RotateCcw className="w-4 h-4
|
|
493
|
+
<RotateCcw className="w-4 h-4" />
|
|
612
494
|
)}
|
|
613
495
|
{isRestarting ? "Restarting..." : "Restart Service"}
|
|
614
|
-
</
|
|
496
|
+
</TintButton>
|
|
615
497
|
</div>
|
|
616
498
|
</div>
|
|
617
499
|
</section>
|
|
@@ -619,21 +501,42 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
619
501
|
|
|
620
502
|
{/* Footer */}
|
|
621
503
|
<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">
|
|
622
|
-
<
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
className="text-neutral-400 hover:text-white hover:bg-white/5"
|
|
626
|
-
>
|
|
627
|
-
Cancel
|
|
628
|
-
</Button>
|
|
629
|
-
<Button
|
|
630
|
-
onClick={handleSave}
|
|
631
|
-
className="bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-400 border border-emerald-500/20"
|
|
632
|
-
>
|
|
633
|
-
{saved ? "Saved!" : "Save Changes"}
|
|
634
|
-
</Button>
|
|
504
|
+
<TintButton tint="neutral" variant="ghost" onClick={onClose}>
|
|
505
|
+
Close
|
|
506
|
+
</TintButton>
|
|
635
507
|
</div>
|
|
636
508
|
</div>
|
|
509
|
+
|
|
510
|
+
{/* Regenerate Token Confirmation Modal */}
|
|
511
|
+
<ConfirmationModal
|
|
512
|
+
isOpen={showRegenConfirm}
|
|
513
|
+
onClose={() => {
|
|
514
|
+
setShowRegenConfirm(false);
|
|
515
|
+
setRegenInputValue("");
|
|
516
|
+
}}
|
|
517
|
+
onConfirm={confirmRegenerate}
|
|
518
|
+
title="Regenerate Access Token"
|
|
519
|
+
description="This will invalidate your current token. All connected MCP clients will need to be reconfigured with the new token."
|
|
520
|
+
confirmText="Regenerate"
|
|
521
|
+
confirmWord="confirm"
|
|
522
|
+
tint="yellow"
|
|
523
|
+
/>
|
|
524
|
+
|
|
525
|
+
{/* Reset Database Confirmation Modal */}
|
|
526
|
+
<ConfirmationModal
|
|
527
|
+
isOpen={showResetConfirm}
|
|
528
|
+
onClose={() => {
|
|
529
|
+
setShowResetConfirm(false);
|
|
530
|
+
setResetConfirmText("");
|
|
531
|
+
}}
|
|
532
|
+
onConfirm={handleReset}
|
|
533
|
+
title="Reset Database"
|
|
534
|
+
description="This will permanently delete ALL memories and cannot be undone. Make sure you have a backup!"
|
|
535
|
+
confirmText="Reset Database"
|
|
536
|
+
confirmWord="RESET"
|
|
537
|
+
tint="red"
|
|
538
|
+
isLoading={isResetting}
|
|
539
|
+
/>
|
|
637
540
|
</div>
|
|
638
541
|
);
|
|
639
542
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { TintButton } from "@/components/ui/tint-button";
|
|
5
|
+
import { AlertTriangle, X } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
interface ConfirmationModalProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
confirmText?: string;
|
|
15
|
+
confirmWord?: string;
|
|
16
|
+
tint?: "red" | "yellow";
|
|
17
|
+
isLoading?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ConfirmationModal({
|
|
21
|
+
isOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
onConfirm,
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
confirmText = "Confirm",
|
|
27
|
+
confirmWord,
|
|
28
|
+
tint = "red",
|
|
29
|
+
isLoading = false,
|
|
30
|
+
}: ConfirmationModalProps) {
|
|
31
|
+
const [inputValue, setInputValue] = useState("");
|
|
32
|
+
|
|
33
|
+
if (!isOpen) return null;
|
|
34
|
+
|
|
35
|
+
const canConfirm = confirmWord ? inputValue === confirmWord : true;
|
|
36
|
+
|
|
37
|
+
const handleConfirm = () => {
|
|
38
|
+
if (canConfirm) {
|
|
39
|
+
onConfirm();
|
|
40
|
+
setInputValue("");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleClose = () => {
|
|
45
|
+
setInputValue("");
|
|
46
|
+
onClose();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
|
51
|
+
{/* Backdrop */}
|
|
52
|
+
<div
|
|
53
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
54
|
+
onClick={handleClose}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Modal */}
|
|
58
|
+
<div className="relative bg-[#0B1116]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl max-w-md w-full mx-4 overflow-hidden">
|
|
59
|
+
{/* Header */}
|
|
60
|
+
<div className="flex items-start gap-4 p-5">
|
|
61
|
+
<div
|
|
62
|
+
className={`p-2 rounded-lg ${tint === "red" ? "bg-red-500/10" : "bg-yellow-500/10"}`}
|
|
63
|
+
>
|
|
64
|
+
<AlertTriangle
|
|
65
|
+
className={`w-5 h-5 ${tint === "red" ? "text-red-400" : "text-yellow-400"}`}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex-1">
|
|
69
|
+
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
|
70
|
+
<p className="text-sm text-neutral-400 mt-1">{description}</p>
|
|
71
|
+
</div>
|
|
72
|
+
<button
|
|
73
|
+
onClick={handleClose}
|
|
74
|
+
className="text-neutral-400 hover:text-white transition-colors"
|
|
75
|
+
>
|
|
76
|
+
<X className="w-5 h-5" />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Confirmation Input */}
|
|
81
|
+
{confirmWord && (
|
|
82
|
+
<div className="px-5 pb-4">
|
|
83
|
+
<Input
|
|
84
|
+
value={inputValue}
|
|
85
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
86
|
+
placeholder={`Type "${confirmWord}" to confirm`}
|
|
87
|
+
className="bg-black/40 border-white/10 text-white text-center font-mono placeholder:text-neutral-600"
|
|
88
|
+
autoFocus
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Actions */}
|
|
94
|
+
<div className="flex gap-3 p-5 pt-0">
|
|
95
|
+
<TintButton
|
|
96
|
+
tint="neutral"
|
|
97
|
+
variant="outline"
|
|
98
|
+
className="flex-1"
|
|
99
|
+
onClick={handleClose}
|
|
100
|
+
>
|
|
101
|
+
Cancel
|
|
102
|
+
</TintButton>
|
|
103
|
+
<TintButton
|
|
104
|
+
tint={tint}
|
|
105
|
+
variant="solid"
|
|
106
|
+
className="flex-1"
|
|
107
|
+
onClick={handleConfirm}
|
|
108
|
+
disabled={!canConfirm || isLoading}
|
|
109
|
+
>
|
|
110
|
+
{isLoading ? "Processing..." : confirmText}
|
|
111
|
+
</TintButton>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
type TintColor = "emerald" | "yellow" | "red" | "sky" | "neutral";
|
|
7
|
+
|
|
8
|
+
interface TintButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9
|
+
tint?: TintColor;
|
|
10
|
+
variant?: "solid" | "outline" | "ghost";
|
|
11
|
+
size?: "sm" | "md" | "lg" | "icon";
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tintStyles: Record<TintColor, Record<string, string>> = {
|
|
16
|
+
emerald: {
|
|
17
|
+
solid:
|
|
18
|
+
"bg-emerald-500/20 hover:bg-emerald-500/30 border-emerald-500/20 text-emerald-400",
|
|
19
|
+
outline:
|
|
20
|
+
"bg-transparent hover:bg-emerald-500/10 border-emerald-500/20 text-emerald-400",
|
|
21
|
+
ghost:
|
|
22
|
+
"bg-transparent hover:bg-emerald-500/10 border-transparent text-emerald-400",
|
|
23
|
+
},
|
|
24
|
+
yellow: {
|
|
25
|
+
solid:
|
|
26
|
+
"bg-yellow-500/20 hover:bg-yellow-500/30 border-yellow-500/20 text-yellow-400",
|
|
27
|
+
outline:
|
|
28
|
+
"bg-transparent hover:bg-yellow-500/10 border-yellow-500/20 text-yellow-400",
|
|
29
|
+
ghost:
|
|
30
|
+
"bg-transparent hover:bg-yellow-500/10 border-transparent text-yellow-400",
|
|
31
|
+
},
|
|
32
|
+
red: {
|
|
33
|
+
solid: "bg-red-500/20 hover:bg-red-500/30 border-red-500/20 text-red-400",
|
|
34
|
+
outline:
|
|
35
|
+
"bg-transparent hover:bg-red-500/10 border-red-500/20 text-red-400",
|
|
36
|
+
ghost: "bg-transparent hover:bg-red-500/10 border-transparent text-red-400",
|
|
37
|
+
},
|
|
38
|
+
sky: {
|
|
39
|
+
solid: "bg-sky-500/20 hover:bg-sky-500/30 border-sky-500/20 text-sky-400",
|
|
40
|
+
outline:
|
|
41
|
+
"bg-transparent hover:bg-sky-500/10 border-sky-500/20 text-sky-400",
|
|
42
|
+
ghost: "bg-transparent hover:bg-sky-500/10 border-transparent text-sky-400",
|
|
43
|
+
},
|
|
44
|
+
neutral: {
|
|
45
|
+
solid: "bg-white/10 hover:bg-white/20 border-white/10 text-white",
|
|
46
|
+
outline: "bg-transparent hover:bg-white/5 border-white/10 text-neutral-300",
|
|
47
|
+
ghost:
|
|
48
|
+
"bg-transparent hover:bg-white/5 border-transparent text-neutral-400 hover:text-white",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizeStyles = {
|
|
53
|
+
sm: "h-8 px-3 text-xs",
|
|
54
|
+
md: "h-10 px-4 text-sm",
|
|
55
|
+
lg: "h-11 px-6 text-base",
|
|
56
|
+
icon: "h-9 w-9 p-0",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const TintButton = forwardRef<HTMLButtonElement, TintButtonProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
tint = "emerald",
|
|
63
|
+
variant = "solid",
|
|
64
|
+
size = "md",
|
|
65
|
+
className,
|
|
66
|
+
children,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref,
|
|
70
|
+
) => {
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
"inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-all",
|
|
76
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0B1116]",
|
|
77
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
78
|
+
"[&_svg]:shrink-0",
|
|
79
|
+
tintStyles[tint][variant],
|
|
80
|
+
sizeStyles[size],
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
TintButton.displayName = "TintButton";
|
package/package.json
CHANGED
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
}
|