@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
package/app/page.tsx CHANGED
@@ -1,167 +1,145 @@
1
1
  "use client";
2
2
 
3
- import AuditLogTable from "@/components/dashboard/audit-log-table";
4
3
  import ChartsSection from "@/components/dashboard/charts-section";
5
4
  import DashboardHeader from "@/components/dashboard/header";
6
5
  import LoginModal from "@/components/dashboard/login-modal";
6
+ import LogViewer from "@/components/dashboard/logs/log-viewer";
7
7
  import MCPConfigModal from "@/components/dashboard/mcp-config-modal";
8
8
  import MetricsGrid from "@/components/dashboard/metrics-grid";
9
9
  import SettingsModal from "@/components/dashboard/settings-modal";
10
10
  import { useDashboard } from "@/lib/data/dashboard-context";
11
- import { DashboardData } from "@/lib/data/types";
11
+ import { useRouter, useSearchParams } from "next/navigation";
12
12
  import { useEffect, useState } from "react";
13
+ import { toast } from "sonner";
13
14
 
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 {
19
- strategy,
20
- isDemo,
21
- toggleDemo,
22
- refreshSignal,
23
- isAuthenticated,
24
- login,
25
- } = useDashboard();
26
-
27
- const [showMCPConfig, setShowMCPConfig] = useState(false);
15
+ export default function DashboardPage() {
16
+ const { stats, logs, loading, refresh, isAuthenticated, login } =
17
+ useDashboard();
28
18
  const [showSettings, setShowSettings] = useState(false);
29
-
30
- // Data State
31
- const [data, setData] = useState<DashboardData>({
32
- stats: {
33
- memoryRecords: 0,
34
- totalClients: 0,
35
- successRate: 0,
36
- totalRequests: 0,
37
- topWriter: { name: "N/A", count: 0 },
38
- topReader: { name: "N/A", count: 0 },
39
- lastWriter: { name: "N/A", timestamp: 0 },
40
- lastReader: { name: "N/A", timestamp: 0 },
41
- },
42
- trends: {
43
- memory: { change: "", trend: "neutral", hasData: false, data: [] },
44
- clients: { change: "", trend: "neutral", hasData: false, data: [] },
45
- success: { change: "", trend: "neutral", hasData: false, data: [] },
46
- requests: { change: "", trend: "neutral", hasData: false, data: [] },
47
- },
48
- logs: [],
49
- });
50
-
51
- // Auth is handled by context (Layout passes initial state from headers)
52
-
53
- const handleLogin = (token: string) => {
54
- login();
55
- };
56
-
57
- // Fetch Data Effect - Reacts to strategy change or refresh signal
58
- useEffect(() => {
59
- async function updateData() {
60
- try {
61
- const potentialData = await strategy.fetchGlobalStats();
62
- setData(potentialData);
63
- } catch (e) {
64
- console.error("Failed to fetch dashboard data:", e);
65
- }
66
- }
67
- updateData();
68
- }, [strategy, refreshSignal]);
69
-
70
- // Audit Log internal state for filtering/sorting (UI logic only)
71
- const [searchTerm, setSearchTerm] = useState("");
19
+ const [showMCPConfig, setShowMCPConfig] = useState(false);
72
20
  const [currentPage, setCurrentPage] = useState(1);
73
- const [sortField, setSortField] = useState<
74
- "date" | "client" | "operation" | "status"
75
- >("date");
21
+ const [sortField, setSortField] = useState("date");
76
22
  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
77
- const itemsPerPage = 10;
78
23
 
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
- };
24
+ const params = useSearchParams();
25
+ const router = useRouter();
26
+ const pageSize = 10;
95
27
 
