@cybermem/dashboard 0.5.16 → 0.8.7

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.
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { Input } from "@/components/ui/input";
4
+ import { TintButton } from "@/components/ui/tint-button";
5
+ import { AlertTriangle, X } from "lucide-react";
6
+ import { useState } from "react";
7
+
8
+ interface ConfirmationModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ onConfirm: () => void;
12
+ title: string;
13
+ description: string;
14
+ confirmText?: string;
15
+ confirmWord?: string;
16
+ tint?: "red" | "yellow";
17
+ isLoading?: boolean;
18
+ }
19
+
20
+ export function ConfirmationModal({
21
+ isOpen,
22
+ onClose,
23
+ onConfirm,
24
+ title,
25
+ description,
26
+ confirmText = "Confirm",
27
+ confirmWord,
28
+ tint = "red",
29
+ isLoading = false,
30
+ }: ConfirmationModalProps) {
31
+ const [inputValue, setInputValue] = useState("");
32
+
33
+ if (!isOpen) return null;
34
+
35
+ const canConfirm = confirmWord ? inputValue === confirmWord : true;
36
+
37
+ const handleConfirm = () => {
38
+ if (canConfirm) {
39
+ onConfirm();
40
+ setInputValue("");
41
+ }
42
+ };
43
+
44
+ const handleClose = () => {
45
+ setInputValue("");
46
+ onClose();
47
+ };
48
+
49
+ return (
50
+ <div className="fixed inset-0 z-[60] flex items-center justify-center">
51
+ {/* Backdrop */}
52
+ <div
53
+ className="absolute inset-0 bg-black/60 backdrop-blur-sm"
54
+ onClick={handleClose}
55
+ />
56
+
57
+ {/* Modal */}
58
+ <div className="relative bg-[#0B1116]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl max-w-md w-full mx-4 overflow-hidden">
59
+ {/* Header */}
60
+ <div className="flex items-start gap-4 p-5">
61
+ <div
62
+ className={`p-2 rounded-lg ${tint === "red" ? "bg-red-500/10" : "bg-yellow-500/10"}`}
63
+ >
64
+ <AlertTriangle
65
+ className={`w-5 h-5 ${tint === "red" ? "text-red-400" : "text-yellow-400"}`}
66
+ />
67
+ </div>
68
+ <div className="flex-1">
69
+ <h3 className="text-lg font-semibold text-white">{title}</h3>
70
+ <p className="text-sm text-neutral-400 mt-1">{description}</p>
71
+ </div>
72
+ <button
73
+ onClick={handleClose}
74
+ className="text-neutral-400 hover:text-white transition-colors"
75
+ >
76
+ <X className="w-5 h-5" />
77
+ </button>
78
+ </div>
79
+
80
+ {/* Confirmation Input */}
81
+ {confirmWord && (
82
+ <div className="px-5 pb-4">
83
+ <Input
84
+ value={inputValue}
85
+ onChange={(e) => setInputValue(e.target.value)}
86
+ placeholder={`Type "${confirmWord}" to confirm`}
87
+ className="bg-black/40 border-white/10 text-white text-center font-mono placeholder:text-neutral-600"
88
+ autoFocus
89
+ />
90
+ </div>
91
+ )}
92
+
93
+ {/* Actions */}
94
+ <div className="flex gap-3 p-5 pt-0">
95
+ <TintButton
96
+ tint="neutral"
97
+ variant="outline"
98
+ className="flex-1"
99
+ onClick={handleClose}
100
+ >
101
+ Cancel
102
+ </TintButton>
103
+ <TintButton
104
+ tint={tint}
105
+ variant="solid"
106
+ className="flex-1"
107
+ onClick={handleConfirm}
108
+ disabled={!canConfirm || isLoading}
109
+ >
110
+ {isLoading ? "Processing..." : confirmText}
111
+ </TintButton>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { forwardRef } from "react";
5
+
6
+ type TintColor = "emerald" | "yellow" | "red" | "sky" | "neutral";
7
+
8
+ interface TintButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9
+ tint?: TintColor;
10
+ variant?: "solid" | "outline" | "ghost";
11
+ size?: "sm" | "md" | "lg" | "icon";
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ const tintStyles: Record<TintColor, Record<string, string>> = {
16
+ emerald: {
17
+ solid:
18
+ "bg-emerald-500/20 hover:bg-emerald-500/30 border-emerald-500/20 text-emerald-400",
19
+ outline:
20
+ "bg-transparent hover:bg-emerald-500/10 border-emerald-500/20 text-emerald-400",
21
+ ghost:
22
+ "bg-transparent hover:bg-emerald-500/10 border-transparent text-emerald-400",
23
+ },
24
+ yellow: {
25
+ solid:
26
+ "bg-yellow-500/20 hover:bg-yellow-500/30 border-yellow-500/20 text-yellow-400",
27
+ outline:
28
+ "bg-transparent hover:bg-yellow-500/10 border-yellow-500/20 text-yellow-400",
29
+ ghost:
30
+ "bg-transparent hover:bg-yellow-500/10 border-transparent text-yellow-400",
31
+ },
32
+ red: {
33
+ solid: "bg-red-500/20 hover:bg-red-500/30 border-red-500/20 text-red-400",
34
+ outline:
35
+ "bg-transparent hover:bg-red-500/10 border-red-500/20 text-red-400",
36
+ ghost: "bg-transparent hover:bg-red-500/10 border-transparent text-red-400",
37
+ },
38
+ sky: {
39
+ solid: "bg-sky-500/20 hover:bg-sky-500/30 border-sky-500/20 text-sky-400",
40
+ outline:
41
+ "bg-transparent hover:bg-sky-500/10 border-sky-500/20 text-sky-400",
42
+ ghost: "bg-transparent hover:bg-sky-500/10 border-transparent text-sky-400",
43
+ },
44
+ neutral: {
45
+ solid: "bg-white/10 hover:bg-white/20 border-white/10 text-white",
46
+ outline: "bg-transparent hover:bg-white/5 border-white/10 text-neutral-300",
47
+ ghost:
48
+ "bg-transparent hover:bg-white/5 border-transparent text-neutral-400 hover:text-white",
49
+ },
50
+ };
51
+
52
+ const sizeStyles = {
53
+ sm: "h-8 px-3 text-xs",
54
+ md: "h-10 px-4 text-sm",
55
+ lg: "h-11 px-6 text-base",
56
+ icon: "h-9 w-9 p-0",
57
+ };
58
+
59
+ export const TintButton = forwardRef<HTMLButtonElement, TintButtonProps>(
60
+ (
61
+ {
62
+ tint = "emerald",
63
+ variant = "solid",
64
+ size = "md",
65
+ className,
66
+ children,
67
+ ...props
68
+ },
69
+ ref,
70
+ ) => {
71
+ return (
72
+ <button
73
+ ref={ref}
74
+ className={cn(
75
+ "inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-all",
76
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0B1116]",
77
+ "disabled:opacity-50 disabled:cursor-not-allowed",
78
+ "[&_svg]:shrink-0",
79
+ tintStyles[tint][variant],
80
+ sizeStyles[size],
81
+ className,
82
+ )}
83
+ {...props}
84
+ >
85
+ {children}
86
+ </button>
87
+ );
88
+ },
89
+ );
90
+
91
+ TintButton.displayName = "TintButton";
@@ -7,208 +7,278 @@
7
7
  * Run with: npm run test:e2e -- crud-happy-path.spec.ts
8
8
  */
9
9
 
10
- import { expect, test } from '@playwright/test';
11
- import { execSync } from 'child_process';
10
+ import { expect, test } from "@playwright/test";
11
+ import { execSync } from "child_process";
12
12
 
13
- const MCP_URL = 'http://127.0.0.1:8626/mcp';
13
+ const MCP_URL = "http://127.0.0.1:8626/mcp";
14
14
  const CLIENT_NAME = `e2e-crud-${Date.now()}`;
15
15
 
16
16
  // Helpers
17
17
  const RPC = (method: string, params: any = {}, id: number) => ({
18
- jsonrpc: "2.0",
19
- id,
20
- method,
21
- params
18
+ jsonrpc: "2.0",
19
+ id,
20
+ method,
21
+ params,
22
22
  });
23
23
 
24
24
  const resetDB = async () => {
25
+ try {
26
+ // Remove database files
25
27
  try {
26
- // Remove database files
27
- try {
28
- execSync("docker exec cybermem-openmemory sh -c 'rm -f /data/openmemory.sqlite*'", { stdio: 'ignore' });
29
- } catch (e) { /* ignore - container might not be running */ }
30
-
31
- // Fix permissions on data directory to prevent SQLITE_READONLY after restart
32
- try {
33
- execSync("docker run --rm -v cybermem-openmemory-data:/data alpine sh -c 'chown -R 1001:1001 /data && chmod 777 /data'", { stdio: 'ignore' });
34
- } catch (e) { /* ignore */ }
35
-
36
- // Restart container
37
- execSync('docker restart cybermem-openmemory', { stdio: 'ignore' });
38
-
39
- // Poll for health AND MCP routing (up to 60s)
40
- const start = Date.now();
41
- while (Date.now() - start < 60000) {
42
- try {
43
- const healthRes = await fetch('http://127.0.0.1:8626/health');
44
- if (healthRes.ok) {
45
- // Also verify MCP routing is working (405 is fine for GET, 404 means not ready)
46
- const mcpRes = await fetch('http://127.0.0.1:8626/mcp');
47
- if (mcpRes.status !== 404) {
48
- // Give additional time for MCP to stabilize
49
- await new Promise(r => setTimeout(r, 3000));
50
- return true;
51
- }
52
- }
53
- } catch (e) { /* retry */ }
54
- await new Promise(r => setTimeout(r, 1000));
55
- }
56
- console.log('⚠️ DB reset timeout, but proceeding');
57
- return true;
28
+ execSync(
29
+ "docker exec cybermem-mcp sh -c 'rm -f /data/openmemory.sqlite*'",
30
+ { stdio: "ignore" },
31
+ );
32
+ } catch (e) {
33
+ /* ignore - container might not be running */
34
+ }
35
+
36
+ // Fix permissions on data directory to prevent SQLITE_READONLY after restart
37
+ try {
38
+ execSync(
39
+ "docker run --rm -v cybermem-openmemory-data:/data alpine sh -c 'chown -R 1001:1001 /data && chmod 777 /data'",
40
+ { stdio: "ignore" },
41
+ );
58
42
  } catch (e) {
59
- console.error('DB Reset failed:', e);
60
- return false;
43
+ /* ignore */
61
44
  }
45
+
46
+ // Restart container
47
+ execSync("docker restart cybermem-mcp", { stdio: "ignore" });
48
+
49
+ // Poll for health (up to 60s)
50
+ const start = Date.now();
51
+ while (Date.now() - start < 60000) {
52
+ try {
53
+ const healthRes = await fetch("http://127.0.0.1:8626/health");
54
+ if (healthRes.ok) {
55
+ const body = await healthRes.json();
56
+ if (body.ok || body.ready) {
57
+ // Give additional time for MCP to stabilize
58
+ await new Promise((r) => setTimeout(r, 3000));
59
+ return true;
60
+ }
61
+ }
62
+ } catch (e) {
63
+ // ignore
64
+ }
65
+ await new Promise((r) => setTimeout(r, 1000));
66
+ }
67
+ console.log("⚠️ DB reset timeout, but proceeding");
68
+ return true;
69
+ } catch (e) {
70
+ console.error("DB Reset failed:", e);
71
+ return false;
72
+ }
62
73
  };
63
74
 
75
+ let sessionId: string | null = null;
76
+
64
77
  const mcpCall = async (method: string, params: any, id: number) => {
65
- const res = await fetch(MCP_URL, {
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- 'Accept': 'application/json, text/event-stream',
70
- 'X-Client-Name': CLIENT_NAME
71
- },
72
- body: JSON.stringify(RPC(method, params, id))
73
- });
74
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
75
- return res.json();
78
+ const headers: any = {
79
+ "Content-Type": "application/json",
80
+ Accept: "application/json, text/event-stream",
81
+ "X-Client-Name": CLIENT_NAME,
82
+ };
83
+
84
+ if (sessionId) {
85
+ headers["Mcp-Session-Id"] = sessionId;
86
+ }
87
+
88
+ const res = await fetch(MCP_URL, {
89
+ method: "POST",
90
+ headers,
91
+ body: JSON.stringify(RPC(method, params, id)),
92
+ });
93
+
94
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
95
+
96
+ // Capture session ID from headers or body if available
97
+ const hSessionId = res.headers.get("Mcp-Session-Id");
98
+ if (hSessionId) {
99
+ sessionId = hSessionId;
100
+ }
101
+
102
+ const text = await res.text();
103
+ // Handle SSE response if the transport forces it
104
+ if (text.includes("event: message") && text.includes("data: ")) {
105
+ const dataLine = text.split("\n").find((l) => l.startsWith("data: "));
106
+ if (dataLine) {
107
+ return JSON.parse(dataLine.replace("data: ", ""));
108
+ }
109
+ }
110
+ return JSON.parse(text);
76
111
  };
