@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.
Files changed (123) hide show
  1. package/.dockerignore +11 -0
  2. package/.eslintrc.json +3 -0
  3. package/Dockerfile +48 -0
  4. package/app/api/audit-logs/route.ts +60 -0
  5. package/app/api/metrics/route.ts +141 -0
  6. package/app/api/prometheus/route.ts +65 -0
  7. package/app/api/settings/regenerate/route.ts +20 -0
  8. package/app/api/settings/route.ts +25 -0
  9. package/app/api/system/restart/route.ts +18 -0
  10. package/app/globals.css +148 -0
  11. package/app/layout.tsx +37 -0
  12. package/app/page.tsx +150 -0
  13. package/components/dashboard/audit-log-table.tsx +195 -0
  14. package/components/dashboard/chart-card.tsx +196 -0
  15. package/components/dashboard/charts-section.tsx +16 -0
  16. package/components/dashboard/header.tsx +82 -0
  17. package/components/dashboard/login-modal.tsx +87 -0
  18. package/components/dashboard/mcp-config-modal.tsx +397 -0
  19. package/components/dashboard/metric-card.tsx +23 -0
  20. package/components/dashboard/metrics-chart.tsx +134 -0
  21. package/components/dashboard/metrics-grid.tsx +136 -0
  22. package/components/dashboard/settings-modal.tsx +345 -0
  23. package/components/theme-provider.tsx +11 -0
  24. package/components/ui/accordion.tsx +66 -0
  25. package/components/ui/alert-dialog.tsx +157 -0
  26. package/components/ui/alert.tsx +66 -0
  27. package/components/ui/aspect-ratio.tsx +11 -0
  28. package/components/ui/avatar.tsx +53 -0
  29. package/components/ui/badge.tsx +46 -0
  30. package/components/ui/breadcrumb.tsx +109 -0
  31. package/components/ui/button-group.tsx +83 -0
  32. package/components/ui/button.tsx +60 -0
  33. package/components/ui/calendar.tsx +213 -0
  34. package/components/ui/card.tsx +92 -0
  35. package/components/ui/carousel.tsx +241 -0
  36. package/components/ui/chart.tsx +353 -0
  37. package/components/ui/checkbox.tsx +32 -0
  38. package/components/ui/collapsible.tsx +33 -0
  39. package/components/ui/command.tsx +184 -0
  40. package/components/ui/context-menu.tsx +252 -0
  41. package/components/ui/dialog.tsx +143 -0
  42. package/components/ui/drawer.tsx +135 -0
  43. package/components/ui/dropdown-menu.tsx +257 -0
  44. package/components/ui/empty.tsx +104 -0
  45. package/components/ui/field.tsx +244 -0
  46. package/components/ui/form.tsx +167 -0
  47. package/components/ui/hover-card.tsx +44 -0
  48. package/components/ui/input-group.tsx +169 -0
  49. package/components/ui/input-otp.tsx +77 -0
  50. package/components/ui/input.tsx +21 -0
  51. package/components/ui/item.tsx +193 -0
  52. package/components/ui/kbd.tsx +28 -0
  53. package/components/ui/label.tsx +24 -0
  54. package/components/ui/menubar.tsx +276 -0
  55. package/components/ui/navigation-menu.tsx +166 -0
  56. package/components/ui/pagination.tsx +127 -0
  57. package/components/ui/popover.tsx +48 -0
  58. package/components/ui/progress.tsx +31 -0
  59. package/components/ui/radio-group.tsx +45 -0
  60. package/components/ui/resizable.tsx +56 -0
  61. package/components/ui/scroll-area.tsx +58 -0
  62. package/components/ui/select.tsx +185 -0
  63. package/components/ui/separator.tsx +28 -0
  64. package/components/ui/sheet.tsx +139 -0
  65. package/components/ui/sidebar.tsx +726 -0
  66. package/components/ui/skeleton.tsx +13 -0
  67. package/components/ui/slider.tsx +63 -0
  68. package/components/ui/sonner.tsx +25 -0
  69. package/components/ui/spinner.tsx +16 -0
  70. package/components/ui/switch.tsx +31 -0
  71. package/components/ui/table.tsx +116 -0
  72. package/components/ui/tabs.tsx +66 -0
  73. package/components/ui/textarea.tsx +18 -0
  74. package/components/ui/toast.tsx +129 -0
  75. package/components/ui/toaster.tsx +35 -0
  76. package/components/ui/toggle-group.tsx +73 -0
  77. package/components/ui/toggle.tsx +47 -0
  78. package/components/ui/tooltip.tsx +61 -0
  79. package/components/ui/use-mobile.tsx +19 -0
  80. package/components/ui/use-toast.ts +191 -0
  81. package/components.json +21 -0
  82. package/hooks/use-mobile.ts +19 -0
  83. package/hooks/use-toast.ts +191 -0
  84. package/lib/data/dashboard-context.tsx +75 -0
  85. package/lib/data/demo-strategy.ts +110 -0
  86. package/lib/data/production-strategy.ts +152 -0
  87. package/lib/data/types.ts +52 -0
  88. package/lib/prometheus/client.ts +58 -0
  89. package/lib/prometheus/index.ts +6 -0
  90. package/lib/prometheus/metrics.ts +234 -0
  91. package/lib/prometheus/sparklines.ts +71 -0
  92. package/lib/prometheus/timeseries.ts +305 -0
  93. package/lib/prometheus/utils.ts +176 -0
  94. package/lib/utils.ts +6 -0
  95. package/next.config.mjs +36 -0
  96. package/package.json +91 -0
  97. package/postcss.config.mjs +8 -0
  98. package/public/clients.json +165 -0
  99. package/public/favicon-dark.svg +1 -0
  100. package/public/favicon-light.svg +1 -0
  101. package/public/icons/antigravity.png +0 -0
  102. package/public/icons/chatgpt.png +0 -0
  103. package/public/icons/claude-code.png +0 -0
  104. package/public/icons/claude.png +0 -0
  105. package/public/icons/codex.png +0 -0
  106. package/public/icons/cursor.png +0 -0
  107. package/public/icons/gemini.png +0 -0
  108. package/public/icons/images.jpeg +0 -0
  109. package/public/icons/mcp.png +0 -0
  110. package/public/icons/mono.png +0 -0
  111. package/public/icons/perplexity.png +0 -0
  112. package/public/icons/vscode.png +0 -0
  113. package/public/icons/warp.png +0 -0
  114. package/public/icons/windsurf.png +0 -0
  115. package/public/logo.png +0 -0
  116. package/public/logo.svg +7 -0
  117. package/public/manifest.json +21 -0
  118. package/public/site.webmanifest +21 -0
  119. package/public/web-app-manifest-192x192.png +0 -0
  120. package/public/web-app-manifest-512x512.png +0 -0
  121. package/shared.env +0 -0
  122. package/styles/globals.css +125 -0
  123. package/tsconfig.json +41 -0
