@cybermem/dashboard 0.1.0
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/.dockerignore +11 -0
- package/.eslintrc.json +3 -0
- package/Dockerfile +48 -0
- package/app/api/audit-logs/route.ts +60 -0
- package/app/api/metrics/route.ts +141 -0
- package/app/api/prometheus/route.ts +65 -0
- package/app/api/settings/regenerate/route.ts +20 -0
- package/app/api/settings/route.ts +25 -0
- package/app/api/system/restart/route.ts +18 -0
- package/app/globals.css +148 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +150 -0
- package/components/dashboard/audit-log-table.tsx +195 -0
- package/components/dashboard/chart-card.tsx +196 -0
- package/components/dashboard/charts-section.tsx +16 -0
- package/components/dashboard/header.tsx +82 -0
- package/components/dashboard/login-modal.tsx +87 -0
- package/components/dashboard/mcp-config-modal.tsx +397 -0
- package/components/dashboard/metric-card.tsx +23 -0
- package/components/dashboard/metrics-chart.tsx +134 -0
- package/components/dashboard/metrics-grid.tsx +136 -0
- package/components/dashboard/settings-modal.tsx +345 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/calendar.tsx +213 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +135 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +104 -0
- package/components/ui/field.tsx +244 -0
- package/components/ui/form.tsx +167 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +169 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/item.tsx +193 -0
- package/components/ui/kbd.tsx +28 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +166 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +56 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +185 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/sonner.tsx +25 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/toast.tsx +129 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.tsx +19 -0
- package/components/ui/use-toast.ts +191 -0
- package/components.json +21 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +191 -0
- package/lib/data/dashboard-context.tsx +75 -0
- package/lib/data/demo-strategy.ts +110 -0
- package/lib/data/production-strategy.ts +152 -0
- package/lib/data/types.ts +52 -0
- package/lib/prometheus/client.ts +58 -0
- package/lib/prometheus/index.ts +6 -0
- package/lib/prometheus/metrics.ts +234 -0
- package/lib/prometheus/sparklines.ts +71 -0
- package/lib/prometheus/timeseries.ts +305 -0
- package/lib/prometheus/utils.ts +176 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +36 -0
- package/package.json +91 -0
- package/postcss.config.mjs +8 -0
- package/public/clients.json +165 -0
- package/public/favicon-dark.svg +1 -0
- package/public/favicon-light.svg +1 -0
- package/public/icons/antigravity.png +0 -0
- package/public/icons/chatgpt.png +0 -0
- package/public/icons/claude-code.png +0 -0
- package/public/icons/claude.png +0 -0
- package/public/icons/codex.png +0 -0
- package/public/icons/cursor.png +0 -0
- package/public/icons/gemini.png +0 -0
- package/public/icons/images.jpeg +0 -0
- package/public/icons/mcp.png +0 -0
- package/public/icons/mono.png +0 -0
- package/public/icons/perplexity.png +0 -0
- package/public/icons/vscode.png +0 -0
- package/public/icons/warp.png +0 -0
- package/public/icons/windsurf.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +7 -0
- package/public/manifest.json +21 -0
- package/public/site.webmanifest +21 -0
- package/public/web-app-manifest-192x192.png +0 -0
- package/public/web-app-manifest-512x512.png +0 -0
- package/shared.env +0 -0
- package/styles/globals.css +125 -0
- package/tsconfig.json +41 -0
package/app/page.tsx
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import AuditLogTable from "@/components/dashboard/audit-log-table"
|
|
4
|
+
import ChartsSection from "@/components/dashboard/charts-section"
|
|
5
|
+
import DashboardHeader from "@/components/dashboard/header"
|
|
6
|
+
import LoginModal from "@/components/dashboard/login-modal"
|
|
7
|
+
import MCPConfigModal from "@/components/dashboard/mcp-config-modal"
|
|
8
|
+
import MetricsGrid from "@/components/dashboard/metrics-grid"
|
|
9
|
+
import SettingsModal from "@/components/dashboard/settings-modal"
|
|
10
|
+
import { useDashboard } from "@/lib/data/dashboard-context"
|
|
11
|
+
import { DashboardData } from "@/lib/data/types"
|
|
12
|
+
import { useEffect, useState } from "react"
|
|
13
|
+
|
|
14
|
+
// Types (Ideally imported, but keeping for now if used elsewhere locally, though strategy returns properly typed data)
|
|
15
|
+
// We use the types from lib/data/types.ts now
|
|
16
|
+
|
|
17
|
+
export default function Dashboard() {
|
|
18
|
+
const { strategy, isDemo, toggleDemo, refreshSignal } = useDashboard()
|
|
19
|
+
|
|
20
|
+
const [showMCPConfig, setShowMCPConfig] = useState(false)
|
|
21
|
+
const [showSettings, setShowSettings] = useState(false)
|
|
22
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
23
|
+
|
|
24
|
+
// Data State
|
|
25
|
+
const [data, setData] = useState<DashboardData>({
|
|
26
|
+
stats: {
|
|
27
|
+
memoryRecords: 0,
|
|
28
|
+
totalClients: 0,
|
|
29
|
+
successRate: 0,
|
|
30
|
+
totalRequests: 0,
|
|
31
|
+
topWriter: { name: "N/A", count: 0 },
|
|
32
|
+
topReader: { name: "N/A", count: 0 },
|
|
33
|
+
lastWriter: { name: "N/A", timestamp: 0 },
|
|
34
|
+
lastReader: { name: "N/A", timestamp: 0 },
|
|
35
|
+
},
|
|
36
|
+
trends: {
|
|
37
|
+
memory: { change: "", trend: "neutral", hasData: false, data: [] },
|
|
38
|
+
clients: { change: "", trend: "neutral", hasData: false, data: [] },
|
|
39
|
+
success: { change: "", trend: "neutral", hasData: false, data: [] },
|
|
40
|
+
requests: { change: "", trend: "neutral", hasData: false, data: [] },
|
|
41
|
+
},
|
|
42
|
+
logs: []
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Check authentication on mount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const auth = sessionStorage.getItem("authenticated")
|
|
48
|
+
if (auth === "true") {
|
|
49
|
+
setIsAuthenticated(true)
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const handleLogin = (password: string) => {
|
|
54
|
+
sessionStorage.setItem("authenticated", "true")
|
|
55
|
+
setIsAuthenticated(true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fetch Data Effect - Reacts to strategy change or refresh signal
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
async function updateData() {
|
|
61
|
+
try {
|
|
62
|
+
const potentialData = await strategy.fetchGlobalStats()
|
|
63
|
+
setData(potentialData)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error("Failed to fetch dashboard data:", e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
updateData()
|
|
69
|
+
}, [strategy, refreshSignal])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// Audit Log internal state for filtering/sorting (UI logic only)
|
|
73
|
+
const [searchTerm, setSearchTerm] = useState("")
|
|
74
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
75
|
+
const [sortField, setSortField] = useState<"date" | "client" | "operation" | "status">("date")
|
|
76
|
+
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc")
|
|
77
|
+
const itemsPerPage = 10
|
|
78
|
+
|
|
79
|
+
const filteredLog = (data.logs || []).filter(
|
|
80
|
+
(log) =>
|
|
81
|
+
log.client.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
82
|
+
log.operation.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
83
|
+
log.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
84
|
+
log.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const handleSort = (field: string) => {
|
|
88
|
+
if (field === sortField) {
|
|
89
|
+
setSortDirection(prev => prev === "asc" ? "desc" : "asc")
|
|
90
|
+
} else {
|
|
91
|
+
setSortField(field as any)
|
|
92
|
+
setSortDirection("asc")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sortedLog = [...filteredLog].sort((a, b) => {
|
|
97
|
+
const modifier = sortDirection === "asc" ? 1 : -1
|
|
98
|
+
if (sortField === "date") {
|
|
99
|
+
return (new Date(a.date).getTime() - new Date(b.date).getTime()) * modifier
|
|
100
|
+
}
|
|
101
|
+
const aValue = (a as any)[sortField] || ""
|
|
102
|
+
const bValue = (b as any)[sortField] || ""
|
|
103
|
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
104
|
+
return aValue.localeCompare(bValue) * modifier
|
|
105
|
+
}
|
|
106
|
+
return 0
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const paginatedLog = sortedLog.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
|
|
110
|
+
const totalPages = Math.ceil(sortedLog.length / itemsPerPage)
|
|
111
|
+
|
|
112
|
+
// Show login modal if not authenticated
|
|
113
|
+
if (!isAuthenticated) {
|
|
114
|
+
return <LoginModal onLogin={handleLogin} />
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="min-h-screen text-foreground">
|
|
119
|
+
<DashboardHeader
|
|
120
|
+
onShowMCPConfig={() => setShowMCPConfig(true)}
|
|
121
|
+
onShowSettings={() => setShowSettings(true)}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<main className="px-6 py-8 max-w-7xl mx-auto space-y-8">
|
|
125
|
+
<MetricsGrid stats={data.stats} trends={data.trends} />
|
|
126
|
+
<ChartsSection period="" />
|
|
127
|
+
<AuditLogTable
|
|
128
|
+
logs={(paginatedLog || []).map(log => ({
|
|
129
|
+
id: log.id,
|
|
130
|
+
date: log.date.toLocaleString(),
|
|
131
|
+
client: log.client,
|
|
132
|
+
operation: log.operation,
|
|
133
|
+
status: log.status,
|
|
134
|
+
description: log.description
|
|
135
|
+
}))}
|
|
136
|
+
loading={false}
|
|
137
|
+
currentPage={currentPage}
|
|
138
|
+
totalPages={totalPages}
|
|
139
|
+
onPageChange={setCurrentPage}
|
|
140
|
+
sortField={sortField}
|
|
141
|
+
sortDirection={sortDirection}
|
|
142
|
+
onSort={handleSort}
|
|
143
|
+
/>
|
|
144
|
+
</main>
|
|
145
|
+
|
|
146
|
+
{showMCPConfig && <MCPConfigModal onClose={() => setShowMCPConfig(false)} />}
|
|
147
|
+
{showSettings && <SettingsModal onClose={() => { setShowSettings(false); }} />}
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, RefreshCw } from "lucide-react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
interface AuditLogTableProps {
|
|
7
|
+
logs: any[]
|
|
8
|
+
loading: boolean
|
|
9
|
+
currentPage: number
|
|
10
|
+
totalPages: number
|
|
11
|
+
onPageChange: (page: number) => void
|
|
12
|
+
sortField: string
|
|
13
|
+
sortDirection: 'asc' | 'desc'
|
|
14
|
+
onSort: (field: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const statusConfig: Record<string, { bg: string; text: string; border: string }> = {
|
|
18
|
+
Success: { bg: "bg-emerald-500/10", text: "text-emerald-400", border: "border-emerald-500/30" },
|
|
19
|
+
Warning: { bg: "bg-amber-500/10", text: "text-amber-400", border: "border-amber-500/30" },
|
|
20
|
+
Error: { bg: "bg-red-500/10", text: "text-red-400", border: "border-red-500/30" },
|
|
21
|
+
Canceled: { bg: "bg-slate-500/10", text: "text-slate-400", border: "border-slate-500/30" },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const periods = [
|
|
25
|
+
{ label: "1 Hour", value: "1h" },
|
|
26
|
+
{ label: "24 Hours", value: "24h" },
|
|
27
|
+
{ label: "7 Days", value: "7d" },
|
|
28
|
+
{ label: "30 Days", value: "30d" },
|
|
29
|
+
{ label: "All Time", value: "all" },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export default function AuditLogTable({
|
|
33
|
+
logs,
|
|
34
|
+
loading,
|
|
35
|
+
currentPage,
|
|
36
|
+
totalPages,
|
|
37
|
+
onPageChange,
|
|
38
|
+
sortField,
|
|
39
|
+
sortDirection,
|
|
40
|
+
onSort
|
|
41
|
+
}: AuditLogTableProps) {
|
|
42
|
+
const [period, setPeriod] = useState("all")
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="group relative overflow-hidden rounded-2xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg p-6 transition-all duration-300">
|
|
46
|
+
{/* Neomorphism glow */}
|
|
47
|
+
<div className="absolute inset-0 bg-gradient-to-br from-white/5 via-transparent to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
|
48
|
+
|
|
49
|
+
{/* Period Selector - Badge Style - Absolute positioned in top-right (ignoring padding) */}
|
|
50
|
+
<div className="absolute top-0 right-0 z-20 group/period">
|
|
51
|
+
<button className="h-8 px-3 rounded-tl-none rounded-tr-2xl 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">
|
|
52
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path
|
|
54
|
+
strokeLinecap="round"
|
|
55
|
+
strokeLinejoin="round"
|
|
56
|
+
strokeWidth={2}
|
|
57
|
+
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
{periods.find((p) => p.value === period)?.label}
|
|
61
|
+
<ChevronDown className="w-3 h-3" />
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
{/* Dropdown Menu */}
|
|
65
|
+
<div className="absolute right-0 mt-2 w-40 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl opacity-0 invisible group-hover/period:opacity-100 group-hover/period:visible transition-all duration-200 z-30 backdrop-blur-xl overflow-hidden">
|
|
66
|
+
{periods.map((p) => (
|
|
67
|
+
<button
|
|
68
|
+
key={p.value}
|
|
69
|
+
onClick={() => setPeriod(p.value)}
|
|
70
|
+
className={`w-full text-left px-3 py-2 text-xs transition-colors ${
|
|
71
|
+
period === p.value
|
|
72
|
+
? "bg-emerald-500/20 text-emerald-400 font-medium"
|
|
73
|
+
: "text-neutral-300 hover:bg-white/5 hover:text-white"
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
{p.label}
|
|
77
|
+
</button>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="relative z-10">
|
|
83
|
+
<div className="flex items-center justify-between mb-6">
|
|
84
|
+
<div className="flex items-center gap-3">
|
|
85
|
+
<h3 className="text-lg font-semibold text-white">Audit Log</h3>
|
|
86
|
+
{loading && <RefreshCw className="w-4 h-4 text-emerald-500 animate-spin" />}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
<div className="overflow-x-auto min-h-[400px]">
|
|
94
|
+
<table className="w-full text-sm">
|
|
95
|
+
<thead>
|
|
96
|
+
<tr className="border-b border-white/10">
|
|
97
|
+
{[
|
|
98
|
+
{ label: "Timestamp", key: "date", width: "w-[200px]" },
|
|
99
|
+
{ label: "Client", key: "client", width: "w-[260px]" },
|
|
100
|
+
{ label: "Operation", key: "operation", width: "w-[120px]" },
|
|
101
|
+
{ label: "Status", key: "status", width: "w-[120px]" },
|
|
102
|
+
{ label: "Description", key: "description", width: "" },
|
|
103
|
+
].map((header) => (
|
|
104
|
+
<th
|
|
105
|
+
key={header.key}
|
|
106
|
+
onClick={() => onSort(header.key)}
|
|
107
|
+
className={`text-left py-4 px-4 font-medium text-neutral-400 cursor-pointer hover:text-white transition-colors select-none group/th ${header.width}`}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
{header.label}
|
|
111
|
+
<div className="flex flex-col">
|
|
112
|
+
{sortField === header.key ? (
|
|
113
|
+
sortDirection === 'asc' ?
|
|
114
|
+
<ArrowUp className="w-3 h-3 text-emerald-400" /> :
|
|
115
|
+
<ArrowDown className="w-3 h-3 text-emerald-400" />
|
|
116
|
+
) : (
|
|
117
|
+
<ArrowUpDown className="w-3 h-3 text-neutral-700 group-hover/th:text-neutral-500 transition-colors" />
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</th>
|
|
122
|
+
))}
|
|
123
|
+
</tr>
|
|
124
|
+
</thead>
|
|
125
|
+
<tbody>
|
|
126
|
+
{loading && logs.length === 0 ? (
|
|
127
|
+
// Loading skeleton
|
|
128
|
+
Array.from({ length: 5 }).map((_, i) => (
|
|
129
|
+
<tr key={i} className="border-b border-white/5">
|
|
130
|
+
<td className="py-4 px-4"><div className="h-4 w-32 bg-white/5 rounded animate-pulse" /></td>
|
|
131
|
+
<td className="py-4 px-4"><div className="h-4 w-24 bg-white/5 rounded animate-pulse" /></td>
|
|
132
|
+
<td className="py-4 px-4"><div className="h-4 w-16 bg-white/5 rounded animate-pulse" /></td>
|
|
133
|
+
<td className="py-4 px-4"><div className="h-6 w-20 bg-white/5 rounded-full animate-pulse" /></td>
|
|
134
|
+
<td className="py-4 px-4"><div className="h-4 w-40 bg-white/5 rounded animate-pulse" /></td>
|
|
135
|
+
</tr>
|
|
136
|
+
))
|
|
137
|
+
) : (
|
|
138
|
+
logs.map((log) => {
|
|
139
|
+
const config = statusConfig[log.status] || statusConfig.Success
|
|
140
|
+
return (
|
|
141
|
+
<tr key={log.id} className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row">
|
|
142
|
+
<td className="py-4 px-4 text-neutral-300 group-hover/row:text-white transition-colors">{log.date}</td>
|
|
143
|
+
<td className="py-4 px-4 text-white font-medium">{log.client}</td>
|
|
144
|
+
<td className="py-4 px-4 text-neutral-300">{log.operation}</td>
|
|
145
|
+
<td className="py-4 px-4">
|
|
146
|
+
<span
|
|
147
|
+
className={`px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
|
|
148
|
+
>
|
|
149
|
+
{log.status}
|
|
150
|
+
</span>
|
|
151
|
+
</td>
|
|
152
|
+
<td className="py-4 px-4 text-neutral-400">{log.description}</td>
|
|
153
|
+
</tr>
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{!loading && logs.length === 0 && (
|
|
159
|
+
<tr>
|
|
160
|
+
<td colSpan={5} className="py-12 text-center text-neutral-500">
|
|
161
|
+
No logs found for this period
|
|
162
|
+
</td>
|
|
163
|
+
</tr>
|
|
164
|
+
)}
|
|
165
|
+
</tbody>
|
|
166
|
+
</table>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="mt-6 flex items-center justify-between">
|
|
170
|
+
<p className="text-sm text-neutral-500">
|
|
171
|
+
Page {currentPage} of {Math.max(1, totalPages)}
|
|
172
|
+
</p>
|
|
173
|
+
<div className="flex gap-2">
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
|
176
|
+
disabled={currentPage === 1}
|
|
177
|
+
className="px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
|
178
|
+
>
|
|
179
|
+
<ChevronLeft className="w-4 h-4" />
|
|
180
|
+
Previous
|
|
181
|
+
</button>
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
|
184
|
+
disabled={currentPage === totalPages || totalPages === 0}
|
|
185
|
+
className="px-3 py-2 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
|
186
|
+
>
|
|
187
|
+
Next
|
|
188
|
+
<ChevronRight className="w-4 h-4" />
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import dynamic from "next/dynamic";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
|
|
9
|
+
// Dynamic import with SSR disabled
|
|
10
|
+
const MetricsChart = dynamic(() => import("./metrics-chart"), { ssr: false });
|
|
11
|
+
|
|
12
|
+
interface ChartCardProps {
|
|
13
|
+
service: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Fallback color generator
|
|
17
|
+
function stringToColor(str: string): string {
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < str.length; i++) {
|
|
20
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
21
|
+
}
|
|
22
|
+
let color = '#';
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
const value = (hash >> (i * 8)) & 0xFF;
|
|
25
|
+
color += ('00' + value.toString(16)).substr(-2);
|
|
26
|
+
}
|
|
27
|
+
return color;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const periods = [
|
|
31
|
+
{ value: "1h", label: "1 Hour" },
|
|
32
|
+
{ value: "24h", label: "24 Hours" },
|
|
33
|
+
{ value: "7d", label: "7 Days" },
|
|
34
|
+
{ value: "30d", label: "30 Days" },
|
|
35
|
+
{ value: "90d", label: "90 Days" },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export default function ChartCard({ service }: ChartCardProps) {
|
|
39
|
+
const { strategy, refreshSignal, clientConfigs } = useDashboard()
|
|
40
|
+
const [period, setPeriod] = useState("24h")
|
|
41
|
+
const [hovered, setHovered] = useState<string | null>(null)
|
|
42
|
+
const [data, setData] = useState<any[]>([])
|
|
43
|
+
const [clientNames, setClientNames] = useState<string[]>([])
|
|
44
|
+
const [clientMetadata, setClientMetadata] = useState<Record<string, any>>({})
|
|
45
|
+
const [loading, setLoading] = useState(true)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
async function fetchData() {
|
|
49
|
+
// Only show loading state on initial load or period change, not background refresh
|
|
50
|
+
// We check if data is empty to determine initial load
|
|
51
|
+
if (data.length === 0) setLoading(true)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const timeSeriesData = await strategy.getChartData(period)
|
|
55
|
+
|
|
56
|
+
// Update client metadata if provided in response
|
|
57
|
+
if (timeSeriesData.metadata) {
|
|
58
|
+
setClientMetadata(prev => ({ ...prev, ...timeSeriesData.metadata }))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Helper to format time based on period
|
|
62
|
+
const formatSeries = (series: any[]) => {
|
|
63
|
+
if (!series) return []
|
|
64
|
+
return series.map(point => {
|
|
65
|
+
const date = new Date((point.time as number) * 1000)
|
|
66
|
+
let timeStr = ""
|
|
67
|
+
// Show date if period is longer than 24h
|
|
68
|
+
if (["7d", "30d", "90d", "all"].includes(period)) {
|
|
69
|
+
timeStr = date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
70
|
+
} else {
|
|
71
|
+
timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...point,
|
|
75
|
+
time: timeStr
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract client names from series and sort by total value (Ascending)
|
|
81
|
+
const getClients = (series: any[]) => {
|
|
82
|
+
if (!series || series.length === 0) return []
|
|
83
|
+
const keys = new Set<string>()
|
|
84
|
+
const totals: Record<string, number> = {}
|
|
85
|
+
|
|
86
|
+
series.forEach(point => {
|
|
87
|
+
Object.keys(point).forEach(k => {
|
|
88
|
+
if (k !== 'time') {
|
|
89
|
+
keys.add(k)
|
|
90
|
+
totals[k] = (totals[k] || 0) + (point[k] as number)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return Array.from(keys).sort((a, b) => (totals[a] || 0) - (totals[b] || 0))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get data based on service type
|
|
99
|
+
let seriesData: any[] = []
|
|
100
|
+
let clients: string[] = []
|
|
101
|
+
|
|
102
|
+
if (service.includes("Creates")) {
|
|
103
|
+
seriesData = formatSeries(timeSeriesData.creates)
|
|
104
|
+
clients = getClients(timeSeriesData.creates)
|
|
105
|
+
} else if (service.includes("Reads")) {
|
|
106
|
+
seriesData = formatSeries(timeSeriesData.reads)
|
|
107
|
+
clients = getClients(timeSeriesData.reads)
|
|
108
|
+
} else if (service.includes("Updates")) {
|
|
109
|
+
seriesData = formatSeries(timeSeriesData.updates)
|
|
110
|
+
clients = getClients(timeSeriesData.updates)
|
|
111
|
+
} else if (service.includes("Deletes")) {
|
|
112
|
+
seriesData = formatSeries(timeSeriesData.deletes)
|
|
113
|
+
clients = getClients(timeSeriesData.deletes)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setData(seriesData)
|
|
117
|
+
setClientNames(clients)
|
|
118
|
+
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error("Failed to fetch chart data:", e)
|
|
121
|
+
} finally {
|
|
122
|
+
setLoading(false)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
fetchData()
|
|
126
|
+
}, [period, service, strategy, refreshSignal])
|
|
127
|
+
|
|
128
|
+
const isMultiSeries = clientNames.length > 0
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Card className="bg-white/5 border-white/10 backdrop-blur-md relative overflow-visible pt-6 pb-2">
|
|
132
|
+
<button
|
|
133
|
+
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"
|
|
134
|
+
onClick={() => document.getElementById(`dropdown-${service}`)?.classList.toggle('hidden')}
|
|
135
|
+
>
|
|
136
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
137
|
+
<path
|
|
138
|
+
strokeLinecap="round"
|
|
139
|
+
strokeLinejoin="round"
|
|
140
|
+
strokeWidth={2}
|
|
141
|
+
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
142
|
+
/>
|
|
143
|
+
</svg>
|
|
144
|
+
{periods.find((p) => p.value === period)?.label}
|
|
145
|
+
<ChevronDown className="w-3 h-3" />
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
{/* Dropdown Menu - Positioned relative to the button or card */}
|
|
149
|
+
<div id={`dropdown-${service}`} className="hidden absolute top-8 right-0 w-40 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-30 backdrop-blur-xl overflow-hidden">
|
|
150
|
+
{periods.map((p) => (
|
|
151
|
+
<button
|
|
152
|
+
key={p.value}
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setPeriod(p.value);
|
|
155
|
+
document.getElementById(`dropdown-${service}`)?.classList.add('hidden');
|
|
156
|
+
}}
|
|
157
|
+
className={`w-full text-left px-3 py-2 text-xs transition-colors ${
|
|
158
|
+
period === p.value
|
|
159
|
+
? "bg-emerald-500/20 text-emerald-400 font-medium"
|
|
160
|
+
: "text-neutral-300 hover:bg-white/5 hover:text-white"
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
{p.label}
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<CardHeader className="relative">
|
|
169
|
+
<div className="flex items-center justify-between">
|
|
170
|
+
<div>
|
|
171
|
+
<CardTitle className="text-sm font-medium text-slate-400">Time Series</CardTitle>
|
|
172
|
+
<div className="text-2xl font-bold text-white">{service}</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
<CardContent>
|
|
177
|
+
{loading ? (
|
|
178
|
+
<div className="h-[200px] w-full flex items-center justify-center">
|
|
179
|
+
<div className="text-neutral-500 text-sm">Loading...</div>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<div className="h-[200px] w-full">
|
|
183
|
+
<MetricsChart
|
|
184
|
+
data={data}
|
|
185
|
+
isMultiSeries={isMultiSeries}
|
|
186
|
+
clientNames={clientNames}
|
|
187
|
+
clientConfigs={clientConfigs}
|
|
188
|
+
hovered={hovered}
|
|
189
|
+
setHovered={setHovered}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import ChartCard from "./chart-card";
|
|
4
|
+
|
|
5
|
+
export default function ChartsSection({ period }: { period: string }) {
|
|
6
|
+
// Remove the period prop since each chart will have its own selector
|
|
7
|
+
// Just render the charts, they'll manage their own data
|
|
8
|
+
return (
|
|
9
|
+
<div className="grid grid-cols-2 gap-6">
|
|
10
|
+
<ChartCard service="Creates by Client" />
|
|
11
|
+
<ChartCard service="Reads by Client" />
|
|
12
|
+
<ChartCard service="Updates by Client" />
|
|
13
|
+
<ChartCard service="Deletes by Client" />
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Book, Settings } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
|
|
7
|
+
export default function DashboardHeader({
|
|
8
|
+
onShowMCPConfig,
|
|
9
|
+
onShowSettings,
|
|
10
|
+
}: {
|
|
11
|
+
onShowMCPConfig: () => void;
|
|
12
|
+
onShowSettings: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
const [isScrolled, setIsScrolled] = useState(false)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Check initial scroll position on mount
|
|
18
|
+
setIsScrolled(window.scrollY > 10)
|
|
19
|
+
|
|
20
|
+
const handleScroll = () => {
|
|
21
|
+
setIsScrolled(window.scrollY > 10)
|
|
22
|
+
}
|
|
23
|
+
window.addEventListener("scroll", handleScroll)
|
|
24
|
+
return () => window.removeEventListener("scroll", handleScroll)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<header className={`sticky top-0 z-50 transition-all duration-300 ${
|
|
29
|
+
isScrolled
|
|
30
|
+
? "border-b border-white/10 backdrop-blur-xl bg-neutral-900/30"
|
|
31
|
+
: "border-b border-transparent bg-transparent"
|
|
32
|
+
}`}>
|
|
33
|
+
<div className="px-6 py-5 max-w-7xl mx-auto">
|
|
34
|
+
<div className="flex items-center justify-between">
|
|
35
|
+
<div className="flex items-center gap-4">
|
|
36
|
+
<div className="relative w-10 h-10 flex-shrink-0">
|
|
37
|
+
<img src="/logo.svg" alt="CyberMem Logo" className="w-full h-full object-contain" />
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">CyberMem</h1>
|
|
41
|
+
<p className="text-sm text-neutral-400 mt-1">Memory MCP Server</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="flex items-center gap-3">
|
|
46
|
+
<Button
|
|
47
|
+
variant="ghost"
|
|
48
|
+
size="sm"
|
|
49
|
+
onClick={onShowMCPConfig}
|
|
50
|
+
className="hidden md:flex h-10 px-4 text-sm font-medium bg-emerald-500/10 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 border border-emerald-500/20 hover:border-emerald-500/40 rounded-lg"
|
|
51
|
+
>
|
|
52
|
+
<img src="/icons/mcp.png" alt="MCP" className="w-4 h-4 mr-2" />
|
|
53
|
+
Connect MCP
|
|
54
|
+
</Button>
|
|
55
|
+
|
|
56
|
+
<Button
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="sm"
|
|
59
|
+
asChild
|
|
60
|
+
className="hidden md:flex h-10 px-4 text-sm font-medium text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
|
|
61
|
+
>
|
|
62
|
+
<a href="https://cybermem.dev/docs" target="_blank">
|
|
63
|
+
<Book className="w-4 h-4 mr-2" />
|
|
64
|
+
Docs
|
|
65
|
+
</a>
|
|
66
|
+
</Button>
|
|
67
|
+
|
|
68
|
+
<Button
|
|
69
|
+
variant="ghost"
|
|
70
|
+
size="icon"
|
|
71
|
+
onClick={onShowSettings}
|
|
72
|
+
className="h-10 w-10 text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
|
|
73
|
+
>
|
|
74
|
+
<Settings className="w-5 h-5" />
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</header>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|