@cybermem/dashboard 0.9.12 → 0.13.4

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 (43) hide show
  1. package/Dockerfile +3 -3
  2. package/app/api/audit-logs/route.ts +12 -6
  3. package/app/api/health/route.ts +2 -1
  4. package/app/api/mcp-config/route.ts +128 -0
  5. package/app/api/metrics/route.ts +22 -70
  6. package/app/api/settings/route.ts +125 -30
  7. package/app/page.tsx +105 -127
  8. package/components/dashboard/{chart-card.tsx → charts/chart-card.tsx} +13 -19
  9. package/components/dashboard/{metrics-chart.tsx → charts/memory-chart.tsx} +1 -1
  10. package/components/dashboard/charts-section.tsx +3 -3
  11. package/components/dashboard/header.tsx +177 -176
  12. package/components/dashboard/{audit-log-table.tsx → logs/log-viewer.tsx} +12 -7
  13. package/components/dashboard/mcp/config-preview.tsx +246 -0
  14. package/components/dashboard/mcp/platform-selector.tsx +96 -0
  15. package/components/dashboard/mcp-config-modal.tsx +97 -503
  16. package/components/dashboard/{metric-card.tsx → metrics/stat-card.tsx} +4 -2
  17. package/components/dashboard/metrics-grid.tsx +10 -2
  18. package/components/dashboard/settings/access-token-section.tsx +131 -0
  19. package/components/dashboard/settings/data-management-section.tsx +122 -0
  20. package/components/dashboard/settings/system-info-section.tsx +98 -0
  21. package/components/dashboard/settings-modal.tsx +55 -299
  22. package/e2e/api.spec.ts +219 -0
  23. package/e2e/routing.spec.ts +39 -0
  24. package/e2e/ui.spec.ts +373 -0
  25. package/lib/data/dashboard-context.tsx +96 -29
  26. package/lib/data/types.ts +32 -38
  27. package/middleware.ts +31 -13
  28. package/package.json +6 -1
  29. package/playwright.config.ts +23 -58
  30. package/public/clients.json +5 -3
  31. package/release-reports/assets/local/1_dashboard.png +0 -0
  32. package/release-reports/assets/local/2_audit_logs.png +0 -0
  33. package/release-reports/assets/local/3_charts.png +0 -0
  34. package/release-reports/assets/local/4_mcp_modal.png +0 -0
  35. package/release-reports/assets/local/5_settings_modal.png +0 -0
  36. package/lib/data/demo-strategy.ts +0 -110
  37. package/lib/data/production-strategy.ts +0 -191
  38. package/lib/prometheus/client.ts +0 -58
  39. package/lib/prometheus/index.ts +0 -6
  40. package/lib/prometheus/metrics.ts +0 -234
  41. package/lib/prometheus/sparklines.ts +0 -71
  42. package/lib/prometheus/timeseries.ts +0 -305
  43. package/lib/prometheus/utils.ts +0 -176
@@ -1,235 +1,236 @@
1
1
  "use client";
2
2
 
3
3
  import { Button } from "@/components/ui/button";
4
- import { useDashboard } from "@/lib/data/dashboard-context";
5
4
  import {
6
- AlertCircle,
7
- Book,
8
- CheckCircle2,
9
- Loader2,
10
- Settings,
11
- XCircle,
12
- } from "lucide-react";
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@/components/ui/popover";
9
+ import { useDashboard } from "@/lib/data/dashboard-context";
10
+ import { cn } from "@/lib/utils";
11
+ import { Activity, Book, Loader2, Settings, Shield, Zap } from "lucide-react";
13
12
  import Image from "next/image";
14
13
  import { useEffect, useState } from "react";
15
14
 
