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