@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
|
@@ -2,33 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { ConfirmationModal } from "@/components/ui/confirmation-modal";
|
|
5
|
-
import { Input } from "@/components/ui/input";
|
|
6
|
-
import { Label } from "@/components/ui/label";
|
|
7
|
-
import { TintButton } from "@/components/ui/tint-button";
|
|
8
5
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
9
|
-
import {
|
|
10
|
-
Check,
|
|
11
|
-
Copy,
|
|
12
|
-
Database,
|
|
13
|
-
Download,
|
|
14
|
-
Eye,
|
|
15
|
-
EyeOff,
|
|
16
|
-
Loader2,
|
|
17
|
-
RotateCcw,
|
|
18
|
-
Server,
|
|
19
|
-
Settings,
|
|
20
|
-
Shield,
|
|
21
|
-
Trash2,
|
|
22
|
-
Upload,
|
|
23
|
-
X,
|
|
24
|
-
} from "lucide-react";
|
|
6
|
+
import { Settings, X } from "lucide-react";
|
|
25
7
|
import { useEffect, useState } from "react";
|
|
26
8
|
import { toast } from "sonner";
|
|
9
|
+
import AccessTokenSection from "./settings/access-token-section";
|
|
10
|
+
import DataManagementSection from "./settings/data-management-section";
|
|
11
|
+
import SystemInfoSection from "./settings/system-info-section";
|
|
27
12
|
|
|
28
13
|
export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
29
14
|
const [apiKey, setApiKey] = useState("");
|
|
30
15
|
const [endpoint, setEndpoint] = useState("");
|
|
31
16
|
const [isManaged, setIsManaged] = useState(false);
|
|
17
|
+
const [instanceType, setInstanceType] = useState<"local" | "rpi" | "vps">(
|
|
18
|
+
"local",
|
|
19
|
+
);
|
|
32
20
|
const [settings, setSettings] = useState<any>(null);
|
|
33
21
|
const [isLoading, setIsLoading] = useState(true);
|
|
34
22
|
|
|
@@ -64,8 +52,9 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
64
52
|
fetch("/api/settings")
|
|
65
53
|
.then((res) => res.json())
|
|
66
54
|
.then((data) => {
|
|
67
|
-
// Enforce Local Mode if
|
|
68
|
-
setIsManaged(data.isManaged
|
|
55
|
+
// Enforce Local Mode ONLY if it's truly local hardware AND accessed via localhost
|
|
56
|
+
setIsManaged(data.isManaged && data.instanceType === "local");
|
|
57
|
+
setInstanceType(data.instanceType || "local");
|
|
69
58
|
setSettings(data);
|
|
70
59
|
|
|
71
60
|
if (localKey && !data.isManaged) {
|
|
@@ -113,14 +102,6 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
113
102
|
}
|
|
114
103
|
};
|
|
115
104
|
|
|
116
|
-
const [saved, setSaved] = useState(false);
|
|
117
|
-
|
|
118
|
-
const handleSave = () => {
|
|
119
|
-
// Settings saved
|
|
120
|
-
setSaved(true);
|
|
121
|
-
setTimeout(() => setSaved(false), 2000);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
105
|
const handleBackup = async () => {
|
|
125
106
|
try {
|
|
126
107
|
setIsBackingUp(true);
|
|
@@ -219,291 +200,66 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
219
200
|
|
|
220
201
|
return (
|
|
221
202
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
222
|
-
<div
|
|
223
|
-
className="bg-[#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<div className="flex items-center justify-between px-6 pt-6 pb-2">
|
|
230
|
-
<div className="flex items-center gap-3">
|
|
231
|
-
<div className="p-2 bg-white/5 rounded-lg border border-white/10 shadow-inner">
|
|
232
|
-
<Settings className="w-5 h-5 text-white shadow-lg" />
|
|
203
|
+
<div className="bg-[#05100F] border-[0.5px] border-white/10 rounded-3xl shadow-[0_20px_50px_rgba(0,0,0,0.5)] max-w-2xl w-full max-h-[90vh] overflow-hidden relative flex flex-col">
|
|
204
|
+
<div className="relative rounded-t-lg bg-[#05100F] px-4 py-4 flex items-center justify-between border-b-0">
|
|
205
|
+
<div className="flex items-center gap-4">
|
|
206
|
+
{/* Title */}
|
|
207
|
+
<div className="text-sm text-white font-semibold flex items-center gap-2 pl-2">
|
|
208
|
+
<Settings className="w-4 h-4 opacity-70" />
|
|
209
|
+
<span>Settings</span>
|
|
233
210
|
</div>
|
|
234
|
-
<h2 className="text-xl font-semibold text-white">Settings</h2>
|
|
235
211
|
</div>
|
|
212
|
+
|
|
236
213
|
<Button
|
|
237
214
|
variant="ghost"
|
|
238
215
|
size="icon"
|
|
239
216
|
onClick={onClose}
|
|
240
|
-
className="text-neutral-400 hover:text-white rounded-full"
|
|
217
|
+
className="text-neutral-400 hover:text-white hover:bg-white/10 rounded-full w-8 h-8"
|
|
241
218
|
>
|
|
242
|
-
<X className="w-
|
|
219
|
+
<X className="w-4 h-4" />
|
|
243
220
|
</Button>
|
|
244
221
|
</div>
|
|
245
222
|
|
|
246
223
|
{/* Content */}
|
|
247
|
-
<div className="p-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)}
|
|
276
|
-
</button>
|
|
277
|
-
</div>
|
|
278
|
-
<TintButton
|
|
279
|
-
tint="neutral"
|
|
280
|
-
variant="ghost"
|
|
281
|
-
size="icon"
|
|
282
|
-
onClick={() => copyToClipboard(apiKey, "accesstoken")}
|
|
283
|
-
title="Copy token"
|
|
284
|
-
>
|
|
285
|
-
{copiedId === "accesstoken" ? (
|
|
286
|
-
<Check className="h-4 w-4 text-emerald-400" />
|
|
287
|
-
) : (
|
|
288
|
-
<Copy className="h-4 w-4" />
|
|
289
|
-
)}
|
|
290
|
-
</TintButton>
|
|
291
|
-
<TintButton
|
|
292
|
-
tint="yellow"
|
|
293
|
-
variant="ghost"
|
|
294
|
-
size="icon"
|
|
295
|
-
onClick={() => setShowRegenConfirm(true)}
|
|
296
|
-
title="Regenerate token"
|
|
297
|
-
>
|
|
298
|
-
<RotateCcw className="w-4 h-4" />
|
|
299
|
-
</TintButton>
|
|
300
|
-
</div>
|
|
301
|
-
<p className="text-xs text-neutral-500">
|
|
302
|
-
Use this token to connect MCP clients from other devices
|
|
303
|
-
</p>
|
|
304
|
-
</div>
|
|
305
|
-
|
|
306
|
-
{/* Auth Status */}
|
|
307
|
-
<div className="pt-4 border-t border-white/10">
|
|
308
|
-
<div className="flex items-center gap-3 p-3 bg-black/20 rounded-lg border border-white/5">
|
|
309
|
-
{isManaged ? (
|
|
310
|
-
<>
|
|
311
|
-
<div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse" />
|
|
312
|
-
<div className="flex-1">
|
|
313
|
-
<p className="text-sm text-emerald-300 font-medium">
|
|
314
|
-
Local Mode Active
|
|
315
|
-
</p>
|
|
316
|
-
<p className="text-xs text-neutral-500">
|
|
317
|
-
No token needed for local connections
|
|
318
|
-
</p>
|
|
319
|
-
</div>
|
|
320
|
-
</>
|
|
321
|
-
) : (
|
|
322
|
-
<>
|
|
323
|
-
<div className="w-2 h-2 bg-yellow-400 rounded-full" />
|
|
324
|
-
<div className="flex-1">
|
|
325
|
-
<p className="text-sm text-yellow-300 font-medium">
|
|
326
|
-
Remote Mode
|
|
327
|
-
</p>
|
|
328
|
-
<p className="text-xs text-neutral-500">
|
|
329
|
-
Token required for MCP client connections
|
|
330
|
-
</p>
|
|
331
|
-
</div>
|
|
332
|
-
</>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
</div>
|
|
337
|
-
</section>
|
|
338
|
-
|
|
339
|
-
{/* Data Management */}
|
|
340
|
-
<section>
|
|
341
|
-
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
342
|
-
<Database className="w-5 h-5" />
|
|
343
|
-
Data Management
|
|
344
|
-
</h3>
|
|
345
|
-
<div className="flex flex-col gap-3">
|
|
346
|
-
<div className="flex items-center gap-3">
|
|
347
|
-
<TintButton
|
|
348
|
-
tint="neutral"
|
|
349
|
-
variant="solid"
|
|
350
|
-
className="flex-1 h-11"
|
|
351
|
-
onClick={handleBackup}
|
|
352
|
-
disabled={isBackingUp}
|
|
353
|
-
>
|
|
354
|
-
{isBackingUp ? (
|
|
355
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
356
|
-
) : (
|
|
357
|
-
<Download className="w-4 h-4" />
|
|
358
|
-
)}
|
|
359
|
-
Backup
|
|
360
|
-
</TintButton>
|
|
361
|
-
|
|
362
|
-
<div className="flex-1 relative">
|
|
363
|
-
<input
|
|
364
|
-
type="file"
|
|
365
|
-
id="restore-file"
|
|
366
|
-
className="hidden"
|
|
367
|
-
accept=".tar.gz,.tgz"
|
|
368
|
-
onChange={handleRestore}
|
|
369
|
-
disabled={isRestoring}
|
|
370
|
-
/>
|
|
371
|
-
<TintButton
|
|
372
|
-
tint="neutral"
|
|
373
|
-
variant="solid"
|
|
374
|
-
className="w-full h-11"
|
|
375
|
-
onClick={() =>
|
|
376
|
-
document.getElementById("restore-file")?.click()
|
|
377
|
-
}
|
|
378
|
-
disabled={isRestoring}
|
|
379
|
-
>
|
|
380
|
-
{isRestoring ? (
|
|
381
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
382
|
-
) : (
|
|
383
|
-
<Upload className="w-4 h-4" />
|
|
384
|
-
)}
|
|
385
|
-
Restore
|
|
386
|
-
</TintButton>
|
|
387
|
-
</div>
|
|
388
|
-
|
|
389
|
-
<TintButton
|
|
390
|
-
tint="red"
|
|
391
|
-
variant="solid"
|
|
392
|
-
className="flex-1 h-11"
|
|
393
|
-
onClick={() => setShowResetConfirm(true)}
|
|
394
|
-
disabled={isResetting}
|
|
395
|
-
>
|
|
396
|
-
<Trash2 className="w-4 h-4" />
|
|
397
|
-
Reset DB
|
|
398
|
-
</TintButton>
|
|
399
|
-
</div>
|
|
400
|
-
|
|
401
|
-
{operationStatus && (
|
|
402
|
-
<div
|
|
403
|
-
className={`p-3 rounded-xl text-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-1 ${
|
|
404
|
-
operationStatus.type === "success"
|
|
405
|
-
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
|
406
|
-
: "bg-red-500/10 text-red-400 border border-red-500/20"
|
|
407
|
-
}`}
|
|
408
|
-
>
|
|
409
|
-
{operationStatus.type === "success" ? (
|
|
410
|
-
<Check className="w-4 h-4" />
|
|
411
|
-
) : (
|
|
412
|
-
<X className="w-4 h-4" />
|
|
413
|
-
)}
|
|
414
|
-
<span className="flex-1">{operationStatus.message}</span>
|
|
415
|
-
<button
|
|
416
|
-
onClick={() => setOperationStatus(null)}
|
|
417
|
-
className="opacity-50 hover:opacity-100 p-1"
|
|
418
|
-
>
|
|
419
|
-
<X className="w-4 h-4" />
|
|
420
|
-
</button>
|
|
421
|
-
</div>
|
|
422
|
-
)}
|
|
423
|
-
</div>
|
|
424
|
-
</section>
|
|
425
|
-
|
|
426
|
-
{/* System Info */}
|
|
427
|
-
<section>
|
|
428
|
-
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
429
|
-
<Server className="w-5 h-5" />
|
|
430
|
-
System
|
|
431
|
-
</h3>
|
|
432
|
-
<div className="bg-white/5 border border-white/10 rounded-xl p-6 shadow-[inset_0_0_30px_rgba(255,255,255,0.01)] backdrop-blur-md space-y-6">
|
|
433
|
-
<div className="grid grid-cols-2 gap-12">
|
|
434
|
-
<div className="space-y-4">
|
|
435
|
-
<span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
|
|
436
|
-
Versions
|
|
437
|
-
</span>
|
|
438
|
-
<div className="space-y-3">
|
|
439
|
-
<div className="flex justify-between items-center group/version">
|
|
440
|
-
<span className="text-xs text-neutral-400 group-hover/version:text-neutral-300 transition-colors">
|
|
441
|
-
Dashboard
|
|
442
|
-
</span>
|
|
443
|
-
<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">
|
|
444
|
-
{settings?.dashboardVersion || "v0.7.5"}
|
|
445
|
-
</code>
|
|
446
|
-
</div>
|
|
447
|
-
<div className="flex justify-between items-center group/version">
|
|
448
|
-
<span className="text-xs text-neutral-400 group-hover/version:text-neutral-300 transition-colors">
|
|
449
|
-
MCP Server
|
|
450
|
-
</span>
|
|
451
|
-
<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">
|
|
452
|
-
{settings?.mcpVersion || "v0.7.5"}
|
|
453
|
-
</code>
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
</div>
|
|
457
|
-
<div className="border-l border-white/5 pl-8">
|
|
458
|
-
<span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
|
|
459
|
-
Environment
|
|
460
|
-
</span>
|
|
461
|
-
<div className="space-y-3">
|
|
462
|
-
<div className="flex justify-between items-center">
|
|
463
|
-
<span className="text-xs text-neutral-400">Status</span>
|
|
464
|
-
<code className="text-[13px] font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
|
465
|
-
Production
|
|
466
|
-
</code>
|
|
467
|
-
</div>
|
|
468
|
-
<div className="flex justify-between items-center">
|
|
469
|
-
<span className="text-xs text-neutral-400">Instance</span>
|
|
470
|
-
<code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10">
|
|
471
|
-
{settings?.isLocal
|
|
472
|
-
? "Local"
|
|
473
|
-
: settings?.isManaged
|
|
474
|
-
? "RPi"
|
|
475
|
-
: "VPS"}
|
|
476
|
-
</code>
|
|
477
|
-
</div>
|
|
478
|
-
</div>
|
|
479
|
-
</div>
|
|
480
|
-
</div>
|
|
481
|
-
|
|
482
|
-
<div className="pt-2 border-t border-white/5">
|
|
483
|
-
<TintButton
|
|
484
|
-
tint="sky"
|
|
485
|
-
variant="solid"
|
|
486
|
-
className="w-full h-10"
|
|
487
|
-
onClick={handleRestart}
|
|
488
|
-
disabled={isRestarting}
|
|
489
|
-
>
|
|
490
|
-
{isRestarting ? (
|
|
491
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
492
|
-
) : (
|
|
493
|
-
<RotateCcw className="w-4 h-4" />
|
|
494
|
-
)}
|
|
495
|
-
{isRestarting ? "Restarting..." : "Restart Service"}
|
|
496
|
-
</TintButton>
|
|
497
|
-
</div>
|
|
498
|
-
</div>
|
|
499
|
-
</section>
|
|
224
|
+
<div className="flex-1 overflow-y-auto p-8 space-y-8 bg-[#05100F]">
|
|
225
|
+
<AccessTokenSection
|
|
226
|
+
apiKey={apiKey}
|
|
227
|
+
showApiKey={showApiKey}
|
|
228
|
+
setShowApiKey={setShowApiKey}
|
|
229
|
+
copiedId={copiedId}
|
|
230
|
+
copyToClipboard={copyToClipboard}
|
|
231
|
+
setShowRegenConfirm={setShowRegenConfirm}
|
|
232
|
+
isManaged={isManaged}
|
|
233
|
+
instanceType={instanceType}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<DataManagementSection
|
|
237
|
+
handleBackup={handleBackup}
|
|
238
|
+
isBackingUp={isBackingUp}
|
|
239
|
+
handleRestore={handleRestore}
|
|
240
|
+
isRestoring={isRestoring}
|
|
241
|
+
setShowResetConfirm={setShowResetConfirm}
|
|
242
|
+
isResetting={isResetting}
|
|
243
|
+
operationStatus={operationStatus}
|
|
244
|
+
setOperationStatus={setOperationStatus}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
<SystemInfoSection
|
|
248
|
+
settings={settings}
|
|
249
|
+
handleRestart={handleRestart}
|
|
250
|
+
isRestarting={isRestarting}
|
|
251
|
+
/>
|
|
500
252
|
</div>
|
|
501
253
|
|
|
502
254
|
{/* Footer */}
|
|
503
|
-
<div className="
|
|
504
|
-
<
|
|
255
|
+
<div className="bg-[#05100F] border-t border-white/[0.03] px-8 py-5 flex justify-end gap-3 z-10">
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
onClick={onClose}
|
|
259
|
+
className="hover:bg-white/5 text-neutral-300 hover:text-white"
|
|
260
|
+
>
|
|
505
261
|
Close
|
|
506
|
-
</
|
|
262
|
+
</Button>
|
|
507
263
|
</div>
|
|
508
264
|
</div>
|
|
509
265
|
|
package/e2e/api.spec.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
// This suite runs against the Next.js API Routes (Backend)
|
|
6
|
+
// IMPORTANT: This uses REAL database with clean state!
|
|
7
|
+
const DASHBOARD_URL = process.env.DASHBOARD_URL || "http://localhost:3000";
|
|
8
|
+
// Normalize MCP URL: strip /mcp suffix to get the base for legacy HTTP endpoints (/add, /query)
|
|
9
|
+
const RAW_MCP_URL = process.env.MCP_URL || "http://localhost:8626";
|
|
10
|
+
const MCP_API_URL = RAW_MCP_URL.replace(/\/mcp\/?$/, "");
|
|
11
|
+
|
|
12
|
+
// Tailscale environments require auth token
|
|
13
|
+
const isLocalhost =
|
|
14
|
+
DASHBOARD_URL.includes("localhost") || DASHBOARD_URL.includes("127.0.0.1");
|
|
15
|
+
const CYBERMEM_TOKEN = process.env.CYBERMEM_TOKEN || "";
|
|
16
|
+
|
|
17
|
+
// Helper to build headers with optional auth
|
|
18
|
+
function getHeaders(clientName: string): Record<string, string> {
|
|
19
|
+
const headers: Record<string, string> = { "X-Client-Name": clientName };
|
|
20
|
+
if (!isLocalhost && CYBERMEM_TOKEN) {
|
|
21
|
+
headers["X-API-Key"] = CYBERMEM_TOKEN;
|
|
22
|
+
}
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SQLITE_PATH = path.join(
|
|
27
|
+
process.env.HOME || "",
|
|
28
|
+
".cybermem",
|
|
29
|
+
"data",
|
|
30
|
+
"openmemory.sqlite",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Helper to run CLI commands
|
|
34
|
+
function runCLI(cmd: string): { stdout: string; success: boolean } {
|
|
35
|
+
try {
|
|
36
|
+
const stdout = execSync(cmd, {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 120000, // 2 min timeout
|
|
39
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
40
|
+
});
|
|
41
|
+
return { stdout, success: true };
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
return { stdout: error.stdout || error.message, success: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
48
|
+
const TEST_CLIENT = `e2e-api-journey-${Date.now()}`;
|
|
49
|
+
|
|
50
|
+
// Note: Reset is handled by global-setup.ts (runs ONCE before all tests)
|
|
51
|
+
test.beforeAll(async ({}, testInfo) => {
|
|
52
|
+
// Attach environment info
|
|
53
|
+
await testInfo.attach("🔧 Test Environment", {
|
|
54
|
+
body: `Dashboard URL: ${DASHBOARD_URL}\nMCP API URL: ${MCP_API_URL}\nSQLite Path: ${SQLITE_PATH}\nTest Client: ${TEST_CLIENT}\n\n✅ Clean state provided by global-setup.ts`,
|
|
55
|
+
contentType: "text/plain",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("1. Health Check", async ({ request }, testInfo) => {
|
|
60
|
+
await test.step("📊 GET /api/health — Verify System Status", async () => {
|
|
61
|
+
console.log("📊 GET /api/health");
|
|
62
|
+
|
|
63
|
+
const response = await request.get(`${DASHBOARD_URL}/api/health`, {
|
|
64
|
+
headers: getHeaders("antigravity-client"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const body = await response.json();
|
|
68
|
+
console.log(" Response:", JSON.stringify(body, null, 2));
|
|
69
|
+
|
|
70
|
+
expect(response.status()).toBe(200);
|
|
71
|
+
expect(body.overall).toBe("ok");
|
|
72
|
+
expect(body.services.length).toBeGreaterThan(0);
|
|
73
|
+
|
|
74
|
+
await testInfo.attach("📊 Health Check Result", {
|
|
75
|
+
body: `Status: ${response.status()}\nOverall: ${body.overall}\nServices: ${body.services.map((s: any) => `${s.name}: ${s.status}`).join(", ")}`,
|
|
76
|
+
contentType: "text/plain",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("2. Full Journey: MCP Write → Metrics → Audit Logs", async ({
|
|
82
|
+
request,
|
|
83
|
+
}, testInfo) => {
|
|
84
|
+
const uniqueContent = `Journey Test ${Date.now()}`;
|
|
85
|
+
|
|
86
|
+
console.log("🔄 FULL JOURNEY TEST START");
|
|
87
|
+
console.log(` Test Client: ${TEST_CLIENT}`);
|
|
88
|
+
console.log(` Content: ${uniqueContent}`);
|
|
89
|
+
|
|
90
|
+
// Step 1: Trigger MCP Write
|
|
91
|
+
await test.step("📤 CRUD — POST /add — Create new memory", async () => {
|
|
92
|
+
console.log("📤 POST /add (MCP API)");
|
|
93
|
+
console.log(
|
|
94
|
+
` Payload: { content: "${uniqueContent}", tags: ["journey"] }`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const resp = await request.post(`${MCP_API_URL}/add`, {
|
|
98
|
+
data: { content: uniqueContent, tags: ["journey"] },
|
|
99
|
+
headers: getHeaders(TEST_CLIENT),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(` Status: ${resp.status()}`);
|
|
103
|
+
const body = await resp.json();
|
|
104
|
+
console.log(` Response: ${JSON.stringify(body, null, 2)}`);
|
|
105
|
+
|
|
106
|
+
expect(resp.status()).toBe(200);
|
|
107
|
+
|
|
108
|
+
await testInfo.attach("📝 CRUD — CREATE", {
|
|
109
|
+
body: `Endpoint: POST ${MCP_API_URL}/add\n\nRequest:\n{\n "content": "${uniqueContent}",\n "tags": ["journey"]\n}\n\nResponse:\n${JSON.stringify(body, null, 2)}`,
|
|
110
|
+
contentType: "text/plain",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Step 2: Verify Metrics
|
|
115
|
+
await test.step("📊 Discovery — Metrics Reflect Write Activity", async () => {
|
|
116
|
+
console.log("📊 GET /api/metrics");
|
|
117
|
+
|
|
118
|
+
const resp = await request.get(`${DASHBOARD_URL}/api/metrics`, {
|
|
119
|
+
headers: getHeaders("antigravity-client"),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const data = await resp.json();
|
|
123
|
+
console.log(` Last Writer: ${data.stats.lastWriter.name}`);
|
|
124
|
+
console.log(` Total Requests: ${data.stats.totalRequests}`);
|
|
125
|
+
console.log(
|
|
126
|
+
` Creates Time Series Length: ${data.timeSeries.creates.length}`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(data.stats.lastWriter.name).toContain(TEST_CLIENT);
|
|
130
|
+
expect(data.stats.totalRequests).toBeGreaterThan(0);
|
|
131
|
+
|
|
132
|
+
await testInfo.attach("📊 Metrics Snapshot", {
|
|
133
|
+
body: `Last Writer: ${data.stats.lastWriter.name}\nTotal Requests: ${data.stats.totalRequests}\nCreates Time Series: ${data.timeSeries.creates.length} entries`,
|
|
134
|
+
contentType: "text/plain",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Step 3: Verify Audit Logs
|
|
139
|
+
await test.step("📋 Discovery — Audit Log Contains Journey Entry", async () => {
|
|
140
|
+
console.log("📋 GET /api/audit-logs");
|
|
141
|
+
|
|
142
|
+
const resp = await request.get(`${DASHBOARD_URL}/api/audit-logs`, {
|
|
143
|
+
headers: getHeaders("antigravity-client"),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const data = await resp.json();
|
|
147
|
+
const latestLog = data.logs.find((l: any) =>
|
|
148
|
+
l.client.includes(TEST_CLIENT),
|
|
149
|
+
);
|
|
150
|
+
console.log(` Found Log: ${latestLog ? "YES" : "NO"}`);
|
|
151
|
+
if (latestLog) {
|
|
152
|
+
console.log(` Log Details: ${JSON.stringify(latestLog, null, 2)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
expect(latestLog).toBeDefined();
|
|
156
|
+
expect(latestLog.operation).toBe("Write");
|
|
157
|
+
|
|
158
|
+
await testInfo.attach("📋 Audit Log Entry", {
|
|
159
|
+
body: `Found: ${latestLog ? "YES" : "NO"}\nClient: ${latestLog?.client}\nOperation: ${latestLog?.operation}\nStatus: ${latestLog?.status}`,
|
|
160
|
+
contentType: "text/plain",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log("✅ FULL JOURNEY TEST COMPLETE");
|
|
165
|
+
|
|
166
|
+
await testInfo.attach("✅ Journey Complete", {
|
|
167
|
+
body: `Test Client: ${TEST_CLIENT}\nContent: ${uniqueContent}\n\nVerified:\n✅ MCP Write (POST /add)\n✅ Metrics (GET /api/metrics) — Last Writer confirmed\n✅ Audit Logs (GET /api/audit-logs) — Entry found`,
|
|
168
|
+
contentType: "text/plain",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("3. Config & Settings", async ({ request }, testInfo) => {
|
|
173
|
+
// MCP Config
|
|
174
|
+
await test.step("⚙️ Config — GET /api/mcp-config", async () => {
|
|
175
|
+
console.log("⚙️ GET /api/mcp-config?type=json");
|
|
176
|
+
|
|
177
|
+
const configResp = await request.get(
|
|
178
|
+
`${DASHBOARD_URL}/api/mcp-config?type=json`,
|
|
179
|
+
{
|
|
180
|
+
headers: getHeaders("antigravity-client"),
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const config = await configResp.json();
|
|
185
|
+
console.log(` Config Type: ${config.configType}`);
|
|
186
|
+
|
|
187
|
+
expect(configResp.status()).toBe(200);
|
|
188
|
+
expect(config.configType).toBe("json");
|
|
189
|
+
expect(config.config.mcpServers).toBeDefined();
|
|
190
|
+
|
|
191
|
+
await testInfo.attach("⚙️ MCP Config", {
|
|
192
|
+
body: `Config Type: ${config.configType}\nmcpServers: ${Object.keys(config.config.mcpServers).join(", ")}`,
|
|
193
|
+
contentType: "text/plain",
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Settings
|
|
198
|
+
await test.step("⚙️ Settings — GET /api/settings", async () => {
|
|
199
|
+
console.log("⚙️ GET /api/settings");
|
|
200
|
+
|
|
201
|
+
const settingsResp = await request.get(`${DASHBOARD_URL}/api/settings`, {
|
|
202
|
+
headers: getHeaders("antigravity-client"),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const settings = await settingsResp.json();
|
|
206
|
+
console.log(` Instance Type: ${settings.instanceType}`);
|
|
207
|
+
console.log(` Endpoint: ${settings.endpoint}`);
|
|
208
|
+
|
|
209
|
+
expect(settingsResp.status()).toBe(200);
|
|
210
|
+
expect(settings).toHaveProperty("apiKey");
|
|
211
|
+
expect(settings).toHaveProperty("endpoint");
|
|
212
|
+
|
|
213
|
+
await testInfo.attach("⚙️ Settings", {
|
|
214
|
+
body: `Instance Type: ${settings.instanceType}\nEndpoint: ${settings.endpoint}\nAPI Key: ${settings.apiKey ? "***" + settings.apiKey.slice(-4) : "N/A"}`,
|
|
215
|
+
contentType: "text/plain",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// This test is ONLY for localhost environments where dashboard serves at root.
|
|
4
|
+
// Tailscale environments use subpaths (/cybermem-staging, /cybermem) by design.
|
|
5
|
+
const dashboardUrl = process.env.DASHBOARD_URL || "";
|
|
6
|
+
const isLocalhost =
|
|
7
|
+
dashboardUrl.includes("localhost") || dashboardUrl.includes("127.0.0.1");
|
|
8
|
+
|
|
9
|
+
test.describe("Routing & URL Canonicalization", () => {
|
|
10
|
+
test.skip(!isLocalhost, "Skipped on Tailscale - uses subpath by design");
|
|
11
|
+
|
|
12
|
+
test("Root Check: Should stay at / and NOT redirect to legacy subpaths", async ({
|
|
13
|
+
request,
|
|
14
|
+
baseURL,
|
|
15
|
+
}) => {
|
|
16
|
+
// We expect the DASHBOARD_URL (baseURL) to be the root or specific entry point.
|
|
17
|
+
// This test ensures that requesting the root does NOT redirect to /cybermem or /cybermem-staging
|
|
18
|
+
// which was a legacy behavior we removed in v0.13.0.
|
|
19
|
+
|
|
20
|
+
// Note: baseURL provided by Playwright config or env var
|
|
21
|
+
console.log(`Checking routing for: ${baseURL}`);
|
|
22
|
+
|
|
23
|
+
const response = await request.get("/");
|
|
24
|
+
|
|
25
|
+
// 1. Should be 200 OK
|
|
26
|
+
expect(response.status()).toBe(200);
|
|
27
|
+
|
|
28
|
+
// 2. Should match the expected URL (no redirects)
|
|
29
|
+
// response.url() returns the final URL after redirects.
|
|
30
|
+
// We check that it doesn't contain legacy subpaths if we started at root
|
|
31
|
+
const finalUrl = response.url();
|
|
32
|
+
expect(finalUrl).not.toContain("/cybermem-staging");
|
|
33
|
+
expect(finalUrl).not.toContain("/cybermem/"); // Trailing slash check
|
|
34
|
+
|
|
35
|
+
// 3. Content Check to ensure it's not a generic 404/nginx page
|
|
36
|
+
const body = await response.text();
|
|
37
|
+
expect(body).toContain("<!DOCTYPE html>"); // Expecting Next.js app
|
|
38
|
+
});
|
|
39
|
+
});
|