@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.
- package/Dockerfile +3 -3
- package/app/api/audit-logs/route.ts +12 -6
- package/app/api/health/route.ts +2 -1
- package/app/api/mcp-config/route.ts +128 -0
- package/app/api/metrics/route.ts +22 -70
- package/app/api/settings/route.ts +125 -30
- package/app/page.tsx +105 -127
- package/components/dashboard/{chart-card.tsx → charts/chart-card.tsx} +13 -19
- package/components/dashboard/{metrics-chart.tsx → charts/memory-chart.tsx} +1 -1
- package/components/dashboard/charts-section.tsx +3 -3
- package/components/dashboard/header.tsx +177 -176
- package/components/dashboard/{audit-log-table.tsx → logs/log-viewer.tsx} +12 -7
- package/components/dashboard/mcp/config-preview.tsx +246 -0
- package/components/dashboard/mcp/platform-selector.tsx +96 -0
- package/components/dashboard/mcp-config-modal.tsx +97 -503
- package/components/dashboard/{metric-card.tsx → metrics/stat-card.tsx} +4 -2
- package/components/dashboard/metrics-grid.tsx +10 -2
- package/components/dashboard/settings/access-token-section.tsx +131 -0
- package/components/dashboard/settings/data-management-section.tsx +122 -0
- package/components/dashboard/settings/system-info-section.tsx +98 -0
- package/components/dashboard/settings-modal.tsx +55 -299
- package/e2e/api.spec.ts +219 -0
- package/e2e/routing.spec.ts +39 -0
- package/e2e/ui.spec.ts +373 -0
- package/lib/data/dashboard-context.tsx +96 -29
- package/lib/data/types.ts +32 -38
- package/middleware.ts +31 -13
- package/package.json +6 -1
- package/playwright.config.ts +23 -58
- package/public/clients.json +5 -3
- package/release-reports/assets/local/1_dashboard.png +0 -0
- package/release-reports/assets/local/2_audit_logs.png +0 -0
- package/release-reports/assets/local/3_charts.png +0 -0
- package/release-reports/assets/local/4_mcp_modal.png +0 -0
- package/release-reports/assets/local/5_settings_modal.png +0 -0
- package/lib/data/demo-strategy.ts +0 -110
- package/lib/data/production-strategy.ts +0 -191
- package/lib/prometheus/client.ts +0 -58
- package/lib/prometheus/index.ts +0 -6
- package/lib/prometheus/metrics.ts +0 -234
- package/lib/prometheus/sparklines.ts +0 -71
- package/lib/prometheus/timeseries.ts +0 -305
- 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 {
|
|
11
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
12
12
|
import { useEffect, useState } from "react";
|
|
13
|
+
import { toast } from "sonner";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
112
|
-
(currentPage - 1) *
|
|
113
|
-
currentPage *
|
|
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
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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={
|
|
124
|
+
totalPages={Math.ceil(logs.length / pageSize)}
|
|
154
125
|
onPageChange={setCurrentPage}
|
|
155
126
|
sortField={sortField}
|
|
156
127
|
sortDirection={sortDirection}
|
|
157
|
-
onSort={
|
|
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
|
-
</
|
|
137
|
+
</div>
|
|
160
138
|
|
|
139
|
+
{showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
|
|
161
140
|
{showMCPConfig && (
|
|
162
141
|
<MCPConfigModal onClose={() => setShowMCPConfig(false)} />
|
|
163
142
|
)}
|
|
164
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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 (
|
|
59
|
+
if (metrics.metadata) {
|
|
58
60
|
setClientMetadata((prev) => ({
|
|
59
61
|
...prev,
|
|
60
|
-
...
|
|
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,
|
|
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
|
-
<
|
|
253
|
+
<MemoryChart
|
|
260
254
|
data={data}
|
|
261
255
|
isMultiSeries={isMultiSeries}
|
|
262
256
|
clientNames={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
|
}
|