@cybermem/dashboard 0.1.0 โ†’ 0.5.1

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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @cybermem/dashboard
2
+
3
+ Monitoring dashboard for CyberMem.
4
+
5
+ ๐ŸŒ [cybermem.dev](https://cybermem.dev) ยท ๐Ÿ“– [Docs](https://docs.cybermem.dev) ยท ๐Ÿ“ฆ [GitHub](https://github.com/mikhailkogan17/cybermem)
@@ -2,6 +2,9 @@ import { NextResponse } from 'next/server'
2
2
 
3
3
  export const dynamic = 'force-dynamic'
4
4
 
5
+ // Use env var for db-exporter URL (Docker internal vs local dev)
6
+ const DB_EXPORTER_URL = process.env.DB_EXPORTER_URL || 'http://localhost:8000'
7
+
5
8
  const CLIENTS = ["Claude Code", "v0", "Cursor", "GitHub Copilot", "Windsurf"]
6
9
  const OPERATIONS = ["Read", "Write", "Update", "Delete", "Create"]
7
10
  const STATUSES = ["Success", "Success", "Success", "Warning", "Error"]
@@ -18,7 +21,7 @@ export async function GET(request: Request) {
18
21
  const controller = new AbortController()
19
22
  const timeoutId = setTimeout(() => controller.abort(), 2000)
20
23
 
21
- const res = await fetch('http://db-exporter:8000/api/logs?limit=100', {
24
+ const res = await fetch(`${DB_EXPORTER_URL}/api/logs?limit=100`, {
22
25
  signal: controller.signal,
23
26
  cache: 'no-store'
24
27
  })
@@ -0,0 +1,94 @@
1
+ import { checkRateLimit, rateLimitResponse } from '@/lib/rate-limit'
2
+ import { NextRequest, NextResponse } from 'next/server'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ interface ServiceStatus {
7
+ name: string
8
+ status: 'ok' | 'error' | 'warning'
9
+ message?: string
10
+ latencyMs?: number
11
+ }
12
+
13
+ interface SystemHealth {
14
+ overall: 'ok' | 'degraded' | 'error'
15
+ services: ServiceStatus[]
16
+ timestamp: string
17
+ }
18
+
19
+ async function checkService(name: string, url: string, timeout = 3000): Promise<ServiceStatus> {
20
+ const start = Date.now()
21
+ try {
22
+ const controller = new AbortController()
23
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
24
+
25
+ const res = await fetch(url, {
26
+ signal: controller.signal,
27
+ cache: 'no-store',
28
+ headers: {
29
+ 'X-Client-Name': 'CyberMem-Dashboard'
30
+ }
31
+ })
32
+ clearTimeout(timeoutId)
33
+
34
+ const latencyMs = Date.now() - start
35
+
36
+ if (res.ok) {
37
+ return { name, status: 'ok', latencyMs }
38
+ }
39
+ return {
40
+ name,
41
+ status: 'warning',
42
+ message: `HTTP ${res.status}`,
43
+ latencyMs
44
+ }
45
+ } catch (error: any) {
46
+ return {
47
+ name,
48
+ status: 'error',
49
+ message: error.name === 'AbortError' ? 'Timeout' : (error.message || 'Connection failed')
50
+ }
51
+ }
52
+ }
53
+
54
+ export async function GET(request: NextRequest) {
55
+ // Rate limiting
56
+ const rateLimit = checkRateLimit(request)
57
+ if (!rateLimit.allowed) {
58
+ return rateLimitResponse(rateLimit.resetIn)
59
+ }
60
+
61
+ // Use environment variables with sensible defaults for local Docker stack
62
+ const prometheusUrl = process.env.PROMETHEUS_URL || 'http://localhost:9092'
63
+ const openMemoryUrl = process.env.CYBERMEM_URL || 'http://localhost:8626'
64
+ const vectorUrl = process.env.VECTOR_URL // Vector is optional
65
+
66
+ const checks: Promise<ServiceStatus>[] = [
67
+ checkService('OpenMemory API', `${openMemoryUrl}/health`),
68
+ checkService('Prometheus', `${prometheusUrl}/-/ready`),
69
+ ]
70
+
71
+ // Only check Vector if configured
72
+ if (vectorUrl) {
73
+ checks.push(checkService('Vector', `${vectorUrl}/health`))
74
+ }
75
+
76
+ const services = await Promise.all(checks)
77
+
78
+ // Determine overall status
79
+ const hasError = services.some(s => s.status === 'error')
80
+ const hasWarning = services.some(s => s.status === 'warning')
81
+
82
+ const health: SystemHealth = {
83
+ overall: hasError ? 'error' : hasWarning ? 'degraded' : 'ok',
84
+ services,
85
+ timestamp: new Date().toISOString()
86
+ }
87
+
88
+ return NextResponse.json(health, {
89
+ headers: {
90
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
91
+ 'X-RateLimit-Remaining': String(rateLimit.remaining)
92
+ }
93
+ })
94
+ }
@@ -1,18 +1,47 @@
1
+ import { checkRateLimit, rateLimitResponse } from '@/lib/rate-limit'
1
2
  import crypto from 'crypto'
2
3
  import fs from 'fs'
3
- import { NextResponse } from 'next/server'
4
+ import { NextRequest, NextResponse } from 'next/server'
4
5
 
5
6
  export const dynamic = 'force-dynamic'
6
7
 
7
- export async function POST(req: Request) {
8
+ export async function POST(req: NextRequest) {
9
+ // Rate limiting check
10
+ const rateLimit = checkRateLimit(req);
11
+ if (!rateLimit.allowed) {
12
+ return rateLimitResponse(rateLimit.resetIn);
13
+ }
14
+
8
15
  try {
9
16
  const apiKey = `sk-${crypto.randomBytes(16).toString('hex')}`
10
17
  const sharedEnvPath = '/app/shared.env'
18
+ const configPath = '/data/config.json'
11
19
 
12
20
  // Write OM_API_KEY=... to shared file mounted at /.env in OpenMemory
13
21
  fs.writeFileSync(sharedEnvPath, `OM_API_KEY=${apiKey}\n`)
14
22
 
15
- return NextResponse.json({ success: true, apiKey })
23
+ // Also persist to config.json
24
+ let config: Record<string, any> = {}
25
+ try {
26
+ if (fs.existsSync(configPath)) {
27
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
28
+ }
29
+ } catch {}
30
+ config.api_key = apiKey
31
+ fs.mkdirSync('/data', { recursive: true })
32
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
33
+
34
+ // Create response with HttpOnly cookie
35
+ const response = NextResponse.json({ success: true, apiKey })
36
+ response.cookies.set('cybermem_api_key', apiKey, {
37
+ httpOnly: true,
38
+ secure: process.env.NODE_ENV === 'production',
39
+ sameSite: 'strict',
40
+ path: '/',
41
+ maxAge: 60 * 60 * 24 * 365 // 1 year
42
+ })
43
+
44
+ return response
16
45
  } catch (error) {
17
46
  console.error('[Settings] Failed to regenerate API Key:', error)
18
47
  return NextResponse.json({ error: 'Failed to regenerate API Key' }, { status: 500 })
@@ -1,25 +1,51 @@
1
+ import { checkRateLimit, rateLimitResponse } from '@/lib/rate-limit'
1
2
  import fs from 'fs'
2
- import { NextResponse } from 'next/server'
3
+ import { NextRequest, NextResponse } from 'next/server'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
7
  const CONFIG_PATH = '/data/config.json'
7
8
 
8
- export async function GET() {
9
+ export async function GET(request: NextRequest) {
10
+ // Rate limiting check
11
+ const rateLimit = checkRateLimit(request);
12
+ if (!rateLimit.allowed) {
13
+ return rateLimitResponse(rateLimit.resetIn);
14
+ }
15
+
9
16
  let apiKey = process.env.OM_API_KEY || 'not-set'
10
17
 
18
+ // Try to read from HttpOnly cookie first
19
+ const cookieKey = request.cookies.get('cybermem_api_key')?.value
20
+ if (cookieKey) {
21
+ apiKey = cookieKey
22
+ }
23
+
24
+ // Fallback to config file
11
25
  try {
12
26
  if (fs.existsSync(CONFIG_PATH)) {
13
27
  const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
14
28
  const conf = JSON.parse(raw)
15
- if (conf.api_key) apiKey = conf.api_key
29
+ if (conf.api_key && apiKey === 'not-set') apiKey = conf.api_key
16
30
  }
17
31
  } catch (e) {
18
32
  // ignore
19
33
  }
20
34
 
35
+ // Endpoint resolution:
36
+ // 1. Env var CYBERMEM_URL
37
+ // 2. Default to localhost:8088/memory (Managed Mode)
38
+ const rawEndpoint = process.env.CYBERMEM_URL;
39
+ const endpoint = rawEndpoint || 'http://localhost:8088/memory';
40
+ const isManaged = !rawEndpoint;
41
+
21
42
  return NextResponse.json({
22
43
  apiKey: apiKey,
23
- endpoint: process.env.CYBERMEM_URL || 'http://localhost:8080'
44
+ endpoint,
45
+ isManaged
46
+ }, {
47
+ headers: {
48
+ 'X-RateLimit-Remaining': String(rateLimit.remaining)
49
+ }
24
50
  })
25
51
  }
@@ -0,0 +1,45 @@
1
+ "use client"
2
+
3
+ import LoginModal from "@/components/dashboard/login-modal"
4
+ import MCPConfigModal from "@/components/dashboard/mcp-config-modal"
5
+ import { useRouter } from "next/navigation"
6
+ import { useEffect, useState } from "react"
7
+
8
+ export default function ClientConnectPage() {
9
+ const router = useRouter()
10
+ const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
11
+
12
+ // Check auth on mount
13
+ useEffect(() => {
14
+ const auth = sessionStorage.getItem("authenticated")
15
+ setIsAuthenticated(auth === "true")
16
+ }, [])
17
+
18
+ const handleLogin = () => {
19
+ sessionStorage.setItem("authenticated", "true")
20
+ setIsAuthenticated(true)
21
+ // Mark that we came from /client-connect for first-run experience
22
+ sessionStorage.setItem("fromClientConnect", "true")
23
+ }
24
+
25
+ const handleClose = () => {
26
+ router.push("/")
27
+ }
28
+
29
+ // Loading state
30
+ if (isAuthenticated === null) {
31
+ return <div className="min-h-screen bg-[#0a0a0a]" />
32
+ }
33
+
34
+ // Not authenticated - show login
35
+ if (!isAuthenticated) {
36
+ return <LoginModal onLogin={handleLogin} />
37
+ }
38
+
39
+ // Authenticated - show MCP modal
40
+ return (
41
+ <div className="min-h-screen bg-[#0a0a0a]">
42
+ <MCPConfigModal onClose={handleClose} />
43
+ </div>
44
+ )
45
+ }
package/app/page.tsx CHANGED
@@ -6,6 +6,7 @@ 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"
9
10
  import SettingsModal from "@/components/dashboard/settings-modal"
10
11
  import { useDashboard } from "@/lib/data/dashboard-context"
11
12
  import { DashboardData } from "@/lib/data/types"
@@ -19,6 +20,8 @@ export default function Dashboard() {
19
20
 
20
21
  const [showMCPConfig, setShowMCPConfig] = useState(false)
21
22
  const [showSettings, setShowSettings] = useState(false)
23
+ const [showPasswordAlert, setShowPasswordAlert] = useState(false)
24
+ const [focusPasswordField, setFocusPasswordField] = useState(false)
22
25
  const [isAuthenticated, setIsAuthenticated] = useState(false)
23
26
 
24
27
  // Data State
@@ -53,6 +56,24 @@ export default function Dashboard() {
53
56
  const handleLogin = (password: string) => {
54
57
  sessionStorage.setItem("authenticated", "true")
55
58
  setIsAuthenticated(true)
59
+
60
+ // Check if using default password and warning not dismissed
61
+ const hasCustomPassword = localStorage.getItem("adminPassword") !== null
62
+ const warningDismissed = 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)
56
77
  }
57
78
 
58
79
  // Fetch Data Effect - Reacts to strategy change or refresh signal
@@ -144,7 +165,13 @@ export default function Dashboard() {
144
165
  </main>
145
166
 
146
167
  {showMCPConfig && <MCPConfigModal onClose={() => setShowMCPConfig(false)} />}
147
- {showSettings && <SettingsModal onClose={() => { setShowSettings(false); }} />}
168
+ {showSettings && <SettingsModal onClose={() => { setShowSettings(false); setFocusPasswordField(false); }} />}
169
+ {showPasswordAlert && (
170
+ <PasswordAlertModal
171
+ onChangePassword={handlePasswordAlertChange}
172
+ onDismiss={handlePasswordAlertDismiss}
173
+ />
174
+ )}
148
175
  </div>
149
176
  )
150
177
  }
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, RefreshCw } from "lucide-react"
3
+ import { useDashboard } from "@/lib/data/dashboard-context"
4
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, Download, RefreshCw } from "lucide-react"
4
5
  import { useState } from "react"
5
6
 
6
7
  interface AuditLogTableProps {
@@ -40,6 +41,61 @@ export default function AuditLogTable({
40
41
  onSort
41
42
  }: AuditLogTableProps) {
42
43
  const [period, setPeriod] = useState("all")
44
+ const [showExportMenu, setShowExportMenu] = useState(false)
45
+ const { clientConfigs } = useDashboard()
46
+
47
+ const getClientConfig = (rawName: string) => {
48
+ if (!rawName) return undefined
49
+ const nameLower = rawName.toLowerCase()
50
+ return clientConfigs.find((c: any) => nameLower.includes(c.match))
51
+ }
52
+
53
+ const getClientDisplayName = (rawName: string) => {
54
+ const config = getClientConfig(rawName)
55
+ return config ? config.name : rawName
56
+ }
57
+
58
+ const exportToCSV = () => {
59
+ const headers = ['Timestamp', 'Client', 'Operation', 'Status', 'Description']
60
+ const csvContent = [
61
+ headers.join(','),
62
+ ...logs.map(log => [
63
+ `"${log.date}"`,
64
+ `"${getClientDisplayName(log.client)}"`,
65
+ `"${log.operation}"`,
66
+ `"${log.status}"`,
67
+ `"${log.description}"`
68
+ ].join(','))
69
+ ].join('\n')
70
+
71
+ const blob = new Blob([csvContent], { type: 'text/csv' })
72
+ const url = URL.createObjectURL(blob)
73
+ const a = document.createElement('a')
74
+ a.href = url
75
+ a.download = `cybermem-audit-${new Date().toISOString().split('T')[0]}.csv`
76
+ a.click()
77
+ URL.revokeObjectURL(url)
78
+ setShowExportMenu(false)
79
+ }
80
+
81
+ const exportToJSON = () => {
82
+ const jsonContent = JSON.stringify(logs.map(log => ({
83
+ timestamp: log.date,
84
+ client: getClientDisplayName(log.client),
85
+ operation: log.operation,
86
+ status: log.status,
87
+ description: log.description
88
+ })), null, 2)
89
+
90
+ const blob = new Blob([jsonContent], { type: 'application/json' })
91
+ const url = URL.createObjectURL(blob)
92
+ const a = document.createElement('a')
93
+ a.href = url
94
+ a.download = `cybermem-audit-${new Date().toISOString().split('T')[0]}.json`
95
+ a.click()
96
+ URL.revokeObjectURL(url)
97
+ setShowExportMenu(false)
98
+ }
43
99
 
44
100
  return (
45
101
  <div className="group relative overflow-hidden rounded-2xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg p-6 transition-all duration-300">
@@ -80,11 +136,9 @@ export default function AuditLogTable({
80
136
  </div>
81
137
 
82
138
  <div className="relative z-10">
83
- <div className="flex items-center justify-between mb-6">
84
- <div className="flex items-center gap-3">
85
- <h3 className="text-lg font-semibold text-white">Audit Log</h3>
86
- {loading && <RefreshCw className="w-4 h-4 text-emerald-500 animate-spin" />}
87
- </div>
139
+ <div className="flex items-center gap-3 mb-6">
140
+ <h3 className="text-lg font-semibold text-white">Audit Log</h3>
141
+ {loading && <RefreshCw className="w-4 h-4 text-emerald-500 animate-spin" />}
88
142
  </div>
89
143
 
90
144
 
@@ -137,10 +191,19 @@ export default function AuditLogTable({
137
191
  ) : (
138
192
  logs.map((log) => {
139
193
  const config = statusConfig[log.status] || statusConfig.Success
194
+ const clientConf = getClientConfig(log.client)
195
+ const displayName = clientConf ? clientConf.name : log.client
196
+ const icon = clientConf?.icon
197
+
140
198
  return (
141
199
  <tr key={log.id} className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row">
142
200
  <td className="py-4 px-4 text-neutral-300 group-hover/row:text-white transition-colors">{log.date}</td>
143
- <td className="py-4 px-4 text-white font-medium">{log.client}</td>
201
+ <td className="py-4 px-4 text-white font-medium">
202
+ <div className="flex items-center gap-2">
203
+ {icon && <img src={icon} alt={displayName} className="w-5 h-5 object-contain" />}
204
+ <span>{displayName}</span>
205
+ </div>
206
+ </td>
144
207
  <td className="py-4 px-4 text-neutral-300">{log.operation}</td>
145
208
  <td className="py-4 px-4">
146
209
  <span
@@ -166,10 +229,40 @@ export default function AuditLogTable({
166
229
  </table>
167
230
  </div>
168
231
 
169
- <div className="mt-6 flex items-center justify-between">
170
- <p className="text-sm text-neutral-500">
232
+ <div className="mt-6 flex items-center justify-between relative">
233
+ {/* Export Button (Left) */}
234
+ <div className="relative">
235
+ <button
236
+ onClick={() => setShowExportMenu(!showExportMenu)}
237
+ className="h-8 px-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 text-neutral-400 hover:text-white text-xs font-medium flex items-center gap-2 transition-all"
238
+ >
239
+ <Download className="w-3 h-3" />
240
+ Export
241
+ <ChevronDown className="w-3 h-3" />
242
+ </button>
243
+
244
+ {showExportMenu && (
245
+ <div className="absolute left-0 bottom-full mb-2 w-32 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-30 backdrop-blur-xl overflow-hidden">
246
+ <button
247
+ onClick={exportToCSV}
248
+ className="w-full text-left px-3 py-2 text-xs text-neutral-300 hover:bg-white/5 hover:text-white transition-colors"
249
+ >
250
+ Export CSV
251
+ </button>
252
+ <button
253
+ onClick={exportToJSON}
254
+ className="w-full text-left px-3 py-2 text-xs text-neutral-300 hover:bg-white/5 hover:text-white transition-colors"
255
+ >
256
+ Export JSON
257
+ </button>
258
+ </div>
259
+ )}
260
+ </div>
261
+
262
+ <p className="absolute left-1/2 -translate-x-1/2 text-sm text-neutral-500">
171
263
  Page {currentPage} of {Math.max(1, totalPages)}
172
264
  </p>
265
+
173
266
  <div className="flex gap-2">
174
267
  <button
175
268
  onClick={() => onPageChange(Math.max(1, currentPage - 1))}
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
4
  import { useDashboard } from "@/lib/data/dashboard-context";
5
5
  import { ChevronDown } from "lucide-react";
6
6
  import dynamic from "next/dynamic";
7
- import { useEffect, useState } from "react";
7
+ import { useEffect, useRef, useState } from "react";
8
8
 
9
9
  // Dynamic import with SSR disabled
10
10
  const MetricsChart = dynamic(() => import("./metrics-chart"), { ssr: false });
@@ -44,11 +44,11 @@ export default function ChartCard({ service }: ChartCardProps) {
44
44
  const [clientMetadata, setClientMetadata] = useState<Record<string, any>>({})
45
45
  const [loading, setLoading] = useState(true)
46
46
 
47
+ const isInitialLoad = useRef(true)
47
48
  useEffect(() => {
48
49
  async function fetchData() {
49
- // Only show loading state on initial load or period change, not background refresh
50
- // We check if data is empty to determine initial load
51
- if (data.length === 0) setLoading(true)
50
+ // Only show loading state on initial load, not background refresh
51
+ if (isInitialLoad.current) setLoading(true)
52
52
 
53
53
  try {
54
54
  const timeSeriesData = await strategy.getChartData(period)
@@ -120,6 +120,7 @@ export default function ChartCard({ service }: ChartCardProps) {
120
120
  console.error("Failed to fetch chart data:", e)
121
121
  } finally {
122
122
  setLoading(false)
123
+ isInitialLoad.current = false
123
124
  }
124
125
  }
125
126
  fetchData()
@@ -1,7 +1,9 @@
1
1
  "use client"
2
2
 
3
3
  import { Button } from "@/components/ui/button";
4
- import { Book, Settings } from "lucide-react";
4
+ import { useDashboard } from "@/lib/data/dashboard-context";
5
+ import { AlertCircle, Book, CheckCircle2, Loader2, Settings, XCircle } from "lucide-react";
6
+ import Image from "next/image";
5
7
  import { useEffect, useState } from "react";
6
8
 
7
9
  export default function DashboardHeader({
@@ -12,6 +14,8 @@ export default function DashboardHeader({
12
14
  onShowSettings: () => void;
13
15
  }) {
14
16
  const [isScrolled, setIsScrolled] = useState(false)
17
+ const [showHealthPopup, setShowHealthPopup] = useState(false)
18
+ const { systemHealth } = useDashboard()
15
19
 
16
20
  useEffect(() => {
17
21
  // Check initial scroll position on mount
@@ -24,6 +28,23 @@ export default function DashboardHeader({
24
28
  return () => window.removeEventListener("scroll", handleScroll)
25
29
  }, [])
26
30
 
31
+ const getStatusConfig = () => {
32
+ if (!systemHealth) {
33
+ return { bg: "bg-neutral-500/10", text: "text-neutral-400", border: "border-neutral-500/20", icon: Loader2, label: "Checking..." }
34
+ }
35
+ switch (systemHealth.overall) {
36
+ case 'ok':
37
+ return { bg: "bg-emerald-500/10", text: "text-emerald-400", border: "border-emerald-500/20", icon: CheckCircle2, label: "All Systems OK" }
38
+ case 'degraded':
39
+ return { bg: "bg-amber-500/10", text: "text-amber-400", border: "border-amber-500/20", icon: AlertCircle, label: "Degraded" }
40
+ case 'error':
41
+ return { bg: "bg-red-500/10", text: "text-red-400", border: "border-red-500/20", icon: XCircle, label: "System Error" }
42
+ }
43
+ }
44
+
45
+ const statusConfig = getStatusConfig()
46
+ const StatusIcon = statusConfig.icon
47
+
27
48
  return (
28
49
  <header className={`sticky top-0 z-50 transition-all duration-300 ${
29
50
  isScrolled
@@ -34,22 +55,76 @@ export default function DashboardHeader({
34
55
  <div className="flex items-center justify-between">
35
56
  <div className="flex items-center gap-4">
36
57
  <div className="relative w-10 h-10 flex-shrink-0">
37
- <img src="/logo.svg" alt="CyberMem Logo" className="w-full h-full object-contain" />
58
+ <Image src="/logo.svg" alt="CyberMem Logo" width={40} height={40} className="object-contain" />
38
59
  </div>
39
60
  <div>
40
- <h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">CyberMem</h1>
61
+ <div className="flex items-center gap-3">
62
+ <h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">CyberMem</h1>
63
+
64
+ {/* System Health Status Badge with Hover Popup */}
65
+ <div
66
+ className="relative mt-1.5"
67
+ onMouseEnter={() => setShowHealthPopup(true)}
68
+ onMouseLeave={() => setShowHealthPopup(false)}
69
+ >
70
+ <div className={`px-2 py-[2px] rounded-full text-[10px] font-medium flex items-center gap-1 cursor-pointer ${statusConfig.bg} ${statusConfig.text} border ${statusConfig.border}`}>
71
+ <StatusIcon className={`w-3 h-3 ${!systemHealth ? 'animate-spin' : ''}`} />
72
+ {statusConfig.label}
73
+ </div>
74
+
75
+ {/* Hover Popup */}
76
+ {showHealthPopup && systemHealth && (
77
+ <div className="absolute top-full left-0 mt-2 w-64 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-50 backdrop-blur-xl overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
78
+ <div className="p-3 border-b border-white/5">
79
+ <p className="text-xs text-neutral-400">System Health</p>
80
+ <p className="text-[10px] text-neutral-500 mt-0.5">Updated: {new Date(systemHealth.timestamp).toLocaleTimeString()}</p>
81
+ </div>
82
+ <div className="p-2 space-y-1">
83
+ {systemHealth.services.map((service, i) => (
84
+ <div key={i} className="flex items-center justify-between px-2 py-1.5 rounded hover:bg-white/5">
85
+ <span className="text-xs text-neutral-300">{service.name}</span>
86
+ <div className="flex items-center gap-2">
87
+ {service.latencyMs && (
88
+ <span className="text-[10px] text-neutral-500">{service.latencyMs}ms</span>
89
+ )}
90
+ {service.status === 'ok' ? (
91
+ <CheckCircle2 className="w-3 h-3 text-emerald-400" />
92
+ ) : service.status === 'warning' ? (
93
+ <AlertCircle className="w-3 h-3 text-amber-400" />
94
+ ) : (
95
+ <XCircle className="w-3 h-3 text-red-400" />
96
+ )}
97
+ </div>
98
+ </div>
99
+ ))}
100
+ {systemHealth.services.some(s => s.status !== 'ok' && s.message) && (
101
+ <div className="mt-2 p-2 rounded bg-red-500/10 border border-red-500/20">
102
+ <p className="text-xs text-red-300 font-medium">Issues:</p>
103
+ {systemHealth.services.filter(s => s.status !== 'ok' && s.message).map((s, i) => (
104
+ <p key={i} className="text-[10px] text-red-400 mt-1">โ€ข {s.name}: {s.message}</p>
105
+ ))}
106
+ </div>
107
+ )}
108
+ </div>
109
+ </div>
110
+ )}
111
+ </div>
112
+ </div>
41
113
  <p className="text-sm text-neutral-400 mt-1">Memory MCP Server</p>
42
114
  </div>
115
+
43
116
  </div>
44
117
 
45
118
  <div className="flex items-center gap-3">
119
+
120
+
46
121
  <Button
47
122
  variant="ghost"
48
123
  size="sm"
49
124
  onClick={onShowMCPConfig}
50
125
  className="hidden md:flex h-10 px-4 text-sm font-medium bg-emerald-500/10 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 border border-emerald-500/20 hover:border-emerald-500/40 rounded-lg"
51
126
  >
52
- <img src="/icons/mcp.png" alt="MCP" className="w-4 h-4 mr-2" />
127
+ <Image src="/icons/mcp.png" alt="MCP" width={16} height={16} className="mr-2" />
53
128
  Connect MCP
54
129
  </Button>
55
130
 
@@ -79,4 +154,3 @@ export default function DashboardHeader({
79
154
  </header>
80
155
  )
81
156
  }
82
-
@@ -76,10 +76,6 @@ export default function LoginModal({ onLogin }: LoginModalProps) {
76
76
  <LogIn className="w-4 h-4" />
77
77
  Login
78
78
  </Button>
79
-
80
- <p className="text-xs text-neutral-500 text-center mt-4">
81
- Default password: <code className="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">admin</code>
82
- </p>
83
79
  </form>
84
80
  </div>
85
81
  </div>