@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,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Card, CardContent } from "@/components/ui/card";
4
- import MetricCard from "./metric-card";
4
+ import MetricCard from "./metrics/stat-card";
5
5
 
6
6
  // Types
7
7
  interface TrendState {
@@ -49,20 +49,24 @@ function ClientCard({
49
49
  name,
50
50
  subtitle,
51
51
  isEmpty,
52
+ testId,
52
53
  }: {
53
54
  label: string;
54
55
  name: string;
55
56
  subtitle: string;
56
57
  isEmpty: boolean;
58
+ testId?: string;
57
59
  }) {
58
60
  return (
59
61
  <Card
60
- className={`bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden ${isEmpty ? "opacity-60" : ""}`}
62
+ className={`card bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden ${isEmpty ? "opacity-60" : ""}`}
63
+ data-testid={testId}
61
64
  >
62
65
  <CardContent className="pt-6 pb-6 relative">
63
66
  <div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
64
67
  <div
65
68
  className={`text-4xl font-bold mb-1 truncate ${isEmpty ? "text-slate-500" : "text-white"}`}
69
+ data-testid="stat-value"
66
70
  >
67
71
  {name}
68
72
  </div>
@@ -146,6 +150,7 @@ export default function MetricsGrid({
146
150
  ) : (
147
151
  <ClientCard
148
152
  label="Top Writer"
153
+ testId="card-top-writer"
149
154
  name={stats.topWriter.count > 0 ? stats.topWriter.name : "N/A"}
150
155
  subtitle={
151
156
  stats.topWriter.count > 0
@@ -162,6 +167,7 @@ export default function MetricsGrid({
162
167
  ) : (
163
168
  <ClientCard
164
169
  label="Top Reader"
170
+ testId="card-top-reader"
165
171
  name={stats.topReader.count > 0 ? stats.topReader.name : "N/A"}
166
172
  subtitle={
167
173
  stats.topReader.count > 0
@@ -178,6 +184,7 @@ export default function MetricsGrid({
178
184
  ) : (
179
185
  <ClientCard
180
186
  label="Last Writer"
187
+ testId="card-last-writer"
181
188
  name={stats.lastWriter.name !== "N/A" ? stats.lastWriter.name : "N/A"}
182
189
  subtitle={formatTimestamp(stats.lastWriter.timestamp)}
183
190
  isEmpty={
@@ -192,6 +199,7 @@ export default function MetricsGrid({
192
199
  ) : (
193
200
  <ClientCard
194
201
  label="Last Reader"
202
+ testId="card-last-reader"
195
203
  name={stats.lastReader.name !== "N/A" ? stats.lastReader.name : "N/A"}
196
204
  subtitle={formatTimestamp(stats.lastReader.timestamp)}
197
205
  isEmpty={
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { TintButton } from "@/components/ui/tint-button";
6
+ import { Check, Copy, Eye, EyeOff, RotateCcw, Shield } from "lucide-react";
7
+
8
+ interface AccessTokenSectionProps {
9
+ apiKey: string;
10
+ showApiKey: boolean;
11
+ setShowApiKey: (show: boolean) => void;
12
+ copiedId: string | null;
13
+ copyToClipboard: (text: string, id: string) => void;
14
+ setShowRegenConfirm: (show: boolean) => void;
15
+ isManaged: boolean;
16
+ instanceType: "local" | "rpi" | "vps";
17
+ }
18
+
19
+ export default function AccessTokenSection({
20
+ apiKey,
21
+ showApiKey,
22
+ setShowApiKey,
23
+ copiedId,
24
+ copyToClipboard,
25
+ setShowRegenConfirm,
26
+ isManaged,
27
+ instanceType,
28
+ }: AccessTokenSectionProps) {
29
+ return (
30
+ <section>
31
+ <h3 className="text-sm font-medium text-neutral-400 uppercase tracking-widest mb-4 flex items-center gap-2">
32
+ <Shield className="w-4 h-4" />
33
+ Access Token
34
+ </h3>
35
+ <div className="bg-white/[0.032] border-[0.5px] border-white/10 rounded-2xl p-6 space-y-4">
36
+ {/* Token Display */}
37
+ <div className="space-y-2">
38
+ <Label htmlFor="access-token">Your Access Token</Label>
39
+ <div className="flex gap-2">
40
+ <div className="relative flex-1">
41
+ <Input
42
+ id="access-token"
43
+ value={apiKey || "Token not generated yet"}
44
+ readOnly
45
+ className="bg-black/40 border-[0.5px] border-white/10 text-white font-mono text-sm pr-10"
46
+ type={showApiKey ? "text" : "password"}
47
+ />
48
+ <button
49
+ data-testid="toggle-visibility"
50
+ onClick={() => setShowApiKey(!showApiKey)}
51
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
52
+ >
53
+ {showApiKey ? (
54
+ <EyeOff className="w-4 h-4" />
55
+ ) : (
56
+ <Eye className="w-4 h-4" />
57
+ )}
58
+ </button>
59
+ </div>
60
+ <TintButton
61
+ tint="neutral"
62
+ variant="ghost"
63
+ size="icon"
64
+ onClick={() => copyToClipboard(apiKey, "accesstoken")}
65
+ title="Copy token"
66
+ >
67
+ {copiedId === "accesstoken" ? (
68
+ <Check className="h-4 w-4 text-emerald-400" />
69
+ ) : (
70
+ <Copy className="h-4 w-4" />
71
+ )}
72
+ </TintButton>
73
+ <TintButton
74
+ tint="yellow"
75
+ variant="ghost"
76
+ size="icon"
77
+ onClick={() => setShowRegenConfirm(true)}
78
+ title="Regenerate token"
79
+ >
80
+ <RotateCcw className="w-4 h-4" />
81
+ </TintButton>
82
+ </div>
83
+ <p className="text-xs text-neutral-500">
84
+ Use this token to connect MCP clients from other devices
85
+ </p>
86
+ </div>
87
+
88
+ {/* Auth Status */}
89
+ <div className="pt-4 border-t border-white/5">
90
+ <div className="flex items-center gap-3 p-3 bg-white/10 rounded-xl border-[0.5px] border-white/10">
91
+ {isManaged ? (
92
+ <>
93
+ <div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse" />
94
+ <div className="flex-1">
95
+ <p className="text-sm text-emerald-300 font-medium">
96
+ {instanceType === "local"
97
+ ? "Local Mode Active"
98
+ : "LAN / RPi Mode Active"}
99
+ </p>
100
+ <p className="text-xs text-white/70">
101
+ {instanceType === "local"
102
+ ? "No token needed for local connections"
103
+ : "Connect from other devices using the secure token"}
104
+ </p>
105
+ </div>
106
+ </>
107
+ ) : (
108
+ <>
109
+ <div className="w-2 h-2 bg-yellow-400 rounded-full" />
110
+ <div className="flex-1">
111
+ <p className="text-sm text-yellow-300 font-medium">
112
+ {instanceType === "rpi"
113
+ ? "LAN / RPi Mode"
114
+ : instanceType === "vps"
115
+ ? "Cloud Mode"
116
+ : "Remote Mode"}
117
+ </p>
118
+ <p className="text-xs text-white/70">
119
+ {instanceType === "rpi"
120
+ ? "Connecting from your laptop to RPi"
121
+ : "Token required for remote MCP connections"}
122
+ </p>
123
+ </div>
124
+ </>
125
+ )}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </section>
130
+ );
131
+ }
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import { TintButton } from "@/components/ui/tint-button";
4
+ import {
5
+ Check,
6
+ Database,
7
+ Download,
8
+ Loader2,
9
+ Trash2,
10
+ Upload,
11
+ X,
12
+ } from "lucide-react";
13
+
14
+ interface DataManagementSectionProps {
15
+ handleBackup: () => void;
16
+ isBackingUp: boolean;
17
+ handleRestore: (e: React.ChangeEvent<HTMLInputElement>) => void;
18
+ isRestoring: boolean;
19
+ setShowResetConfirm: (show: boolean) => void;
20
+ isResetting: boolean;
21
+ operationStatus: { type: "success" | "error"; message: string } | null;
22
+ setOperationStatus: (
23
+ status: { type: "success" | "error"; message: string } | null,
24
+ ) => void;
25
+ }
26
+
27
+ export default function DataManagementSection({
28
+ handleBackup,
29
+ isBackingUp,
30
+ handleRestore,
31
+ isRestoring,
32
+ setShowResetConfirm,
33
+ isResetting,
34
+ operationStatus,
35
+ setOperationStatus,
36
+ }: DataManagementSectionProps) {
37
+ return (
38
+ <section>
39
+ <h3 className="text-sm font-medium text-neutral-400 uppercase tracking-widest mb-4 flex items-center gap-2">
40
+ <Database className="w-4 h-4" />
41
+ Data Management
42
+ </h3>
43
+ <div className="bg-white/[0.032] border-[0.5px] border-white/10 rounded-2xl p-6 flex flex-col gap-4">
44
+ <div className="flex items-center gap-3">
45
+ <TintButton
46
+ tint="neutral"
47
+ variant="solid"
48
+ className="flex-1 h-11"
49
+ onClick={handleBackup}
50
+ disabled={isBackingUp}
51
+ >
52
+ {isBackingUp ? (
53
+ <Loader2 className="w-4 h-4 animate-spin" />
54
+ ) : (
55
+ <Download className="w-4 h-4" />
56
+ )}
57
+ Backup
58
+ </TintButton>
59
+
60
+ <div className="flex-1 relative">
61
+ <input
62
+ type="file"
63
+ id="restore-file"
64
+ className="hidden"
65
+ accept=".tar.gz,.tgz"
66
+ onChange={handleRestore}
67
+ disabled={isRestoring}
68
+ />
69
+ <TintButton
70
+ tint="neutral"
71
+ variant="solid"
72
+ className="w-full h-11"
73
+ onClick={() => document.getElementById("restore-file")?.click()}
74
+ disabled={isRestoring}
75
+ >
76
+ {isRestoring ? (
77
+ <Loader2 className="w-4 h-4 animate-spin" />
78
+ ) : (
79
+ <Upload className="w-4 h-4" />
80
+ )}
81
+ Restore
82
+ </TintButton>
83
+ </div>
84
+
85
+ <TintButton
86
+ tint="red"
87
+ variant="solid"
88
+ className="flex-1 h-11"
89
+ onClick={() => setShowResetConfirm(true)}
90
+ disabled={isResetting}
91
+ >
92
+ <Trash2 className="w-4 h-4" />
93
+ Reset DB
94
+ </TintButton>
95
+ </div>
96
+
97
+ {operationStatus && (
98
+ <div
99
+ className={`p-3 rounded-xl text-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-1 ${
100
+ operationStatus.type === "success"
101
+ ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
102
+ : "bg-red-500/10 text-red-400 border border-red-500/20"
103
+ }`}
104
+ >
105
+ {operationStatus.type === "success" ? (
106
+ <Check className="w-4 h-4" />
107
+ ) : (
108
+ <X className="w-4 h-4" />
109
+ )}
110
+ <span className="flex-1">{operationStatus.message}</span>
111
+ <button
112
+ onClick={() => setOperationStatus(null)}
113
+ className="opacity-50 hover:opacity-100 p-1"
114
+ >
115
+ <X className="w-4 h-4" />
116
+ </button>
117
+ </div>
118
+ )}
119
+ </div>
120
+ </section>
121
+ );
122
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { TintButton } from "@/components/ui/tint-button";
4
+ import { Loader2, RotateCcw, Server } from "lucide-react";
5
+
6
+ interface SystemInfoSectionProps {
7
+ settings: any;
8
+ handleRestart: () => void;
9
+ isRestarting: boolean;
10
+ }
11
+
12
+ export default function SystemInfoSection({
13
+ settings,
14
+ handleRestart,
15
+ isRestarting,
16
+ }: SystemInfoSectionProps) {
17
+ return (
18
+ <section className="pb-4">
19
+ <h3 className="text-sm font-medium text-neutral-400 uppercase tracking-widest mb-4 flex items-center gap-2">
20
+ <Server className="w-4 h-4" />
21
+ System Information
22
+ </h3>
23
+ <div className="bg-white/[0.032] border-[0.5px] border-white/10 rounded-2xl p-6 space-y-6">
24
+ <div className="grid grid-cols-2 gap-12">
25
+ <div className="space-y-4">
26
+ <span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
27
+ Versions
28
+ </span>
29
+ <div className="space-y-3">
30
+ <div className="flex justify-between items-center group/version">
31
+ <span className="text-xs text-neutral-400 group-hover/version:text-neutral-300 transition-colors">
32
+ Dashboard
33
+ </span>
34
+ <code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10 group-hover/version:border-emerald-500/30 group-hover/version:text-emerald-400 transition-all">
35
+ {settings?.dashboardVersion || "v0.7.5"}
36
+ </code>
37
+ </div>
38
+ <div className="flex justify-between items-center group/version">
39
+ <span className="text-xs text-neutral-400 group-hover/version:text-neutral-300 transition-colors">
40
+ MCP Server
41
+ </span>
42
+ <code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10 group-hover/version:border-emerald-500/30 group-hover/version:text-emerald-400 transition-all">
43
+ {settings?.mcpVersion || "v0.7.5"}
44
+ </code>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ <div className="border-l border-white/5 pl-8">
49
+ <span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
50
+ Environment
51
+ </span>
52
+ <div className="space-y-3">
53
+ <div className="flex justify-between items-center">
54
+ <span className="text-xs text-neutral-400">Status</span>
55
+ <code
56
+ className={`text-[13px] font-mono px-2 py-0.5 rounded border ${
57
+ settings?.env === "staging"
58
+ ? "text-yellow-400 bg-yellow-500/10 border-yellow-500/20"
59
+ : "text-emerald-400 bg-emerald-500/10 border-emerald-500/20"
60
+ }`}
61
+ >
62
+ {settings?.env === "staging" ? "Staging" : "Production"}
63
+ </code>
64
+ </div>
65
+ <div className="flex justify-between items-center">
66
+ <span className="text-xs text-neutral-400">Instance</span>
67
+ <code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10">
68
+ {settings?.instanceType === "rpi"
69
+ ? "Raspberry Pi"
70
+ : settings?.instanceType === "vps"
71
+ ? "Cloud / VPS"
72
+ : "Local Machine"}
73
+ </code>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div className="pt-2 border-t border-white/5">
80
+ <TintButton
81
+ tint="sky"
82
+ variant="solid"
83
+ className="w-full h-10"
84
+ onClick={handleRestart}
85
+ disabled={isRestarting}
86
+ >
87
+ {isRestarting ? (
88
+ <Loader2 className="w-4 h-4 animate-spin" />
89
+ ) : (
90
+ <RotateCcw className="w-4 h-4" />
91
+ )}
92
+ {isRestarting ? "Restarting..." : "Restart Service"}
93
+ </TintButton>
94
+ </div>
95
+ </div>
96
+ </section>
97
+ );
98
+ }