16
- export default function DashboardHeader({
17
- onShowMCPConfig,
18
- onShowSettings,
19
- }: {
15
+ export interface HeaderProps {
20
16
  onShowMCPConfig: () => void;
21
17
  onShowSettings: () => void;
22
- }) {
23
- const [isScrolled, setIsScrolled] = useState(false);
24
- const [showHealthPopup, setShowHealthPopup] = useState(false);
18
+ memoryCount?: number;
19
+ }
20
+
21
+ export default function Header({
22
+ onShowMCPConfig,
23
+ onShowSettings,
24
+ memoryCount = 0,
25
+ }: HeaderProps) {
25
26
  const { systemHealth } = useDashboard();
27
+ const [isScrolled, setIsScrolled] = useState(false);
28
+ const [instanceLabel, setInstanceLabel] = useState<string>("checking...");
26
29
 
27
30
  useEffect(() => {
28
- // Check initial scroll position on mount
31
+ // Initial check
29
32
  setIsScrolled(window.scrollY > 10);
30
33
 
31
34
  const handleScroll = () => {
32
35
  setIsScrolled(window.scrollY > 10);
33
36
  };
34
37
  window.addEventListener("scroll", handleScroll);
38
+
39
+ fetch("/api/settings")
40
+ .then((res) => res.json())
41
+ .then((data) => {
42
+ const { env, instance, tailscale } = data;
43
+ let prefix = instance || "local";
44
+ if (instance === "local") prefix = "localhost";
45
+ else if (instance === "rpi") prefix = tailscale ? "rpi-ts" : "rpi-lan";
46
+ else if (instance === "vps") prefix = "vps";
47
+ setInstanceLabel(`${prefix}-${env || "prod"}`);
48
+ })
49
+ .catch(() => {});
50
+
35
51
  return () => window.removeEventListener("scroll", handleScroll);
36
52
  }, []);
37
53
 
38
54
  const getStatusConfig = () => {
39
55
  if (!systemHealth) {
40
56
  return {
41
- bg: "bg-neutral-500/10",
42
- text: "text-neutral-400",
43
- border: "border-neutral-500/20",
57
+ text: "text-zinc-400",
44
58
  icon: Loader2,
45
- label: "Checking...",
59
+ label: "probing",
46
60
  };
47
61
  }
48
- switch (systemHealth.overall) {
49
- case "ok":
50
- return {
51
- bg: "bg-emerald-500/10",
52
- text: "text-emerald-400",
53
- border: "border-emerald-500/20",
54
- icon: CheckCircle2,
55
- label: "All Systems OK",
56
- };
57
- case "degraded":
58
- return {
59
- bg: "bg-amber-500/10",
60
- text: "text-amber-400",
61
- border: "border-amber-500/20",
62
- icon: AlertCircle,
63
- label: "Degraded",
64
- };
65
- case "error":
66
- return {
67
- bg: "bg-red-500/10",
68
- text: "text-red-400",
69
- border: "border-red-500/20",
70
- icon: XCircle,
71
- label: "System Error",
72
- };
62
+ if (systemHealth.overall === "ok") {
63
+ return {
64
+ text: "text-emerald-400",
65
+ icon: Zap,
66
+ label: "healthy",
67
+ };
73
68
  }
69
+ return {
70
+ text: "text-red-400",
71
+ icon: Zap,
72
+ label: "degraded",
73
+ };
74
74
  };
75
75
 
76
- const statusConfig = getStatusConfig();
77
- const StatusIcon = statusConfig.icon;
76
+ const status = getStatusConfig();
77
+ const StatusIcon = status.icon;
78
78
 
79
79
  return (
80
80
  <header
81
- className={`sticky top-0 z-50 transition-all duration-300 ${
81
+ className={cn(
82
+ "fixed top-0 left-0 right-0 z-50 transition-all duration-300",
82
83
  isScrolled
83
- ? "border-b border-white/10 backdrop-blur-xl bg-neutral-900/30"
84
- : "border-b border-transparent bg-transparent"
85
- }`}
84
+ ? "border-b border-white/10 backdrop-blur-xl bg-neutral-900/30 shadow-2xl"
85
+ : "border-b border-transparent bg-transparent",
86
+ )}
86
87
  >
87
- <div className="px-6 py-5 max-w-7xl mx-auto">
88
- <div className="flex items-center justify-between">
89
- <div className="flex items-center gap-4">
90
- <div className="relative w-10 h-10 flex-shrink-0">
91
- <Image
92
- src="/logo.svg"
93
- alt="CyberMem Logo"
94
- width={40}
95
- height={40}
96
- className="object-contain"
97
- />
98
- </div>
99
- <div>
100
- <div className="flex items-center gap-3">
101
- <h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">
102
- CyberMem
103
- </h1>
88
+ <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
89
+ {/* Left Section: Logo & Integrated Info/Status */}
90
+ <div className="flex items-center gap-6">
91
+ <div className="h-10 w-10 relative">
92
+ <Image
93
+ src="/logo.png"
94
+ alt="CyberMem"
95
+ fill
96
+ className="object-contain opacity-80"
97
+ />
98
+ </div>
99
+ <div className="flex flex-col">
100
+ <h1 className="text-2xl font-bold tracking-tighter text-white font-[family-name:var(--font-exo2)]">
101
+ CyberMem
102
+ </h1>
104
103
 
105
- {/* System Health Status Badge with Hover Popup */}
106
- <div
107
- className="relative mt-1.5"
108
- onMouseEnter={() => setShowHealthPopup(true)}
109
- onMouseLeave={() => setShowHealthPopup(false)}
110
- >
111
- {!systemHealth ? (
112
- /* Shimmer loading state */
113
- <div className="px-3 py-[2px] rounded-full bg-white/5 border border-white/10 animate-pulse">
114
- <div className="flex items-center gap-1">
115
- <div className="w-3 h-3 rounded-full bg-white/10" />
116
- <div className="w-16 h-3 rounded bg-white/10" />
117
- </div>
118
- </div>
119
- ) : (
120
- <div
121
- 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}`}
122
- >
123
- <StatusIcon className="w-3 h-3" />
124
- {statusConfig.label}
125
- </div>
126
- )}
104
+ <div className="flex items-center gap-2 text-[12px] font-normal tracking-normal text-white">
105
+ <span className="opacity-90 lowercase">{instanceLabel}</span>
106
+ <span className="opacity-50 text-[10px]">•</span>
127
107
 
128
- {/* Hover Popup */}
129
- {showHealthPopup && systemHealth && (
130
- <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">
131
- <div className="p-3 border-b border-white/5">
132
- <p className="text-xs text-neutral-400">
133
- System Health
134
- </p>
135
- <p className="text-[10px] text-neutral-500 mt-0.5">
136
- Updated:{" "}
137
- {new Date(
138
- systemHealth.timestamp,
139
- ).toLocaleTimeString()}
140
- </p>
141
- </div>
142
- <div className="p-2 space-y-1">
108
+ <Popover>
109
+ <PopoverTrigger asChild>
110
+ <button
111
+ className={cn(
112
+ "flex items-center gap-1 transition-colors hover:opacity-80 active:opacity-100",
113
+ status.text,
114
+ )}
115
+ >
116
+ <StatusIcon
117
+ className={cn(
118
+ "h-3.5 w-3.5 flex-shrink-0",
119
+ status.label === "probing" && "animate-spin",
120
+ )}
121
+ />
122
+ <span className="font-normal lowercase truncate">
123
+ {status.label}
124
+ </span>
125
+ </button>
126
+ </PopoverTrigger>
127
+ <PopoverContent className="w-80 bg-zinc-950/95 backdrop-blur-2xl border-white/10 p-6 rounded-[2rem] shadow-[0_20px_50px_rgba(0,0,0,0.5)] animate-in slide-in-from-top-2">
128
+ <div className="space-y-6">
129
+ <div className="flex items-center justify-between pb-4 border-b border-white/5">
130
+ <h4 className="text-xs font-black uppercase tracking-widest text-zinc-400">
131
+ System Health
132
+ </h4>
133
+ <Activity className="h-4 w-4 text-emerald-400" />
134
+ </div>
135
+ {systemHealth?.services ? (
136
+ <div className="space-y-4">
143
137
  {systemHealth.services.map((service, i) => (
144
138
  <div
145
139
  key={i}
146
- className="flex items-center justify-between px-2 py-1.5 rounded hover:bg-white/5"
140
+ className="flex items-center justify-between group"
147
141
  >
148
- <span className="text-xs text-neutral-300">
149
- {service.name}
150
- </span>
151
- <div className="flex items-center gap-2">
152
- {service.latencyMs && (
153
- <span className="text-[10px] text-neutral-500">
154
- {service.latencyMs}ms
155
- </span>
156
- )}
157
- {service.status === "ok" ? (
158
- <CheckCircle2 className="w-3 h-3 text-emerald-400" />
159
- ) : service.status === "warning" ? (
160
- <AlertCircle className="w-3 h-3 text-amber-400" />
161
- ) : (
162
- <XCircle className="w-3 h-3 text-red-400" />
163
- )}
142
+ <div className="flex items-center gap-3">
143
+ <div
144
+ className={cn(
145
+ "h-2 w-2 rounded-full",
146
+ service.status === "ok"
147
+ ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.4)]"
148
+ : "bg-red-500",
149
+ )}
150
+ />
151
+ <span className="text-[11px] font-bold text-zinc-300 group-hover:text-white transition-colors">
152
+ {service.name}
153
+ </span>
164
154
  </div>
155
+ <span className="text-[10px] font-black text-zinc-600 uppercase tracking-tighter">
156
+ {service.latencyMs
157
+ ? `${service.latencyMs}ms`
158
+ : service.status === "ok"
159
+ ? "Online"
160
+ : "Error"}
161
+ </span>
165
162
  </div>
166
163
  ))}
167
- {systemHealth.services.some(
168
- (s) => s.status !== "ok" && s.message,
169
- ) && (
170
- <div className="mt-2 p-2 rounded bg-red-500/10 border border-red-500/20">
171
- <p className="text-xs text-red-300 font-medium">
172
- Issues:
173
- </p>
174
- {systemHealth.services
175
- .filter((s) => s.status !== "ok" && s.message)
176
- .map((s, i) => (
177
- <p
178
- key={i}
179
- className="text-[10px] text-red-400 mt-1"
180
- >
181
- • {s.name}: {s.message}
182
- </p>
183
- ))}
184
- </div>
185
- )}
186
164
  </div>
165
+ ) : (
166
+ <div className="flex items-center gap-3 text-zinc-500">
167
+ <Loader2 className="h-4 w-4 animate-spin" />
168
+ <span className="text-xs font-bold">
169
+ Synchronizing telemetry...
170
+ </span>
171
+ </div>
172
+ )}
173
+ <div className="pt-4 border-t border-white/5 flex items-center justify-between">
174
+ <span className="text-[9px] font-black text-zinc-600 uppercase">
175
+ Latency: Minimal
176
+ </span>
177
+ <span className="text-[10px] font-medium opacity-40 ml-auto mr-2">
178
+ {memoryCount} memories
179
+ </span>
180
+ <Shield className="h-3 w-3 text-zinc-700" />
187
181
  </div>
188
- )}
189
- </div>
190
- </div>
191
- <p className="text-sm text-neutral-400 mt-1">Memory MCP Server</p>
182
+ </div>
183
+ </PopoverContent>
184
+ </Popover>
192
185
  </div>
193
186
  </div>
187
+ </div>
194
188
 
195
- <div className="flex items-center gap-3">
196
- <Button
197
- variant="ghost"
198
- size="sm"
199
- onClick={onShowMCPConfig}
200
- 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"
201
- >
189
+ {/* Right Section: Actions */}
190
+ <div className="flex items-center gap-3">
191
+ <Button
192
+ data-testid="mcp-button"
193
+ onClick={onShowMCPConfig}
194
+ className="bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 text-sm font-medium transition-colors h-9 px-3 rounded-lg flex items-center gap-2"
195
+ >
196
+ <div className="relative h-4 w-4">
202
197
  <Image
203
198
  src="/icons/mcp.png"
204
199
  alt="MCP"
205
- width={16}
206
- height={16}
207
- className="mr-2"
200
+ fill
201
+ className="object-contain"
208
202
  />
209
- Connect MCP
210
- </Button>
203
+ </div>
204
+ Connect MCP
205
+ </Button>
211
206
 
212
- <Button
213
- variant="ghost"
214
- size="sm"
215
- asChild
216
- 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"
207
+ <Button
208
+ variant="ghost"
209
+ size="sm"
210
+ className="text-white/50 hover:text-white hover:bg-white/5 transition-all h-9 px-4 rounded-lg group/docs border border-transparent hover:border-white/10"
211
+ asChild
212
+ >
213
+ <a
214
+ href="https://docs.cybermem.dev"
215
+ target="_blank"
216
+ rel="noopener noreferrer"
217
+ className="flex items-center"
217
218
  >
218
- <a href="https://docs.cybermem.dev" target="_blank">
219
- <Book className="w-4 h-4 mr-2" />
220
- Docs
221
- </a>
222
- </Button>
219
+ <Book className="h-4 w-4 mr-2 opacity-50 group-hover/docs:opacity-100 transition-opacity" />
220
+ Docs
221
+ </a>
222
+ </Button>
223
223
 
224
- <Button
225
- variant="ghost"
226
- size="icon"
227
- onClick={onShowSettings}
228
- className="h-10 w-10 text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
229
- >
230
- <Settings className="w-5 h-5" />
231
- </Button>
232
- </div>
224
+ <Button
225
+ data-testid="settings-button"
226
+ variant="ghost"
227
+ size="icon"
228
+ onClick={onShowSettings}
229
+ className="text-white/50 hover:text-white hover:bg-white/10 transition-all h-9 w-9 rounded-lg border border-transparent hover:border-white/10"
230
+ title="Settings"
231
+ >
232
+ <Settings className="h-5 w-5" />
233
+ </Button>
233
234
  </div>
234
235
  </div>
235
236
  </header>
@@ -11,9 +11,10 @@ import {
11
11
  Download,
12
12
  RefreshCw,
13
13
  } from "lucide-react";
14
+ import Image from "next/image";
14
15
  import { useState } from "react";
15
16
 
16
- interface AuditLogTableProps {
17
+ interface LogViewerProps {
17
18
  logs: any[];
18
19
  loading: boolean;
19
20
  currentPage: number;
@@ -58,7 +59,7 @@ const periods = [
58
59
  { label: "All Time", value: "all" },
59
60
  ];
60
61
 
61
- export default function AuditLogTable({
62
+ export default function LogViewer({
62
63
  logs,
63
64
  loading,
64
65
  currentPage,
@@ -67,7 +68,7 @@ export default function AuditLogTable({
67
68
  sortField,
68
69
  sortDirection,
69
70
  onSort,
70
- }: AuditLogTableProps) {
71
+ }: LogViewerProps) {
71
72
  const [period, setPeriod] = useState("all");
72
73
  const [showExportMenu, setShowExportMenu] = useState(false);
73
74
  const { clientConfigs } = useDashboard();
@@ -260,15 +261,19 @@ export default function AuditLogTable({
260
261
  key={log.id}
261
262
  className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row"
262
263
  >
263
- <td className="py-4 px-3 text-neutral-300 group-hover/row:text-white transition-colors">
264
- {log.date}
264
+ <td className="py-4 px-3 text-neutral-300">
265
+ {log.date
266
+ ? new Date(log.date).toLocaleString()
267
+ : "N/A"}
265
268
  </td>
266
269
  <td className="py-4 px-3 text-white font-medium">
267
270
  <div className="flex items-center gap-2">
268
271
  {icon && (
269
- <img
272
+ <Image
270
273
  src={icon}
271
274
  alt={displayName}
275
+ width={20}
276
+ height={20}
272
277
  className="w-5 h-5 object-contain"
273
278
  />
274
279
  )}
@@ -283,7 +288,7 @@ export default function AuditLogTable({
283
288
  </td>
284
289
  <td className="py-4 px-3">
285
290
  <span
286
- className={`px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
291
+ className={`status-pill px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
287
292
  >
288
293
  {log.status}
289
294
  </span>