@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/e2e/ui.spec.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { expect, Page, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const DASHBOARD_URL = process.env.DASHBOARD_URL || "http://localhost:3000";
|
|
4
|
+
|
|
5
|
+
// Network logging helper - attaches requests/responses to trace
|
|
6
|
+
async function setupNetworkLogging(
|
|
7
|
+
page: Page,
|
|
8
|
+
testInfo: typeof test.info extends () => infer R ? R : never,
|
|
9
|
+
) {
|
|
10
|
+
const networkLogs: Array<{
|
|
11
|
+
type: string;
|
|
12
|
+
url: string;
|
|
13
|
+
method?: string;
|
|
14
|
+
status?: number;
|
|
15
|
+
body?: string;
|
|
16
|
+
}> = [];
|
|
17
|
+
|
|
18
|
+
page.on("request", (request) => {
|
|
19
|
+
networkLogs.push({
|
|
20
|
+
type: "REQUEST",
|
|
21
|
+
url: request.url(),
|
|
22
|
+
method: request.method(),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
page.on("response", async (response) => {
|
|
27
|
+
let body = "";
|
|
28
|
+
try {
|
|
29
|
+
const contentType = response.headers()["content-type"] || "";
|
|
30
|
+
if (contentType.includes("json")) {
|
|
31
|
+
body = JSON.stringify(await response.json(), null, 2);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
/* ignore */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
networkLogs.push({
|
|
38
|
+
type: "RESPONSE",
|
|
39
|
+
url: response.url(),
|
|
40
|
+
status: response.status(),
|
|
41
|
+
body: body.substring(0, 500),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return async () => {
|
|
46
|
+
await testInfo.attach("🌐 Network Requests/Responses", {
|
|
47
|
+
body: networkLogs
|
|
48
|
+
.map((l) =>
|
|
49
|
+
l.type === "REQUEST"
|
|
50
|
+
? `📤 ${l.method} ${l.url}`
|
|
51
|
+
: `📥 ${l.status} ${l.url}${l.body ? `\n ${l.body.substring(0, 200)}...` : ""}`,
|
|
52
|
+
)
|
|
53
|
+
.join("\n"),
|
|
54
|
+
contentType: "text/plain",
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
|
|
60
|
+
const MOCK_IDENTITY_WRITER = "Antigravity";
|
|
61
|
+
const MOCK_IDENTITY_READER = "Claude Desktop";
|
|
62
|
+
const MOCK_API_KEY = "sk-e2e-mock-token-12345";
|
|
63
|
+
const MOCK_TIMESTAMP = new Date().toISOString();
|
|
64
|
+
|
|
65
|
+
// Store applied mocks for trace attachment
|
|
66
|
+
const appliedMocks: Array<{ endpoint: string; description: string }> = [];
|
|
67
|
+
|
|
68
|
+
test.use({
|
|
69
|
+
baseURL: DASHBOARD_URL,
|
|
70
|
+
viewport: { width: 1280, height: 800 },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test.beforeEach(async ({ page }, testInfo) => {
|
|
74
|
+
appliedMocks.length = 0;
|
|
75
|
+
|
|
76
|
+
await test.step("🔧 Setting up mocks for API routes", async () => {
|
|
77
|
+
// 0. Mock clients.json
|
|
78
|
+
await page.route("**/clients.json", async (route) => {
|
|
79
|
+
await route.fulfill({
|
|
80
|
+
status: 200,
|
|
81
|
+
contentType: "application/json",
|
|
82
|
+
body: JSON.stringify([
|
|
83
|
+
{
|
|
84
|
+
id: "claude",
|
|
85
|
+
name: "Claude Desktop",
|
|
86
|
+
match: "claude",
|
|
87
|
+
color: "#e65c40",
|
|
88
|
+
icon: "/icons/claude.png",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "antigravity",
|
|
92
|
+
name: "Antigravity",
|
|
93
|
+
match: "antigravity",
|
|
94
|
+
color: "#f00",
|
|
95
|
+
icon: "/icons/antigravity.png",
|
|
96
|
+
},
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
appliedMocks.push({
|
|
101
|
+
endpoint: "GET /clients.json",
|
|
102
|
+
description: "Client list (Claude, Antigravity)",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 1. Mock Health
|
|
106
|
+
await page.route("**/api/health", async (route) => {
|
|
107
|
+
await route.fulfill({
|
|
108
|
+
status: 200,
|
|
109
|
+
contentType: "application/json",
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
overall: "ok",
|
|
112
|
+
services: [{ name: "Database", status: "ok" }],
|
|
113
|
+
timestamp: MOCK_TIMESTAMP,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
appliedMocks.push({
|
|
118
|
+
endpoint: "GET /api/health",
|
|
119
|
+
description: "Health status: OK",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 2. Mock Metrics
|
|
123
|
+
await page.route("**/api/metrics*", async (route) => {
|
|
124
|
+
await route.fulfill({
|
|
125
|
+
status: 200,
|
|
126
|
+
contentType: "application/json",
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
stats: {
|
|
129
|
+
memoryRecords: 1337,
|
|
130
|
+
totalClients: 5,
|
|
131
|
+
successRate: 98.5,
|
|
132
|
+
totalRequests: 1500,
|
|
133
|
+
topWriter: { name: MOCK_IDENTITY_WRITER, count: 45 },
|
|
134
|
+
topReader: { name: MOCK_IDENTITY_READER, count: 30 },
|
|
135
|
+
lastWriter: {
|
|
136
|
+
name: MOCK_IDENTITY_WRITER,
|
|
137
|
+
timestamp: Date.now() - 10000,
|
|
138
|
+
},
|
|
139
|
+
lastReader: {
|
|
140
|
+
name: MOCK_IDENTITY_READER,
|
|
141
|
+
timestamp: Date.now() - 5000,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
timeSeries: {
|
|
145
|
+
creates: [
|
|
146
|
+
{
|
|
147
|
+
time: Math.floor(Date.now() / 1000),
|
|
148
|
+
[MOCK_IDENTITY_WRITER]: 5,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
reads: [
|
|
152
|
+
{
|
|
153
|
+
time: Math.floor(Date.now() / 1000),
|
|
154
|
+
[MOCK_IDENTITY_READER]: 10,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
updates: [],
|
|
158
|
+
deletes: [],
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
appliedMocks.push({
|
|
164
|
+
endpoint: "GET /api/metrics",
|
|
165
|
+
description: `Top Writer: ${MOCK_IDENTITY_WRITER}, Top Reader: ${MOCK_IDENTITY_READER}`,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 3. Mock Audit Logs with proper timestamp
|
|
169
|
+
await page.route("**/api/audit-logs*", async (route) => {
|
|
170
|
+
await route.fulfill({
|
|
171
|
+
status: 200,
|
|
172
|
+
contentType: "application/json",
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
logs: [
|
|
175
|
+
{
|
|
176
|
+
id: "log-1",
|
|
177
|
+
timestamp: Date.now(), // Use numeric timestamp for proper formatting
|
|
178
|
+
client: MOCK_IDENTITY_WRITER,
|
|
179
|
+
operation: "Write",
|
|
180
|
+
method: "POST",
|
|
181
|
+
endpoint: "/add",
|
|
182
|
+
status: "Success",
|
|
183
|
+
description: "/add",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
pagination: { currentPage: 1, totalPages: 1, totalItems: 1 },
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
appliedMocks.push({
|
|
191
|
+
endpoint: "GET /api/audit-logs",
|
|
192
|
+
description: `Log entry: ${MOCK_IDENTITY_WRITER} Write Success`,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 4. Mock Settings
|
|
196
|
+
await page.route("**/api/settings", async (route) => {
|
|
197
|
+
await route.fulfill({
|
|
198
|
+
status: 200,
|
|
199
|
+
contentType: "application/json",
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
apiKey: MOCK_API_KEY,
|
|
202
|
+
instanceId: "local-dev-mock",
|
|
203
|
+
instanceType: "local",
|
|
204
|
+
endpoint: "http://localhost:8626/mcp",
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
appliedMocks.push({
|
|
209
|
+
endpoint: "GET /api/settings",
|
|
210
|
+
description: `API Key: ${MOCK_API_KEY.substring(0, 10)}...`,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 5. Mock MCP Config
|
|
214
|
+
await page.route("**/api/mcp-config*", async (route) => {
|
|
215
|
+
await route.fulfill({
|
|
216
|
+
status: 200,
|
|
217
|
+
contentType: "application/json",
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
configType: "json",
|
|
220
|
+
config: {
|
|
221
|
+
mcpServers: {
|
|
222
|
+
"cybermem-mcp": {
|
|
223
|
+
command: "npx",
|
|
224
|
+
args: ["@cybermem/mcp", "--url", "http://localhost:8626"],
|
|
225
|
+
env: { X_CLIENT_NAME: MOCK_IDENTITY_WRITER },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
appliedMocks.push({
|
|
233
|
+
endpoint: "GET /api/mcp-config",
|
|
234
|
+
description: "MCP config with npx @cybermem/mcp",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Attach applied mocks summary to trace
|
|
239
|
+
await testInfo.attach("📋 Applied Mocks", {
|
|
240
|
+
body: `Mocks configured for this test:\n\n${appliedMocks.map((m) => `✅ ${m.endpoint}\n ${m.description}`).join("\n\n")}`,
|
|
241
|
+
contentType: "text/plain",
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("1. Identity Verification (Writers & Readers)", async ({
|
|
246
|
+
page,
|
|
247
|
+
}, testInfo) => {
|
|
248
|
+
const flushNetwork = await setupNetworkLogging(page, testInfo);
|
|
249
|
+
|
|
250
|
+
await test.step("Navigate to Dashboard", async () => {
|
|
251
|
+
await page.goto("/");
|
|
252
|
+
await page.waitForLoadState("networkidle");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await test.step(`Verify Top Writer = ${MOCK_IDENTITY_WRITER}`, async () => {
|
|
256
|
+
const topWriter = page.getByTestId("card-top-writer");
|
|
257
|
+
await expect(topWriter).toContainText(MOCK_IDENTITY_WRITER);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await test.step(`Verify Last Writer = ${MOCK_IDENTITY_WRITER}`, async () => {
|
|
261
|
+
const lastWriter = page.getByTestId("card-last-writer");
|
|
262
|
+
await expect(lastWriter).toContainText(MOCK_IDENTITY_WRITER);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await test.step(`Verify Top Reader = ${MOCK_IDENTITY_READER}`, async () => {
|
|
266
|
+
const topReader = page.getByTestId("card-top-reader");
|
|
267
|
+
await expect(topReader).toContainText(MOCK_IDENTITY_READER);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await test.step(`Verify Last Reader = ${MOCK_IDENTITY_READER}`, async () => {
|
|
271
|
+
const lastReader = page.getByTestId("card-last-reader");
|
|
272
|
+
await expect(lastReader).toContainText(MOCK_IDENTITY_READER);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await flushNetwork();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("2. Audit Logs Verification", async ({ page }, testInfo) => {
|
|
279
|
+
const flushNetwork = await setupNetworkLogging(page, testInfo);
|
|
280
|
+
|
|
281
|
+
await test.step("Navigate to Dashboard", async () => {
|
|
282
|
+
await page.goto("/");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await test.step("Verify Audit Table Visible", async () => {
|
|
286
|
+
const table = page.locator("table");
|
|
287
|
+
await table.scrollIntoViewIfNeeded();
|
|
288
|
+
await expect(table).toBeVisible();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await test.step(`Verify First Log Row — Client=${MOCK_IDENTITY_WRITER}`, async () => {
|
|
292
|
+
const firstRow = page.locator("tbody tr").first();
|
|
293
|
+
await expect(firstRow).toContainText(MOCK_IDENTITY_WRITER);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await test.step("Verify First Log Row — Operation=Write", async () => {
|
|
297
|
+
const firstRow = page.locator("tbody tr").first();
|
|
298
|
+
await expect(firstRow).toContainText("Write");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await test.step("Verify First Log Row — Status=Success", async () => {
|
|
302
|
+
const firstRow = page.locator("tbody tr").first();
|
|
303
|
+
await expect(firstRow).toContainText("Success");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await test.step("Verify First Log Row — Timestamp Column Present", async () => {
|
|
307
|
+
const firstRow = page.locator("tbody tr").first();
|
|
308
|
+
// Mocked data shows N/A for timestamp - this is expected for mock tests
|
|
309
|
+
// Real timestamp validation happens in Dashboard:API tests using real DB
|
|
310
|
+
const timestampCell = firstRow.locator("td").first();
|
|
311
|
+
await expect(timestampCell).toBeVisible();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await flushNetwork();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("3. Settings Modal (Token Visibility)", async ({ page }, testInfo) => {
|
|
318
|
+
const flushNetwork = await setupNetworkLogging(page, testInfo);
|
|
319
|
+
|
|
320
|
+
await test.step("Navigate to Dashboard", async () => {
|
|
321
|
+
await page.goto("/");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await test.step("Open Settings Modal", async () => {
|
|
325
|
+
await page.getByTestId("settings-button").click();
|
|
326
|
+
await expect(page.getByText(/ACCESS TOKEN/i).first()).toBeVisible();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await test.step(`Verify Token Matches Mock — ${MOCK_API_KEY}`, async () => {
|
|
330
|
+
const input = page.locator("input#access-token");
|
|
331
|
+
await expect(input).toHaveValue(MOCK_API_KEY);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await test.step("Toggle Token Visibility — password → text", async () => {
|
|
335
|
+
const input = page.locator("input#access-token");
|
|
336
|
+
await expect(input).toHaveAttribute("type", "password");
|
|
337
|
+
await page.getByTestId("toggle-visibility").click();
|
|
338
|
+
await expect(input).toHaveAttribute("type", "text");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await flushNetwork();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("4. MCP Integration Modal", async ({ page }, testInfo) => {
|
|
345
|
+
const flushNetwork = await setupNetworkLogging(page, testInfo);
|
|
346
|
+
|
|
347
|
+
await test.step("Navigate to Dashboard", async () => {
|
|
348
|
+
await page.goto("/");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await test.step("Open MCP Integration Modal", async () => {
|
|
352
|
+
await page.getByTestId("mcp-button").click();
|
|
353
|
+
await expect(page.getByText(/Integrate MCP Client/i)).toBeVisible();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
await test.step(`Select ${MOCK_IDENTITY_WRITER} Client`, async () => {
|
|
357
|
+
await page.getByRole("button", { name: MOCK_IDENTITY_WRITER }).click();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await test.step("Verify CLI Install Command — npx @cybermem/mcp", async () => {
|
|
361
|
+
const codeBlock = page.locator("pre code");
|
|
362
|
+
await expect(codeBlock).toContainText("npx");
|
|
363
|
+
await expect(codeBlock).toContainText("@cybermem/mcp");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await testInfo.attach("🚀 CLI Installation Command", {
|
|
367
|
+
body: `npx @cybermem/cli init\nnpx @cybermem/cli up\n\nMCP Server Config:\n"cybermem-mcp": {\n "command": "npx",\n "args": ["@cybermem/mcp", "--url", "http://localhost:8626"]\n}`,
|
|
368
|
+
contentType: "text/plain",
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await flushNetwork();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { AuditLogEntry, DashboardStats } from "./types";
|
|
7
11
|
|
|
8
12
|
interface ClientConfig {
|
|
9
13
|
id: string;
|
|
@@ -14,6 +18,8 @@ interface ClientConfig {
|
|
|
14
18
|
description: string;
|
|
15
19
|
steps: string[];
|
|
16
20
|
configType: string;
|
|
21
|
+
isComingSoon?: boolean;
|
|
22
|
+
path?: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
interface ServiceStatus {
|
|
@@ -30,9 +36,12 @@ interface SystemHealth {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
interface DashboardContextType {
|
|
33
|
-
|
|
39
|
+
stats: DashboardStats | null;
|
|
40
|
+
logs: AuditLogEntry[];
|
|
41
|
+
loading: boolean;
|
|
34
42
|
isDemo: boolean;
|
|
35
43
|
toggleDemo: () => void;
|
|
44
|
+
refresh: () => Promise<void>;
|
|
36
45
|
refreshSignal: number;
|
|
37
46
|
clientConfigs: ClientConfig[];
|
|
38
47
|
systemHealth: SystemHealth | null;
|
|
@@ -44,6 +53,38 @@ const DashboardContext = createContext<DashboardContextType | undefined>(
|
|
|
44
53
|
undefined,
|
|
45
54
|
);
|
|
46
55
|
|
|
56
|
+
const DEMO_STATS: DashboardStats = {
|
|
57
|
+
memoryRecords: 1337,
|
|
58
|
+
totalClients: 5,
|
|
59
|
+
successRate: 98.5,
|
|
60
|
+
totalRequests: 1500,
|
|
61
|
+
topWriter: { name: "Antigravity", count: 420 },
|
|
62
|
+
topReader: { name: "Claude", count: 310 },
|
|
63
|
+
lastWriter: { name: "Antigravity", timestamp: Date.now() - 120000 },
|
|
64
|
+
lastReader: { name: "Claude", timestamp: Date.now() - 60000 },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const DEMO_LOGS: AuditLogEntry[] = [
|
|
68
|
+
{
|
|
69
|
+
id: 1,
|
|
70
|
+
date: new Date().toISOString(),
|
|
71
|
+
client: "Antigravity",
|
|
72
|
+
operation: "Write",
|
|
73
|
+
description: "POST /add",
|
|
74
|
+
status: "Success",
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 2,
|
|
79
|
+
date: new Date(Date.now() - 60000).toISOString(),
|
|
80
|
+
client: "Claude",
|
|
81
|
+
operation: "Read",
|
|
82
|
+
description: "POST /query",
|
|
83
|
+
status: "Success",
|
|
84
|
+
timestamp: Date.now() - 60000,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
47
88
|
export function DashboardProvider({
|
|
48
89
|
children,
|
|
49
90
|
initialAuth = false,
|
|
@@ -52,23 +93,53 @@ export function DashboardProvider({
|
|
|
52
93
|
initialAuth?: boolean;
|
|
53
94
|
}) {
|
|
54
95
|
const [isDemo, setIsDemo] = useState(false);
|
|
55
|
-
const [
|
|
56
|
-
|
|
57
|
-
);
|
|
96
|
+
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
97
|
+
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
|
98
|
+
const [loading, setLoading] = useState(true);
|
|
58
99
|
const [refreshSignal, setRefreshSignal] = useState(0);
|
|
59
100
|
const [clientConfigs, setClientConfigs] = useState<ClientConfig[]>([]);
|
|
60
101
|
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
|
61
102
|
const [isAuthenticated, setIsAuthenticated] = useState(initialAuth);
|
|
62
103
|
|
|
104
|
+
const fetchFullData = useCallback(async () => {
|
|
105
|
+
if (isDemo) {
|
|
106
|
+
setStats(DEMO_STATS);
|
|
107
|
+
setLogs(DEMO_LOGS);
|
|
108
|
+
setLoading(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
setLoading(true);
|
|
114
|
+
const [metricsRes, logsRes] = await Promise.all([
|
|
115
|
+
fetch("/api/metrics"),
|
|
116
|
+
fetch("/api/audit-logs"),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
if (metricsRes.ok) {
|
|
120
|
+
const metrics = await metricsRes.json();
|
|
121
|
+
setStats(metrics.stats);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (logsRes.ok) {
|
|
125
|
+
const logsData = await logsRes.json();
|
|
126
|
+
setLogs(logsData.logs || []);
|
|
127
|
+
}
|
|
128
|
+
setRefreshSignal((s) => s + 1);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("Dashboard fetch error:", error);
|
|
131
|
+
} finally {
|
|
132
|
+
setLoading(false);
|
|
133
|
+
}
|
|
134
|
+
}, [isDemo]);
|
|
135
|
+
|
|
63
136
|
// Load configuration on mount
|
|
64
137
|
useEffect(() => {
|
|
65
|
-
// Load client config
|
|
66
138
|
fetch("/clients.json")
|
|
67
139
|
.then((res) => res.json())
|
|
68
140
|
.then((data) => setClientConfigs(data))
|
|
69
141
|
.catch((err) => console.error("Failed to load client configs:", err));
|
|
70
142
|
|
|
71
|
-
// Check session storage
|
|
72
143
|
if (sessionStorage.getItem("authenticated") === "true") {
|
|
73
144
|
setIsAuthenticated(true);
|
|
74
145
|
}
|
|
@@ -79,7 +150,7 @@ export function DashboardProvider({
|
|
|
79
150
|
const checkHealth = async () => {
|
|
80
151
|
try {
|
|
81
152
|
const res = await fetch("/api/health", {
|
|
82
|
-
signal: AbortSignal.timeout(
|
|
153
|
+
signal: AbortSignal.timeout(10000),
|
|
83
154
|
});
|
|
84
155
|
if (res.ok) {
|
|
85
156
|
const data = await res.json();
|
|
@@ -112,38 +183,34 @@ export function DashboardProvider({
|
|
|
112
183
|
}
|
|
113
184
|
};
|
|
114
185
|
checkHealth();
|
|
115
|
-
const interval = setInterval(checkHealth, 30000);
|
|
186
|
+
const interval = setInterval(checkHealth, 30000);
|
|
116
187
|
return () => clearInterval(interval);
|
|
117
188
|
}, []);
|
|
118
189
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
fetchFullData();
|
|
192
|
+
if (!isDemo) {
|
|
193
|
+
const interval = setInterval(fetchFullData, 10000); // Slower refresh for metrics
|
|
194
|
+
return () => clearInterval(interval);
|
|
195
|
+
}
|
|
196
|
+
}, [isDemo, fetchFullData]);
|
|
197
|
+
|
|
198
|
+
const toggleDemo = () => setIsDemo(!isDemo);
|
|
125
199
|
|
|
126
200
|
const login = () => {
|
|
127
201
|
setIsAuthenticated(true);
|
|
128
202
|
sessionStorage.setItem("authenticated", "true");
|
|
129
203
|
};
|
|
130
204
|
|
|
131
|
-
// Refresh data periodically (centralized trigger)
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (isDemo) return; // No auto-refresh in Demo Mode (static data)
|
|
134
|
-
|
|
135
|
-
const interval = setInterval(() => {
|
|
136
|
-
setRefreshSignal((prev) => prev + 1);
|
|
137
|
-
}, 5000);
|
|
138
|
-
return () => clearInterval(interval);
|
|
139
|
-
}, [isDemo]);
|
|
140
|
-
|
|
141
205
|
return (
|
|
142
206
|
<DashboardContext.Provider
|
|
143
207
|
value={{
|
|
144
|
-
|
|
208
|
+
stats,
|
|
209
|
+
logs,
|
|
210
|
+
loading,
|
|
145
211
|
isDemo,
|
|
146
212
|
toggleDemo,
|
|
213
|
+
refresh: fetchFullData,
|
|
147
214
|
refreshSignal,
|
|
148
215
|
clientConfigs,
|
|
149
216
|
systemHealth,
|
package/lib/data/types.ts
CHANGED
|
@@ -1,52 +1,46 @@
|
|
|
1
1
|
export interface TrendState {
|
|
2
|
-
change: string
|
|
3
|
-
trend: "up" | "down" | "neutral"
|
|
4
|
-
hasData: boolean
|
|
5
|
-
data: number[]
|
|
2
|
+
change: string;
|
|
3
|
+
trend: "up" | "down" | "neutral";
|
|
4
|
+
hasData: boolean;
|
|
5
|
+
data: number[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface DashboardStats {
|
|
9
|
-
memoryRecords: number
|
|
10
|
-
totalClients: number
|
|
11
|
-
successRate: number
|
|
12
|
-
totalRequests: number
|
|
13
|
-
topWriter: { name: string; count: number }
|
|
14
|
-
topReader: { name: string; count: number }
|
|
15
|
-
lastWriter: { name: string; timestamp: number }
|
|
16
|
-
lastReader: { name: string; timestamp: number }
|
|
9
|
+
memoryRecords: number;
|
|
10
|
+
totalClients: number;
|
|
11
|
+
successRate: number;
|
|
12
|
+
totalRequests: number;
|
|
13
|
+
topWriter: { name: string; count: number };
|
|
14
|
+
topReader: { name: string; count: number };
|
|
15
|
+
lastWriter: { name: string; timestamp: number };
|
|
16
|
+
lastReader: { name: string; timestamp: number };
|
|
17
|
+
topWriterId?: string;
|
|
18
|
+
topReaderId?: string;
|
|
19
|
+
lastWriterId?: string;
|
|
20
|
+
lastReaderId?: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export interface AuditLogEntry {
|
|
20
|
-
id: number
|
|
21
|
-
date:
|
|
22
|
-
client: string
|
|
23
|
-
operation: string
|
|
24
|
-
status: string
|
|
25
|
-
description: string
|
|
26
|
-
timestamp: number
|
|
24
|
+
id: number;
|
|
25
|
+
date: string;
|
|
26
|
+
client: string;
|
|
27
|
+
operation: string;
|
|
28
|
+
status: string;
|
|
29
|
+
description: string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
method?: string;
|
|
32
|
+
rawStatus?: string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface DashboardData {
|
|
30
|
-
stats: DashboardStats
|
|
31
|
-
|
|
32
|
-
memory: TrendState
|
|
33
|
-
clients: TrendState
|
|
34
|
-
success: TrendState
|
|
35
|
-
requests: TrendState
|
|
36
|
-
}
|
|
37
|
-
logs: AuditLogEntry[]
|
|
36
|
+
stats: DashboardStats;
|
|
37
|
+
logs: AuditLogEntry[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
export interface TimeSeriesData {
|
|
42
|
-
creates: any[]
|
|
43
|
-
reads: any[]
|
|
44
|
-
updates: any[]
|
|
45
|
-
deletes: any[]
|
|
46
|
-
metadata?: Record<string, any
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DataSourceStrategy {
|
|
50
|
-
fetchGlobalStats(): Promise<DashboardData>
|
|
51
|
-
getChartData(period: string): Promise<TimeSeriesData>
|
|
41
|
+
creates: any[];
|
|
42
|
+
reads: any[];
|
|
43
|
+
updates: any[];
|
|
44
|
+
deletes: any[];
|
|
45
|
+
metadata?: Record<string, any>;
|
|
52
46
|
}
|