77
112
 
78
113
  // Skip DB reset if SKIP_DB_RESET=true (for faster runs when stack is clean)
79
- const SKIP_DB_RESET = process.env.SKIP_DB_RESET === 'true';
114
+ const SKIP_DB_RESET = process.env.SKIP_DB_RESET === "true";
80
115
 
81
116
  // Run tests in serial mode since they share state (memoryId)
82
- test.describe.configure({ mode: 'serial' });
83
-
84
- test.describe('CRUD Happy Path with X-Client-Name', () => {
85
- let memoryId: string;
117
+ test.describe.configure({ mode: "serial" });
86
118
 
87
- test.beforeAll(async () => {
88
- if (SKIP_DB_RESET) {
89
- console.log('⏭️ Skipping DB reset (SKIP_DB_RESET=true)');
90
- return;
91
- }
92
- console.log(`🧹 Resetting DB before test suite...`);
93
- await resetDB();
94
- });
119
+ test.describe("CRUD Happy Path with X-Client-Name", () => {
120
+ let memoryId: string;
95
121
 
96
- test.afterAll(async () => {
97
- if (SKIP_DB_RESET) {
98
- console.log('⏭️ Skipping DB reset (SKIP_DB_RESET=true)');
99
- return;
100
- }
101
- console.log(`🧹 Resetting DB after test suite...`);
102
- await resetDB();
103
- });
104
-
105
- test('1. Initialize MCP connection', async () => {
106
- const initRes: any = await mcpCall("initialize", {
107
- protocolVersion: "2024-11-05",
108
- capabilities: { roots: { listChanged: true } },
109
- clientInfo: { name: "e2e-crud-tester", version: "1.0.0" }
110
- }, 1);
111
-
112
- expect(initRes.result?.serverInfo?.name).toBe("openmemory-mcp");
113
- await mcpCall("notifications/initialized", {}, 2);
114
- });
115
-
116
- test('2. CREATE - Store memory', async () => {
117
- const storeRes: any = await mcpCall("tools/call", {
118
- name: "openmemory_store",
119
- arguments: {
120
- content: `CRUD Happy Path Test Memory ${CLIENT_NAME}`,
121
- tags: ["e2e", "crud-test"]
122
- }
123
- }, 3);
124
-
125
- expect(storeRes.error).toBeUndefined();
126
- const payload = JSON.parse(storeRes.result.content[1].text);
127
- memoryId = payload.id;
128
- expect(memoryId).toBeTruthy();
129
- console.log(` ✅ Created memory: ${memoryId}`);
130
- });
131
-
132
- test('3. READ - Get memory by ID', async () => {
133
- const getRes: any = await mcpCall("tools/call", {
134
- name: "openmemory_get",
135
- arguments: { id: memoryId }
136
- }, 4);
137
-
138
- expect(getRes.error).toBeUndefined();
139
- const payload = JSON.parse(getRes.result.content[0].text);
140
- expect(payload.content).toContain('CRUD Happy Path Test Memory');
122
+ test.beforeAll(async () => {
123
+ if (SKIP_DB_RESET) {
124
+ console.log("⏭️ Skipping DB reset (SKIP_DB_RESET=true)");
125
+ return;
126
+ }
127
+ console.log(`🧹 Resetting DB before test suite...`);
128
+ await resetDB();
129
+ });
130
+
131
+ test.afterAll(async () => {
132
+ if (SKIP_DB_RESET) {
133
+ console.log("⏭️ Skipping DB reset (SKIP_DB_RESET=true)");
134
+ return;
135
+ }
136
+ console.log(`🧹 Resetting DB after test suite...`);
137
+ await resetDB();
138
+ });
139
+
140
+ test("1. Initialize MCP connection", async () => {
141
+ const initRes: any = await mcpCall(
142
+ "initialize",
143
+ {
144
+ protocolVersion: "2024-11-05",
145
+ capabilities: { roots: { listChanged: true } },
146
+ clientInfo: { name: "e2e-crud-tester", version: "1.0.0" },
147
+ },
148
+ 1,
149
+ );
150
+
151
+ expect(initRes.result?.serverInfo?.name).toBe("cybermem");
152
+
153
+ // Notifications MUST NOT have an id
154
+ await fetch(MCP_URL, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ "X-Client-Name": CLIENT_NAME,
159
+ },
160
+ body: JSON.stringify({
161
+ jsonrpc: "2.0",
162
+ method: "notifications/initialized",
163
+ params: {},
164
+ }),
141
165
  });
166
+ });
167
+
168
+ test("2. CREATE - Store memory", async () => {
169
+ const storeRes: any = await mcpCall(
170
+ "tools/call",
171
+ {
172
+ name: "add_memory",
173
+ arguments: {
174
+ content: `CRUD Happy Path Test Memory ${CLIENT_NAME}`,
175
+ tags: ["e2e", "crud-test"],
176
+ },
177
+ },
178
+ 3,
179
+ );
180
+
181
+ expect(storeRes.result?.content?.[0]?.text).toBeDefined();
182
+ const data = JSON.parse(storeRes.result.content[0].text);
183
+ memoryId = data.id;
184
+ expect(memoryId).toBeDefined();
185
+ console.log(`✅ Created memory with ID: ${memoryId}`);
186
+ });
187
+
188
+ test("3. READ - List memories", async () => {
189
+ const listRes: any = await mcpCall(
190
+ "tools/call",
191
+ {
192
+ name: "list_memories",
193
+ arguments: { limit: 10 },
194
+ },
195
+ 4,
196
+ );
197
+
198
+ const memories = JSON.parse(listRes.result.content[0].text);
199
+ const found = memories.some((m: any) => m.id === memoryId);
200
+ expect(found).toBe(true);
201
+ console.log(`✅ Verified memory exists in list`);
202
+ });
203
+
204
+ test("4. QUERY - Semantic search", async () => {
205
+ const queryRes: any = await mcpCall(
206
+ "tools/call",
207
+ {
208
+ name: "query_memory",
209
+ arguments: {
210
+ query: `Happy Path Test ${CLIENT_NAME}`,
211
+ k: 1,
212
+ },
213
+ },
214
+ 5,
215
+ );
216
+
217
+ const results = JSON.parse(queryRes.result.content[0].text);
218
+ expect(results.length).toBeGreaterThan(0);
219
+ expect(results[0].id).toBe(memoryId);
220
+ console.log(`✅ Verified semantic search returns the memory`);
221
+ });
222
+
223
+ test("5. DELETE - Remove memory", async () => {
224
+ const delRes: any = await mcpCall(
225
+ "tools/call",
226
+ {
227
+ name: "delete_memory",
228
+ arguments: { id: memoryId },
229
+ },
230
+ 6,
231
+ );
232
+
233
+ expect(delRes.result?.content?.[0]?.text).toContain("deleted");
234
+ console.log(`✅ Deleted memory`);
235
+ });
236
+
237
+ test("7. Verify client appears in Dashboard", async ({ page }) => {
238
+ // Navigate to dashboard
239
+ await page.goto("http://localhost:3000");
240
+
241
+ // Login if needed
242
+ const passwordInput = page.getByPlaceholder("Enter admin password");
243
+ if (await passwordInput.isVisible()) {
244
+ await passwordInput.fill("admin");
245
+ await page.keyboard.press("Enter");
246
+ }
142
247
 
143
- test('4. READ - List memories', async () => {
144
- const listRes: any = await mcpCall("tools/call", {
145
- name: "openmemory_list",
146
- arguments: { limit: 10 }
147
- }, 5);
248
+ // Handle password alert modal if appears
249
+ const dontShowAgainButton = page.locator(
250
+ 'button:has-text("Don\'t show again")',
251
+ );
252
+ if (
253
+ await dontShowAgainButton.isVisible({ timeout: 2000 }).catch(() => false)
254
+ ) {
255
+ await dontShowAgainButton.click();
256
+ }
148
257
 
149
- expect(listRes.error).toBeUndefined();
150
- const payload = JSON.parse(listRes.result.content[1].text);
151
- const found = payload.items.some((m: any) => m.id === memoryId);
152
- expect(found).toBe(true);
258
+ // Wait for dashboard to load
259
+ await expect(page.getByRole("heading", { name: "CyberMem" })).toBeVisible({
260
+ timeout: 15000,
153
261
  });
154
262
 
155
- test('5. READ - Query memories (semantic search)', async () => {
156
- const queryRes: any = await mcpCall("tools/call", {
157
- name: "openmemory_query",
158
- arguments: { query: "CRUD Happy Path" }
159
- }, 6);
263
+ // Wait for metrics to propagate (immediate for SQLite)
264
+ await page.waitForTimeout(2000);
160
265
 
161
- expect(queryRes.error).toBeUndefined();
162
- });
266
+ // Scroll to find Audit Log section
267
+ const auditHeader = page.locator('h3:has-text("Audit Log")');
268
+ await auditHeader.scrollIntoViewIfNeeded();
163
269
 
164
- test('6. DELETE - Remove memory', async () => {
165
- const deleteRes: any = await mcpCall("tools/call", {
166
- name: "openmemory_delete",
167
- arguments: { id: memoryId }
168
- }, 7);
169
-
170
- // DELETE may not exist in all OpenMemory versions - skip if not available
171
- if (deleteRes.error?.message?.includes('not found')) {
172
- test.skip();
173
- }
270
+ // Verify audit log table is visible
271
+ await expect(page.locator('th:has-text("Client")')).toBeVisible({
272
+ timeout: 10000,
174
273
  });
175
274
 
176
- test('7. Verify client appears in Dashboard', async ({ page }) => {
177
- // Navigate to dashboard
178
- await page.goto('http://localhost:3000');
275
+ // Look for our client in the audit log (partial match on e2e-crud)
276
+ const pageContent = await page.content();
277
+ const clientVisible =
278
+ pageContent.includes("e2e-crud") || pageContent.includes("E2E CRUD");
179
279
 
180
- // Login if needed
181
- const passwordInput = page.getByPlaceholder('Enter admin password');
182
- if (await passwordInput.isVisible()) {
183
- await passwordInput.fill('admin');
184
- await page.keyboard.press('Enter');
185
- }
186
-
187
- // Handle password alert modal if appears
188
- const dontShowAgainButton = page.locator('button:has-text("Don\'t show again")');
189
- if (await dontShowAgainButton.isVisible({ timeout: 2000 }).catch(() => false)) {
190
- await dontShowAgainButton.click();
191
- }
192
-
193
- // Wait for dashboard to load
194
- await expect(page.getByRole('heading', { name: 'CyberMem' })).toBeVisible({ timeout: 15000 });
195
-
196
- // Wait for metrics to propagate (Prometheus scrape interval)
197
- await page.waitForTimeout(5000);
198
-
199
- // Scroll to find Audit Log section
200
- const auditHeader = page.locator('h3:has-text("Audit Log")');
201
- await auditHeader.scrollIntoViewIfNeeded();
202
-
203
- // Verify audit log table is visible
204
- await expect(page.locator('th:has-text("Client")')).toBeVisible({ timeout: 10000 });
205
-
206
- // Look for our client in the audit log (partial match on e2e-crud)
207
- // The client name should appear in audit log entries
208
- const pageContent = await page.content();
209
- const clientVisible = pageContent.includes('e2e-crud') || pageContent.includes('E2E CRUD');
210
-
211
- console.log(` Client ${CLIENT_NAME} visible in dashboard: ${clientVisible}`);
212
- // Note: Client might show up as display name if mapped in clients.json
213
- });
280
+ console.log(
281
+ ` Client ${CLIENT_NAME} visible in dashboard: ${clientVisible}`,
282
+ );
283
+ });
214
284
  });