96
- const sortedLog = [...filteredLog].sort((a, b) => {
97
- const modifier = sortDirection === "asc" ? 1 : -1;
98
- if (sortField === "date") {
99
- return (
100
- (new Date(a.date).getTime() - new Date(b.date).getTime()) * modifier
101
- );
102
- }
103
- const aValue = (a as any)[sortField] || "";
104
- const bValue = (b as any)[sortField] || "";
105
- if (typeof aValue === "string" && typeof bValue === "string") {
106
- return aValue.localeCompare(bValue) * modifier;
28
+ useEffect(() => {
29
+ // Handle login toast
30
+ if (params.get("logged_in") === "true") {
31
+ toast.success("Welcome back, Mikhail!", {
32
+ description: "Authenticated via GitHub Zero Trust",
33
+ });
34
+ // Clean URL
35
+ const newParams = new URLSearchParams(params.toString());
36
+ newParams.delete("logged_in");
37
+ router.replace(`/?${newParams.toString()}`);
107
38
  }
39
+ }, [params, router]);
40
+
41
+ // Handle sorting locally for now since we have a limited set (100 logs)
42
+ const sortedLogs = [...logs].sort((a: any, b: any) => {
43
+ const aVal = a[sortField];
44
+ const bVal = b[sortField];
45
+ if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
46
+ if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
108
47
  return 0;
109
48
  });
110
49
 
111
- const paginatedLog = sortedLog.slice(
112
- (currentPage - 1) * itemsPerPage,
113
- currentPage * itemsPerPage,
50
+ const paginatedLogs = sortedLogs.slice(
51
+ (currentPage - 1) * pageSize,
52
+ currentPage * pageSize,
114
53
  );
115
- const totalPages = Math.ceil(sortedLog.length / itemsPerPage);
116
-
117
- const [mounted, setMounted] = useState(false);
118
-
119
- useEffect(() => {
120
- setMounted(true);
121
- }, []);
122
54
 
123
- // Show login modal if not authenticated
124
- if (!isAuthenticated) {
125
- return <LoginModal onLogin={handleLogin} />;
126
- }
55
+ // Default trends (until backend provides them or we calculate them)
56
+ const metricsTrends = {
57
+ memory: {
58
+ change: "0%",
59
+ trend: "neutral" as const,
60
+ hasData: false,
61
+ data: [],
62
+ },
63
+ clients: {
64
+ change: "0%",
65
+ trend: "neutral" as const,
66
+ hasData: false,
67
+ data: [],
68
+ },
69
+ success: {
70
+ change: "0%",
71
+ trend: "neutral" as const,
72
+ hasData: false,
73
+ data: [],
74
+ },
75
+ requests: {
76
+ change: "0%",
77
+ trend: "neutral" as const,
78
+ hasData: false,
79
+ data: [],
80
+ },
81
+ };
127
82
 
128
- if (!mounted) {
129
- return null; // Return nothing on server and initial client render to avoid mismatch
130
- }
83
+ const currentStats = stats || {
84
+ memoryRecords: 0,
85
+ totalClients: 0,
86
+ successRate: 0,
87
+ totalRequests: 0,
88
+ topWriter: { name: "N/A", count: 0 },
89
+ topReader: { name: "N/A", count: 0 },
90
+ lastWriter: { name: "N/A", timestamp: 0 },
91
+ lastReader: { name: "N/A", timestamp: 0 },
92
+ };
131
93
 
132
94
  return (
133
- <div className="min-h-screen text-foreground">
95
+ <main className="min-h-screen bg-[#0B1116] text-white">
134
96
  <DashboardHeader
135
- onShowMCPConfig={() => setShowMCPConfig(true)}
136
97
  onShowSettings={() => setShowSettings(true)}
98
+ onShowMCPConfig={() => setShowMCPConfig(true)}
99
+ memoryCount={currentStats.memoryRecords}
137
100
  />
138
101
 
139
- <main className="px-6 py-8 max-w-7xl mx-auto space-y-8">
140
- <MetricsGrid stats={data.stats} trends={data.trends} />
141
- <ChartsSection period="" />
142
- <AuditLogTable
143
- logs={(paginatedLog || []).map((log) => ({
144
- id: log.id,
145
- date: log.date.toLocaleString(),
146
- client: log.client,
147
- operation: log.operation,
148
- status: log.status,
149
- description: log.description,
150
- }))}
151
- loading={false}
102
+ {!isAuthenticated && (
103
+ <LoginModal
104
+ onLogin={(token) => {
105
+ // Note: Cookie is set server-side via /api/auth/token with HttpOnly flag.
106
+ // Client-side we only trigger state refresh.
107
+ login();
108
+ refresh();
109
+ }}
110
+ />
111
+ )}
112
+
113
+ <div className="max-w-7xl mx-auto px-6 pt-32 pb-20 space-y-12 animate-in fade-in duration-700">
114
+ <MetricsGrid
115
+ stats={currentStats}
116
+ trends={metricsTrends}
117
+ loading={loading}
118
+ />
119
+ <ChartsSection period="all" />
120
+ <LogViewer
121
+ logs={paginatedLogs}
122
+ loading={loading}
152
123
  currentPage={currentPage}
153
- totalPages={totalPages}
124
+ totalPages={Math.ceil(logs.length / pageSize)}
154
125
  onPageChange={setCurrentPage}
155
126
  sortField={sortField}
156
127
  sortDirection={sortDirection}
157
- onSort={handleSort}
128
+ onSort={(field) => {
129
+ if (field === sortField) {
130
+ setSortDirection(sortDirection === "asc" ? "desc" : "asc");
131
+ } else {
132
+ setSortField(field);
133
+ setSortDirection("desc");
134
+ }
135
+ }}
158
136
  />
159
- </main>
137
+ </div>
160
138
 
139
+ {showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
161
140
  {showMCPConfig && (
162
141
  <MCPConfigModal onClose={() => setShowMCPConfig(false)} />
163
142
  )}
164
- {showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
165
- </div>
143
+ </main>
166
144
  );
167
145
  }
@@ -7,7 +7,7 @@ import dynamic from "next/dynamic";
7
7
  import { useEffect, useRef, useState } from "react";
8
8
 
9
9
  // Dynamic import with SSR disabled
10
- const MetricsChart = dynamic(() => import("./metrics-chart"), { ssr: false });
10
+ const MemoryChart = dynamic(() => import("./memory-chart"), { ssr: false });
11
11
 
12
12
  interface ChartCardProps {
13
13
  service: string;
@@ -36,7 +36,7 @@ const periods = [
36
36
  ];
37
37
 
38
38
  export default function ChartCard({ service }: ChartCardProps) {
39
- const { strategy, refreshSignal, clientConfigs } = useDashboard();
39
+ const { refreshSignal, clientConfigs } = useDashboard();
40
40
  const [period, setPeriod] = useState("24h");
41
41
  const [hovered, setHovered] = useState<string | null>(null);
42
42
  const [data, setData] = useState<any[]>([]);
@@ -47,27 +47,28 @@ export default function ChartCard({ service }: ChartCardProps) {
47
47
  const isInitialLoad = useRef(true);
48
48
  useEffect(() => {
49
49
  async function fetchData() {
50
- // Only show loading state on initial load, not background refresh
51
50
  if (isInitialLoad.current) setLoading(true);
52
51
 
53
52
  try {
54
- const timeSeriesData = await strategy.getChartData(period);
53
+ const res = await fetch(`/api/metrics?period=${period}`);
54
+ if (!res.ok) throw new Error("Failed to fetch metrics");
55
+ const metrics = await res.json();
56
+ const timeSeriesData = metrics.timeSeries;
55
57
 
56
58
  // Update client metadata if provided in response
57
- if (timeSeriesData.metadata) {
59
+ if (metrics.metadata) {
58
60
  setClientMetadata((prev) => ({
59
61
  ...prev,
60
- ...timeSeriesData.metadata,
62
+ ...metrics.metadata,
61
63
  }));
62
64
  }
63
65
 
64
66
  // Helper to format time based on period
65
67
  const formatSeries = (series: any[]) => {
66
68
  if (!series) return [];
67
- return series.map((point) => {
69
+ return series.map((point: any) => {
68
70
  const date = new Date((point.time as number) * 1000);
69
71
  let timeStr = "";
70
- // Show date if period is longer than 24h
71
72
  if (["7d", "30d", "90d", "all"].includes(period)) {
72
73
  timeStr =
73
74
  date.toLocaleDateString([], {
@@ -85,19 +86,14 @@ export default function ChartCard({ service }: ChartCardProps) {
85
86
  minute: "2-digit",
86
87
  });
87
88
  }
88
- return {
89
- ...point,
90
- time: timeStr,
91
- };
89
+ return { ...point, time: timeStr };
92
90
  });
93
91
  };
94
92
 
95
- // Extract client names from series and sort by total value (Ascending)
96
93
  const getClients = (series: any[]) => {
97
94
  if (!series || series.length === 0) return [];
98
95
  const keys = new Set<string>();
99
96
  const totals: Record<string, number> = {};
100
-
101
97
  series.forEach((point) => {
102
98
  Object.keys(point).forEach((k) => {
103
99
  if (k !== "time") {
@@ -106,13 +102,11 @@ export default function ChartCard({ service }: ChartCardProps) {
106
102
  }
107
103
  });
108
104
  });
109
-
110
105
  return Array.from(keys).sort(
111
106
  (a, b) => (totals[a] || 0) - (totals[b] || 0),
112
107
  );
113
108
  };
114
109
 
115
- // Get data based on service type
116
110
  let seriesData: any[] = [];
117
111
  let clients: string[] = [];
118
112
 
@@ -140,12 +134,12 @@ export default function ChartCard({ service }: ChartCardProps) {
140
134
  }
141
135
  }
142
136
  fetchData();
143
- }, [period, service, strategy, refreshSignal]);
137
+ }, [period, service, refreshSignal]);
144
138
 
145
139
  const isMultiSeries = clientNames.length > 0;
146
140
 
147
141
  return (
148
- <Card className="bg-white/5 border-white/10 backdrop-blur-md relative overflow-visible pt-6 pb-2">
142
+ <Card className="card bg-white/5 border-white/10 backdrop-blur-md relative overflow-visible pt-6 pb-2">
149
143
  <button
150
144
  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"
151
145
  onClick={() =>
@@ -256,7 +250,7 @@ export default function ChartCard({ service }: ChartCardProps) {
256
250
  </div>
257
251
  ) : (
258
252
  <div className="h-[200px] w-full">
259
- <MetricsChart
253
+ <MemoryChart
260
254
  data={data}
261
255
  isMultiSeries={isMultiSeries}
262
256
  clientNames={clientNames}
@@ -34,7 +34,7 @@ interface MetricsChartProps {
34
34
  setHovered: (id: string | null) => void;
35
35
  }
36
36
 
37
- export default function MetricsChart({
37
+ export default function MemoryChart({
38
38
  data,
39
39
  isMultiSeries,
40
40
  clientNames,
@@ -1,6 +1,6 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import ChartCard from "./chart-card";
3
+ import ChartCard from "./charts/chart-card";
4
4
 
5
5
  export default function ChartsSection({ period }: { period: string }) {
6
6
  // Remove the period prop since each chart will have its own selector
@@ -12,5 +12,5 @@ export default function ChartsSection({ period }: { period: string }) {
12
12
  <ChartCard service="Updates by Client" />
13
13
  <ChartCard service="Deletes by Client" />
14
14
  </div>
15
- )
15
+ );
16
16
  }