package/app/page.tsx ADDED
@@ -0,0 +1,150 @@
1
+ "use client"
2
+
3
+ import AuditLogTable from "@/components/dashboard/audit-log-table"
4
+ import ChartsSection from "@/components/dashboard/charts-section"
5
+ import DashboardHeader from "@/components/dashboard/header"
6
+ import LoginModal from "@/components/dashboard/login-modal"
7
+ import MCPConfigModal from "@/components/dashboard/mcp-config-modal"
8
+ import MetricsGrid from "@/components/dashboard/metrics-grid"
9
+ import SettingsModal from "@/components/dashboard/settings-modal"
10
+ import { useDashboard } from "@/lib/data/dashboard-context"
11
+ import { DashboardData } from "@/lib/data/types"
12
+ import { useEffect, useState } from "react"
13
+
14
+ // Types (Ideally imported, but keeping for now if used elsewhere locally, though strategy returns properly typed data)
15
+ // We use the types from lib/data/types.ts now
16
+
17
+ export default function Dashboard() {
18
+ const { strategy, isDemo, toggleDemo, refreshSignal } = useDashboard()
19
+
20
+ const [showMCPConfig, setShowMCPConfig] = useState(false)
21
+ const [showSettings, setShowSettings] = useState(false)
22
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
23
+
24
+ // Data State
25
+ const [data, setData] = useState<DashboardData>({
26
+ stats: {
27
+ memoryRecords: 0,
28
+ totalClients: 0,
29
+ successRate: 0,
30
+ totalRequests: 0,
31
+ topWriter: { name: "N/A", count: 0 },
32
+ topReader: { name: "N/A", count: 0 },
33
+ lastWriter: { name: "N/A", timestamp: 0 },
34
+ lastReader: { name: "N/A", timestamp: 0 },
35
+ },
36
+ trends: {
37
+ memory: { change: "", trend: "neutral", hasData: false, data: [] },
38
+ clients: { change: "", trend: "neutral", hasData: false, data: [] },
39
+ success: { change: "", trend: "neutral", hasData: false, data: [] },
40
+ requests: { change: "", trend: "neutral", hasData: false, data: [] },
41
+ },
42
+ logs: []
43
+ })
44
+
45
+ // Check authentication on mount
46
+ useEffect(() => {
47
+ const auth = sessionStorage.getItem("authenticated")
48
+ if (auth === "true") {
49
+ setIsAuthenticated(true)
50
+ }
51
+ }, [])
52
+
53
+ const handleLogin = (password: string) => {
54
+ sessionStorage.setItem("authenticated", "true")
55
+ setIsAuthenticated(true)
56
+ }
57
+
58
+ // Fetch Data Effect - Reacts to strategy change or refresh signal
59
+ useEffect(() => {
60
+ async function updateData() {
61
+ try {
62
+ const potentialData = await strategy.fetchGlobalStats()
63
+ setData(potentialData)
64
+ } catch (e) {
65
+ console.error("Failed to fetch dashboard data:", e)
66
+ }
67
+ }
68
+ updateData()
69
+ }, [strategy, refreshSignal])
70
+
71
+
72
+ // Audit Log internal state for filtering/sorting (UI logic only)
73
+ const [searchTerm, setSearchTerm] = useState("")
74
+ const [currentPage, setCurrentPage] = useState(1)
75
+ const [sortField, setSortField] = useState<"date" | "client" | "operation" | "status">("date")
76
+ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc")
77
+ const itemsPerPage = 10
78
+
79
+ const filteredLog = (data.logs || []).filter(
80
+ (log) =>
81
+ log.client.toLowerCase().includes(searchTerm.toLowerCase()) ||
82
+ log.operation.toLowerCase().includes(searchTerm.toLowerCase()) ||
83
+ log.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
84
+ log.description.toLowerCase().includes(searchTerm.toLowerCase()),
85
+ )
86
+
87
+ const handleSort = (field: string) => {
88
+ if (field === sortField) {
89
+ setSortDirection(prev => prev === "asc" ? "desc" : "asc")
90
+ } else {
91
+ setSortField(field as any)
92
+ setSortDirection("asc")
93
+ }
94
+ }
95
+
96
+ const sortedLog = [...filteredLog].sort((a, b) => {
97
+ const modifier = sortDirection === "asc" ? 1 : -1
98
+ if (sortField === "date") {
99
+ return (new Date(a.date).getTime() - new Date(b.date).getTime()) * modifier
100
+ }
101
+ const aValue = (a as any)[sortField] || ""
102
+ const bValue = (b as any)[sortField] || ""
103
+ if (typeof aValue === "string" && typeof bValue === "string") {
104
+ return aValue.localeCompare(bValue) * modifier
105
+ }
106
+ return 0
107
+ })
108
+
109
+ const paginatedLog = sortedLog.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
110
+ const totalPages = Math.ceil(sortedLog.length / itemsPerPage)
111
+
112
+ // Show login modal if not authenticated
113
+ if (!isAuthenticated) {
114
+ return <LoginModal onLogin={handleLogin} />
115
+ }
116
+
117
+ return (
118
+ <div className="min-h-screen text-foreground">
119
+ <DashboardHeader
120
+ onShowMCPConfig={() => setShowMCPConfig(true)}
121
+ onShowSettings={() => setShowSettings(true)}
122
+ />
123
+
124
+ <main className="px-6 py-8 max-w-7xl mx-auto space-y-8">
125
+ <MetricsGrid stats={data.stats} trends={data.trends} />
126
+ <ChartsSection period="" />
127
+ <AuditLogTable
128
+ logs={(paginatedLog || []).map(log => ({
129
+ id: log.id,
130
+ date: log.date.toLocaleString(),
131
+ client: log.client,
132
+ operation: log.operation,
133
+ status: log.status,
134
+ description: log.description
135
+ }))}
136
+ loading={false}
137
+ currentPage={currentPage}
138
+ totalPages={totalPages}
139
+ onPageChange={setCurrentPage}
140
+ sortField={sortField}
141
+ sortDirection={sortDirection}
142
+ onSort={handleSort}
143
+ />
144
+ </main>
145
+
146
+ {showMCPConfig && <MCPConfigModal onClose={() => setShowMCPConfig(false)} />}
147
+ {showSettings && <SettingsModal onClose={() => { setShowSettings(false); }} />}
148
+ </div>
149
+ )
150
+ }
@@ -0,0 +1,195 @@
1
+ "use client"
2
+
3
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, RefreshCw } from "lucide-react"
4
+ import { useState } from "react"
5
+
6
+ interface AuditLogTableProps {
7
+ logs: any[]
8
+ loading: boolean
9
+ currentPage: number
10
+ totalPages: number
11
+ onPageChange: (page: number) => void
12
+ sortField: string
13
+ sortDirection: 'asc' | 'desc'
14
+ onSort: (field: string) => void
15
+ }
16
+
17
+ const statusConfig: Record<string, { bg: string; text: string; border: string }> = {
18
+ Success: { bg: "bg-emerald-500/10", text: "text-emerald-400", border: "border-emerald-500/30" },
19
+ Warning: { bg: "bg-amber-500/10", text: "text-amber-400", border: "border-amber-500/30" },
20
+ Error: { bg: "bg-red-500/10", text: "text-red-400", border: "border-red-500/30" },
21
+ Canceled: { bg: "bg-slate-500/10", text: "text-slate-400", border: "border-slate-500/30" },
22
+ }
23
+
24
+ const periods = [
25
+ { label: "1 Hour", value: "1h" },
26
+ { label: "24 Hours", value: "24h" },
27
+ { label: "7 Days", value: "7d" },
28
+ { label: "30 Days", value: "30d" },
29
+ { label: "All Time", value: "all" },
30
+ ]
31
+
32
+ export default function AuditLogTable({
33
+ logs,
34
+ loading,
35
+ currentPage,
36
+ totalPages,
37
+ onPageChange,
38
+ sortField,
39
+ sortDirection,
40
+ onSort
41
+ }: AuditLogTableProps) {
42
+ const [period, setPeriod] = useState("all")
43
+
44
+ return (
45
+ <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">
46
+ {/* Neomorphism glow */}
47
+ <div className="absolute inset-0 bg-gradient-to-br from-white/5 via-transparent to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
48
+
49
+ {/* Period Selector - Badge Style - Absolute positioned in top-right (ignoring padding) */}
50
+ <div className="absolute top-0 right-0 z-20 group/period">
51
+ <button className="h-8 px-3 rounded-tl-none rounded-tr-2xl rounded-bl-2xl rounded-br-none bg-white/5 border-b border-l border-white/10 hover:bg-white/10 text-white text-xs font-medium flex items-center gap-2 transition-all">
52
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ strokeWidth={2}
57
+ d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
58
+ />
59
+ </svg>
60
+ {periods.find((p) => p.value === period)?.label}
61
+ <ChevronDown className="w-3 h-3" />
62
+ </button>
63
+
64
+ {/* Dropdown Menu */}
65
+ <div className="absolute right-0 mt-2 w-40 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl opacity-0 invisible group-hover/period:opacity-100 group-hover/period:visible transition-all duration-200 z-30 backdrop-blur-xl overflow-hidden">
66
+ {periods.map((p) => (
67
+ <button
68
+ key={p.value}
69
+ onClick={() => setPeriod(p.value)}
70
+ className={`w-full text-left px-3 py-2 text-xs transition-colors ${
71
+ period === p.value
72
+ ? "bg-emerald-500/20 text-emerald-400 font-medium"
73
+ : "text-neutral-300 hover:bg-white/5 hover:text-white"
74
+ }`}
75
+ >
76
+ {p.label}
77
+ </button>
78
+ ))}
79
+ </div>
80
+ </div>
81
+
82
+ <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>
88
+ </div>
89
+
90
+
91
+
92
+
93
+ <div className="overflow-x-auto min-h-[400px]">
94
+ <table className="w-full text-sm">
95
+ <thead>
96
+ <tr className="border-b border-white/10">
97
+ {[
98
+ { label: "Timestamp", key: "date", width: "w-[200px]" },
99
+ { label: "Client", key: "client", width: "w-[260px]" },
100
+ { label: "Operation", key: "operation", width: "w-[120px]" },
101
+ { label: "Status", key: "status", width: "w-[120px]" },
102
+ { label: "Description", key: "description", width: "" },
103
+ ].map((header) => (
104
+ <th
105
+ key={header.key}
106
+ onClick={() => onSort(header.key)}
107
+ className={`text-left py-4 px-4 font-medium text-neutral-400 cursor-pointer hover:text-white transition-colors select-none group/th ${header.width}`}
108
+ >
109
+ <div className="flex items-center gap-2">
110
+ {header.label}
111
+ <div className="flex flex-col">
112
+ {sortField === header.key ? (
113
+ sortDirection === 'asc' ?
114
+ <ArrowUp className="w-3 h-3 text-emerald-400" /> :
115
+ <ArrowDown className="w-3 h-3 text-emerald-400" />
116
+ ) : (
117
+ <ArrowUpDown className="w-3 h-3 text-neutral-700 group-hover/th:text-neutral-500 transition-colors" />
118
+ )}
119
+ </div>
120
+ </div>
121
+ </th>
122
+ ))}
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ {loading && logs.length === 0 ? (
127
+ // Loading skeleton
128
+ Array.from({ length: 5 }).map((_, i) => (
129
+ <tr key={i} className="border-b border-white/5">
130
+ <td className="py-4 px-4"><div className="h-4 w-32 bg-white/5 rounded animate-pulse" /></td>
131
+ <td className="py-4 px-4"><div className="h-4 w-24 bg-white/5 rounded animate-pulse" /></td>
132
+ <td className="py-4 px-4"><div className="h-4 w-16 bg-white/5 rounded animate-pulse" /></td>
133
+ <td className="py-4 px-4"><div className="h-6 w-20 bg-white/5 rounded-full animate-pulse" /></td>
134
+ <td className="py-4 px-4"><div className="h-4 w-40 bg-white/5 rounded animate-pulse" /></td>
135
+ </tr>
136
+ ))
137
+ ) : (
138
+ logs.map((log) => {
139
+ const config = statusConfig[log.status] || statusConfig.Success
140
+ return (
141
+ <tr key={log.id} className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row">
142
+ <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>
144
+ <td className="py-4 px-4 text-neutral-300">{log.operation}</td>
145
+ <td className="py-4 px-4">
146
+ <span
147
+ className={`px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
148
+ >
149
+ {log.status}
150
+ </span>
151
+ </td>
152
+ <td className="py-4 px-4 text-neutral-400">{log.description}</td>
153
+ </tr>
154
+ )
155
+ })
156
+ )}
157
+
158
+ {!loading && logs.length === 0 && (
159
+ <tr>
160
+ <td colSpan={5} className="py-12 text-center text-neutral-500">
161
+ No logs found for this period
162
+ </td>
163
+ </tr>
164
+ )}
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+
169
+ <div className="mt-6 flex items-center justify-between">
170
+ <p className="text-sm text-neutral-500">
171
+ Page {currentPage} of {Math.max(1, totalPages)}
172
+ </p>
173
+ <div className="flex gap-2">
174
+ <button
175
+ onClick={() => onPageChange(Math.max(1, currentPage - 1))}
176
+ disabled={currentPage === 1}
177
+ className="px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
178
+ >
179
+ <ChevronLeft className="w-4 h-4" />
180
+ Previous
181
+ </button>
182
+ <button
183
+ onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
184
+ disabled={currentPage === totalPages || totalPages === 0}
185
+ className="px-3 py-2 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
186
+ >
187
+ Next
188
+ <ChevronRight className="w-4 h-4" />
189
+ </button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ )
195
+ }
@@ -0,0 +1,196 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { useDashboard } from "@/lib/data/dashboard-context";
5
+ import { ChevronDown } from "lucide-react";
6
+ import dynamic from "next/dynamic";
7
+ import { useEffect, useState } from "react";
8
+
9
+ // Dynamic import with SSR disabled
10
+ const MetricsChart = dynamic(() => import("./metrics-chart"), { ssr: false });
11
+
12
+ interface ChartCardProps {
13
+ service: string
14
+ }
15
+
16
+ // Fallback color generator
17
+ function stringToColor(str: string): string {
18
+ let hash = 0;
19
+ for (let i = 0; i < str.length; i++) {
20
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
21
+ }
22
+ let color = '#';
23
+ for (let i = 0; i < 3; i++) {
24
+ const value = (hash >> (i * 8)) & 0xFF;
25
+ color += ('00' + value.toString(16)).substr(-2);
26
+ }
27
+ return color;
28
+ }
29
+
30
+ const periods = [
31
+ { value: "1h", label: "1 Hour" },
32
+ { value: "24h", label: "24 Hours" },
33
+ { value: "7d", label: "7 Days" },
34
+ { value: "30d", label: "30 Days" },
35
+ { value: "90d", label: "90 Days" },
36
+ ]
37
+
38
+ export default function ChartCard({ service }: ChartCardProps) {
39
+ const { strategy, refreshSignal, clientConfigs } = useDashboard()
40
+ const [period, setPeriod] = useState("24h")
41
+ const [hovered, setHovered] = useState<string | null>(null)
42
+ const [data, setData] = useState<any[]>([])
43
+ const [clientNames, setClientNames] = useState<string[]>([])
44
+ const [clientMetadata, setClientMetadata] = useState<Record<string, any>>({})
45
+ const [loading, setLoading] = useState(true)
46
+
47
+ useEffect(() => {
48
+ 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)
52
+
53
+ try {
54
+ const timeSeriesData = await strategy.getChartData(period)
55
+
56
+ // Update client metadata if provided in response
57
+ if (timeSeriesData.metadata) {
58
+ setClientMetadata(prev => ({ ...prev, ...timeSeriesData.metadata }))
59
+ }
60
+
61
+ // Helper to format time based on period
62
+ const formatSeries = (series: any[]) => {
63
+ if (!series) return []
64
+ return series.map(point => {
65
+ const date = new Date((point.time as number) * 1000)
66
+ let timeStr = ""
67
+ // Show date if period is longer than 24h
68
+ if (["7d", "30d", "90d", "all"].includes(period)) {
69
+ timeStr = date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
70
+ } else {
71
+ timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
72
+ }
73
+ return {
74
+ ...point,
75
+ time: timeStr
76
+ }
77
+ })
78
+ }
79
+
80
+ // Extract client names from series and sort by total value (Ascending)
81
+ const getClients = (series: any[]) => {
82
+ if (!series || series.length === 0) return []
83
+ const keys = new Set<string>()
84
+ const totals: Record<string, number> = {}
85
+
86
+ series.forEach(point => {
87
+ Object.keys(point).forEach(k => {
88
+ if (k !== 'time') {
89
+ keys.add(k)
90
+ totals[k] = (totals[k] || 0) + (point[k] as number)
91
+ }
92
+ })
93
+ })
94
+
95
+ return Array.from(keys).sort((a, b) => (totals[a] || 0) - (totals[b] || 0))
96
+ }
97
+
98
+ // Get data based on service type
99
+ let seriesData: any[] = []
100
+ let clients: string[] = []
101
+
102
+ if (service.includes("Creates")) {
103
+ seriesData = formatSeries(timeSeriesData.creates)
104
+ clients = getClients(timeSeriesData.creates)
105
+ } else if (service.includes("Reads")) {
106
+ seriesData = formatSeries(timeSeriesData.reads)
107
+ clients = getClients(timeSeriesData.reads)
108
+ } else if (service.includes("Updates")) {
109
+ seriesData = formatSeries(timeSeriesData.updates)
110
+ clients = getClients(timeSeriesData.updates)
111
+ } else if (service.includes("Deletes")) {
112
+ seriesData = formatSeries(timeSeriesData.deletes)
113
+ clients = getClients(timeSeriesData.deletes)
114
+ }
115
+
116
+ setData(seriesData)
117
+ setClientNames(clients)
118
+
119
+ } catch (e) {
120
+ console.error("Failed to fetch chart data:", e)
121
+ } finally {
122
+ setLoading(false)
123
+ }
124
+ }
125
+ fetchData()
126
+ }, [period, service, strategy, refreshSignal])
127
+
128
+ const isMultiSeries = clientNames.length > 0
129
+
130
+ return (
131
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md relative overflow-visible pt-6 pb-2">
132
+ <button
133
+ className="absolute top-0 right-0 z-20 h-8 px-3 rounded-tl-none rounded-tr-xl rounded-bl-2xl rounded-br-none bg-white/5 border-b border-l border-white/10 hover:bg-white/10 text-white text-xs font-medium flex items-center gap-2 transition-all group"
134
+ onClick={() => document.getElementById(`dropdown-${service}`)?.classList.toggle('hidden')}
135
+ >
136
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path
138
+ strokeLinecap="round"
139
+ strokeLinejoin="round"
140
+ strokeWidth={2}
141
+ d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
142
+ />
143
+ </svg>
144
+ {periods.find((p) => p.value === period)?.label}
145
+ <ChevronDown className="w-3 h-3" />
146
+ </button>
147
+
148
+ {/* Dropdown Menu - Positioned relative to the button or card */}
149
+ <div id={`dropdown-${service}`} className="hidden absolute top-8 right-0 w-40 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-30 backdrop-blur-xl overflow-hidden">
150
+ {periods.map((p) => (
151
+ <button
152
+ key={p.value}
153
+ onClick={() => {
154
+ setPeriod(p.value);
155
+ document.getElementById(`dropdown-${service}`)?.classList.add('hidden');
156
+ }}
157
+ className={`w-full text-left px-3 py-2 text-xs transition-colors ${
158
+ period === p.value
159
+ ? "bg-emerald-500/20 text-emerald-400 font-medium"
160
+ : "text-neutral-300 hover:bg-white/5 hover:text-white"
161
+ }`}
162
+ >
163
+ {p.label}
164
+ </button>
165
+ ))}
166
+ </div>
167
+
168
+ <CardHeader className="relative">
169
+ <div className="flex items-center justify-between">
170
+ <div>
171
+ <CardTitle className="text-sm font-medium text-slate-400">Time Series</CardTitle>
172
+ <div className="text-2xl font-bold text-white">{service}</div>
173
+ </div>
174
+ </div>
175
+ </CardHeader>
176
+ <CardContent>
177
+ {loading ? (
178
+ <div className="h-[200px] w-full flex items-center justify-center">
179
+ <div className="text-neutral-500 text-sm">Loading...</div>
180
+ </div>
181
+ ) : (
182
+ <div className="h-[200px] w-full">
183
+ <MetricsChart
184
+ data={data}
185
+ isMultiSeries={isMultiSeries}
186
+ clientNames={clientNames}
187
+ clientConfigs={clientConfigs}
188
+ hovered={hovered}
189
+ setHovered={setHovered}
190
+ />
191
+ </div>
192
+ )}
193
+ </CardContent>
194
+ </Card>
195
+ )
196
+ }
@@ -0,0 +1,16 @@
1
+ "use client"
2
+
3
+ import ChartCard from "./chart-card";
4
+
5
+ export default function ChartsSection({ period }: { period: string }) {
6
+ // Remove the period prop since each chart will have its own selector
7
+ // Just render the charts, they'll manage their own data
8
+ return (
9
+ <div className="grid grid-cols-2 gap-6">
10
+ <ChartCard service="Creates by Client" />
11
+ <ChartCard service="Reads by Client" />
12
+ <ChartCard service="Updates by Client" />
13
+ <ChartCard service="Deletes by Client" />
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,82 @@
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Book, Settings } from "lucide-react";
5
+ import { useEffect, useState } from "react";
6
+
7
+ export default function DashboardHeader({
8
+ onShowMCPConfig,
9
+ onShowSettings,
10
+ }: {
11
+ onShowMCPConfig: () => void;
12
+ onShowSettings: () => void;
13
+ }) {
14
+ const [isScrolled, setIsScrolled] = useState(false)
15
+
16
+ useEffect(() => {
17
+ // Check initial scroll position on mount
18
+ setIsScrolled(window.scrollY > 10)
19
+
20
+ const handleScroll = () => {
21
+ setIsScrolled(window.scrollY > 10)
22
+ }
23
+ window.addEventListener("scroll", handleScroll)
24
+ return () => window.removeEventListener("scroll", handleScroll)
25
+ }, [])
26
+
27
+ return (
28
+ <header className={`sticky top-0 z-50 transition-all duration-300 ${
29
+ isScrolled
30
+ ? "border-b border-white/10 backdrop-blur-xl bg-neutral-900/30"
31
+ : "border-b border-transparent bg-transparent"
32
+ }`}>
33
+ <div className="px-6 py-5 max-w-7xl mx-auto">
34
+ <div className="flex items-center justify-between">
35
+ <div className="flex items-center gap-4">
36
+ <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" />
38
+ </div>
39
+ <div>
40
+ <h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">CyberMem</h1>
41
+ <p className="text-sm text-neutral-400 mt-1">Memory MCP Server</p>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="flex items-center gap-3">
46
+ <Button
47
+ variant="ghost"
48
+ size="sm"
49
+ onClick={onShowMCPConfig}
50
+ 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
+ >
52
+ <img src="/icons/mcp.png" alt="MCP" className="w-4 h-4 mr-2" />
53
+ Connect MCP
54
+ </Button>
55
+
56
+ <Button
57
+ variant="ghost"
58
+ size="sm"
59
+ asChild
60
+ className="hidden md:flex h-10 px-4 text-sm font-medium text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
61
+ >
62
+ <a href="https://cybermem.dev/docs" target="_blank">
63
+ <Book className="w-4 h-4 mr-2" />
64
+ Docs
65
+ </a>
66
+ </Button>
67
+
68
+ <Button
69
+ variant="ghost"
70
+ size="icon"
71
+ onClick={onShowSettings}
72
+ className="h-10 w-10 text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
73
+ >
74
+ <Settings className="w-5 h-5" />
75
+ </Button>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </header>
80
+ )
81
+ }
82
+