express-performance-toolkit 1.0.0 → 2.0.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.
Files changed (78) hide show
  1. package/README.md +119 -76
  2. package/dashboard-ui/README.md +73 -0
  3. package/dashboard-ui/eslint.config.js +23 -0
  4. package/dashboard-ui/index.html +13 -0
  5. package/dashboard-ui/package-lock.json +3382 -0
  6. package/dashboard-ui/package.json +32 -0
  7. package/dashboard-ui/src/App.css +184 -0
  8. package/dashboard-ui/src/App.tsx +182 -0
  9. package/dashboard-ui/src/components/BlockedModal.tsx +108 -0
  10. package/dashboard-ui/src/components/CachePanel.tsx +45 -0
  11. package/dashboard-ui/src/components/HealthCharts.tsx +142 -0
  12. package/dashboard-ui/src/components/InsightsPanel.tsx +49 -0
  13. package/dashboard-ui/src/components/KpiGrid.tsx +178 -0
  14. package/dashboard-ui/src/components/LiveLogs.tsx +76 -0
  15. package/dashboard-ui/src/components/Login.tsx +83 -0
  16. package/dashboard-ui/src/components/RoutesTable.tsx +110 -0
  17. package/dashboard-ui/src/hooks/useMetrics.ts +131 -0
  18. package/dashboard-ui/src/index.css +652 -0
  19. package/dashboard-ui/src/main.tsx +10 -0
  20. package/dashboard-ui/src/pages/InsightsPage.tsx +42 -0
  21. package/dashboard-ui/src/pages/LogsPage.tsx +26 -0
  22. package/dashboard-ui/src/pages/OverviewPage.tsx +32 -0
  23. package/dashboard-ui/src/pages/RoutesPage.tsx +26 -0
  24. package/dashboard-ui/src/utils/formatters.ts +27 -0
  25. package/dashboard-ui/tsconfig.app.json +28 -0
  26. package/dashboard-ui/tsconfig.json +7 -0
  27. package/dashboard-ui/tsconfig.node.json +26 -0
  28. package/dashboard-ui/vite.config.ts +12 -0
  29. package/dist/analyzer.d.ts +6 -0
  30. package/dist/analyzer.d.ts.map +1 -0
  31. package/dist/analyzer.js +70 -0
  32. package/dist/analyzer.js.map +1 -0
  33. package/dist/dashboard/dashboardRouter.d.ts +4 -4
  34. package/dist/dashboard/dashboardRouter.d.ts.map +1 -1
  35. package/dist/dashboard/dashboardRouter.js +67 -21
  36. package/dist/dashboard/dashboardRouter.js.map +1 -1
  37. package/dist/dashboard-ui/assets/index-CX-zE-Qy.css +1 -0
  38. package/dist/dashboard-ui/assets/index-Q9TGkd8n.js +41 -0
  39. package/dist/dashboard-ui/index.html +14 -0
  40. package/dist/index.d.ts +11 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +35 -11
  43. package/dist/index.js.map +1 -1
  44. package/dist/logger.d.ts +3 -3
  45. package/dist/logger.d.ts.map +1 -1
  46. package/dist/logger.js +167 -9
  47. package/dist/logger.js.map +1 -1
  48. package/dist/queryHelper.d.ts.map +1 -1
  49. package/dist/queryHelper.js +1 -0
  50. package/dist/queryHelper.js.map +1 -1
  51. package/dist/rateLimit.d.ts +5 -0
  52. package/dist/rateLimit.d.ts.map +1 -0
  53. package/dist/rateLimit.js +67 -0
  54. package/dist/rateLimit.js.map +1 -0
  55. package/dist/store.d.ts +9 -2
  56. package/dist/store.d.ts.map +1 -1
  57. package/dist/store.js +147 -25
  58. package/dist/store.js.map +1 -1
  59. package/dist/types.d.ts +93 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/example/server.ts +68 -37
  62. package/package.json +9 -6
  63. package/src/analyzer.ts +78 -0
  64. package/src/dashboard/dashboardRouter.ts +88 -23
  65. package/src/index.ts +70 -30
  66. package/src/logger.ts +177 -13
  67. package/src/queryHelper.ts +2 -0
  68. package/src/rateLimit.ts +86 -0
  69. package/src/store.ts +136 -27
  70. package/src/types.ts +98 -0
  71. package/tests/analyzer.test.ts +108 -0
  72. package/tests/auth.test.ts +79 -0
  73. package/tests/bandwidth.test.ts +72 -0
  74. package/tests/integration.test.ts +51 -54
  75. package/tests/rateLimit.test.ts +57 -0
  76. package/tests/store.test.ts +37 -18
  77. package/tsconfig.json +1 -0
  78. package/src/dashboard/dashboard.html +0 -756
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "dashboard-ui",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^0.577.0",
14
+ "react": "^19.2.4",
15
+ "react-dom": "^19.2.4",
16
+ "recharts": "^3.8.0"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.39.4",
20
+ "@types/node": "^24.12.0",
21
+ "@types/react": "^19.2.14",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitejs/plugin-react": "^6.0.1",
24
+ "eslint": "^9.39.4",
25
+ "eslint-plugin-react-hooks": "^7.0.1",
26
+ "eslint-plugin-react-refresh": "^0.5.2",
27
+ "globals": "^17.4.0",
28
+ "typescript": "~5.9.3",
29
+ "typescript-eslint": "^8.57.0",
30
+ "vite": "^8.0.1"
31
+ }
32
+ }
@@ -0,0 +1,184 @@
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
@@ -0,0 +1,182 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ AlertTriangle,
4
+ Zap,
5
+ LogOut,
6
+ LayoutDashboard,
7
+ Route,
8
+ Bell,
9
+ Terminal,
10
+ } from "lucide-react";
11
+ import { useMetrics } from "./hooks/useMetrics";
12
+ import { formatUptime } from "./utils/formatters";
13
+ import { Login } from "./components/Login";
14
+
15
+ // Pages
16
+ import { OverviewPage } from "./pages/OverviewPage";
17
+ import { RoutesPage } from "./pages/RoutesPage";
18
+ import { InsightsPage } from "./pages/InsightsPage";
19
+ import { LogsPage } from "./pages/LogsPage";
20
+
21
+ type PageType = "overview" | "routes" | "insights" | "logs";
22
+
23
+ export default function App() {
24
+ const [activePage, setActivePage] = useState<PageType>("overview");
25
+ const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
26
+ const [isAuthRequired, setIsAuthRequired] = useState(false);
27
+
28
+ const { data, history, error } = useMetrics(
29
+ isAuthenticated === true || (isAuthenticated !== null && !isAuthRequired),
30
+ );
31
+
32
+ const checkAuth = async () => {
33
+ try {
34
+ const resp = await fetch("./api/auth-check");
35
+ if (!resp.ok) throw new Error("Auth check failed");
36
+ const authData = await resp.json();
37
+ setIsAuthenticated(authData.authenticated);
38
+ setIsAuthRequired(authData.required);
39
+ } catch (err) {
40
+ console.error("Auth check failed", err);
41
+ }
42
+ };
43
+
44
+ const handleLogout = async () => {
45
+ try {
46
+ await fetch("./api/logout", { method: "POST" });
47
+ setIsAuthenticated(false);
48
+ } catch (err) {
49
+ console.error("Logout failed", err);
50
+ }
51
+ };
52
+
53
+ useEffect(() => {
54
+ checkAuth();
55
+ }, []);
56
+
57
+ useEffect(() => {
58
+ if (String(error) === "Unauthorized" && isAuthenticated !== false) {
59
+ checkAuth();
60
+ }
61
+ }, [error, isAuthenticated]);
62
+
63
+ if (isAuthRequired && isAuthenticated === false) {
64
+ return <Login onLoginSuccess={() => setIsAuthenticated(true)} />;
65
+ }
66
+
67
+ if (error && String(error) !== "Unauthorized") {
68
+ return (
69
+ <div className="dashboard-wrapper centered">
70
+ <div className="panel error-state">
71
+ <AlertTriangle size={48} color="var(--accent-rose)" />
72
+ <h2>Connection Lost</h2>
73
+ <p>
74
+ Unable to connect to the Performance API. Ensure your Express server
75
+ is running.
76
+ </p>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ if (!data || isAuthenticated === null)
83
+ return (
84
+ <div className="dashboard-wrapper empty-state">Connecting to API...</div>
85
+ );
86
+
87
+ const renderPage = () => {
88
+ switch (activePage) {
89
+ case "overview":
90
+ return <OverviewPage data={data} history={history} />;
91
+ case "routes":
92
+ return <RoutesPage data={data} />;
93
+ case "insights":
94
+ return <InsightsPage data={data} />;
95
+ case "logs":
96
+ return <LogsPage data={data} />;
97
+ default:
98
+ return <OverviewPage data={data} history={history} />;
99
+ }
100
+ };
101
+
102
+ return (
103
+ <>
104
+ <nav className="navbar animate-in">
105
+ <div
106
+ className="brand"
107
+ onClick={() => setActivePage("overview")}
108
+ style={{ cursor: "pointer" }}
109
+ >
110
+ <div className="brand-icon">
111
+ <Zap size={18} />
112
+ </div>
113
+ <div className="brand-title">
114
+ Express <span>Performance</span> Toolkit
115
+ </div>
116
+ </div>
117
+
118
+ <div className="nav-links">
119
+ <button
120
+ className={`nav-link ${activePage === "overview" ? "active" : ""}`}
121
+ onClick={() => setActivePage("overview")}
122
+ >
123
+ <LayoutDashboard size={16} /> Overview
124
+ </button>
125
+ <button
126
+ className={`nav-link ${activePage === "routes" ? "active" : ""}`}
127
+ onClick={() => setActivePage("routes")}
128
+ >
129
+ <Route size={16} /> Routes
130
+ </button>
131
+ <button
132
+ className={`nav-link ${activePage === "insights" ? "active" : ""}`}
133
+ onClick={() => setActivePage("insights")}
134
+ >
135
+ <Bell size={16} /> Insights
136
+ {data.insights.length > 0 && (
137
+ <span className="badge">{data.insights.length}</span>
138
+ )}
139
+ </button>
140
+ <button
141
+ className={`nav-link ${activePage === "logs" ? "active" : ""}`}
142
+ onClick={() => setActivePage("logs")}
143
+ >
144
+ <Terminal size={16} /> Logs
145
+ </button>
146
+ </div>
147
+
148
+ <div className="nav-actions">
149
+ <div className="live-indicator hide-mobile">
150
+ <div className="pulse-dot"></div> Live
151
+ <span className="uptime-mono">{formatUptime(data.uptime)}</span>
152
+ </div>
153
+ <div className="live-indicator">
154
+ <span>Lag</span>
155
+ <span
156
+ className="lag-mono"
157
+ style={{
158
+ color:
159
+ data.eventLoopLag > 100
160
+ ? "var(--accent-rose)"
161
+ : "var(--accent-emerald)",
162
+ }}
163
+ >
164
+ {data.eventLoopLag}ms
165
+ </span>
166
+ </div>
167
+ {isAuthRequired && (
168
+ <button
169
+ className="nav-btn logout-btn"
170
+ onClick={handleLogout}
171
+ title="Logout"
172
+ >
173
+ <LogOut size={16} />
174
+ </button>
175
+ )}
176
+ </div>
177
+ </nav>
178
+
179
+ <div className="dashboard-wrapper">{renderPage()}</div>
180
+ </>
181
+ );
182
+ }
@@ -0,0 +1,108 @@
1
+ import { X, ShieldAlert, Clock, Globe } from "lucide-react";
2
+ import type { BlockedEvent } from "../hooks/useMetrics";
3
+
4
+ interface BlockedModalProps {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ events: BlockedEvent[];
8
+ }
9
+
10
+ export function BlockedModal({ isOpen, onClose, events }: BlockedModalProps) {
11
+ if (!isOpen) return null;
12
+
13
+ return (
14
+ <div className="modal-overlay" onClick={onClose}>
15
+ <div
16
+ className="modal-content animate-in"
17
+ onClick={(e) => e.stopPropagation()}
18
+ >
19
+ <div className="modal-header">
20
+ <div className="modal-title">
21
+ <ShieldAlert
22
+ size={20}
23
+ className="val-rose"
24
+ style={{ marginRight: 10 }}
25
+ />
26
+ Blocked IP Monitor
27
+ </div>
28
+ <button className="modal-close" onClick={onClose}>
29
+ <X size={20} />
30
+ </button>
31
+ </div>
32
+
33
+ <div className="modal-body">
34
+ {events.length === 0 ? (
35
+ <div className="empty-state">
36
+ <p>No blocked traffic detected yet.</p>
37
+ </div>
38
+ ) : (
39
+ <div className="blocked-list">
40
+ <table className="routes-table">
41
+ <thead>
42
+ <tr>
43
+ <th>Timestamp</th>
44
+ <th>IP Address</th>
45
+ <th>Method</th>
46
+ <th>Path</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ {[...events].reverse().map((event, i) => (
51
+ <tr key={i}>
52
+ <td>
53
+ <div
54
+ style={{
55
+ display: "flex",
56
+ alignItems: "center",
57
+ gap: 6,
58
+ fontSize: "0.8rem",
59
+ color: "var(--text-400)",
60
+ }}
61
+ >
62
+ <Clock size={12} />
63
+ {new Date(event.timestamp).toLocaleTimeString()}
64
+ </div>
65
+ </td>
66
+ <td>
67
+ <div
68
+ style={{
69
+ display: "flex",
70
+ alignItems: "center",
71
+ gap: 6,
72
+ fontWeight: 600,
73
+ }}
74
+ >
75
+ <Globe size={12} className="val-rose" />
76
+ {event.ip}
77
+ </div>
78
+ </td>
79
+ <td>
80
+ <span
81
+ className={`method-badge ${event.method.toLowerCase()}`}
82
+ >
83
+ {event.method}
84
+ </span>
85
+ </td>
86
+ <td
87
+ style={{ fontFamily: "monospace", fontSize: "0.9rem" }}
88
+ >
89
+ {event.path}
90
+ </td>
91
+ </tr>
92
+ ))}
93
+ </tbody>
94
+ </table>
95
+ </div>
96
+ )}
97
+ </div>
98
+
99
+ <div className="modal-footer">
100
+ <p style={{ fontSize: "0.8rem", color: "var(--text-400)" }}>
101
+ Showing last {events.length} security events. Use this data to
102
+ identify brute-force patterns.
103
+ </p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,45 @@
1
+ import type { MetricsData } from '../hooks/useMetrics';
2
+ import { fNum } from '../utils/formatters';
3
+
4
+ export function CachePanel({ data }: { data: MetricsData }) {
5
+ const hitPct = data.cacheHitRate;
6
+ return (
7
+ <div className="panel animate-in delay-4" style={{ flex: 1 }}>
8
+ <div className="panel-header">
9
+ <div className="panel-title">📦 Cache Engine</div>
10
+ </div>
11
+ <div className="panel-body">
12
+ <div className="cache-viz">
13
+ <div className="donut-wrap">
14
+ <svg viewBox="0 0 42 42" width="140" height="140" style={{ transform: "rotate(-90deg)" }}>
15
+ <circle cx="21" cy="21" r="15.915" fill="none" stroke="var(--bg-hover)" strokeWidth="6" />
16
+ <circle cx="21" cy="21" r="15.915" fill="none" stroke="url(#gradHit)" strokeWidth="6" strokeDasharray={`${hitPct} ${100 - hitPct}`} strokeLinecap="round" style={{ transition: "stroke-dasharray 1s cubic-bezier(0.4, 0, 0.2, 1)" }} />
17
+ <defs>
18
+ <linearGradient id="gradHit" x1="0" y1="0" x2="0" y2="1">
19
+ <stop offset="0%" stopColor="var(--accent-cyan)" />
20
+ <stop offset="100%" stopColor="var(--accent-emerald)" />
21
+ </linearGradient>
22
+ </defs>
23
+ </svg>
24
+ <div className="donut-center" style={{ transform: "scale(0.9)" }}>
25
+ <div className="donut-val">{hitPct}%</div>
26
+ <div className="donut-lbl">Hits</div>
27
+ </div>
28
+ </div>
29
+ <div className="legend">
30
+ <div className="legend-item">
31
+ <div className="legend-dot" style={{ background: "var(--grad-success)" }}></div>
32
+ <span>Hits</span>
33
+ <span className="legend-val">{fNum(data.cacheHits)}</span>
34
+ </div>
35
+ <div className="legend-item">
36
+ <div className="legend-dot" style={{ background: "var(--bg-hover)", border: "1px solid var(--text-400)" }}></div>
37
+ <span>Misses</span>
38
+ <span className="legend-val">{fNum(data.cacheMisses)}</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,142 @@
1
+ import { Activity } from "lucide-react";
2
+ import { AreaChart, Area, Tooltip, ResponsiveContainer } from "recharts";
3
+
4
+ export type ChartDataPoint = {
5
+ time: string;
6
+ lag: number;
7
+ memory: number;
8
+ };
9
+
10
+ export function HealthCharts({ history }: { history: ChartDataPoint[] }) {
11
+ return (
12
+ <div
13
+ className="panel animate-in delay-3"
14
+ style={{ flex: 1, minHeight: "260px" }}
15
+ >
16
+ <div className="panel-header">
17
+ <div className="panel-title">
18
+ <Activity size={18} /> Server Health (Live)
19
+ </div>
20
+ </div>
21
+ <div
22
+ className="panel-body"
23
+ style={{
24
+ padding: "1rem",
25
+ display: "flex",
26
+ flexDirection: "column",
27
+ gap: "1rem",
28
+ }}
29
+ >
30
+ <div style={{ height: "90px", width: "100%" }}>
31
+ <div
32
+ style={{
33
+ fontSize: "0.75rem",
34
+ color: "var(--text-400)",
35
+ marginBottom: "4px",
36
+ display: "flex",
37
+ justifyContent: "space-between",
38
+ }}
39
+ >
40
+ <span>Event Loop Lag (ms)</span>
41
+ <span style={{ color: "var(--accent-cyan)" }}>
42
+ Latest: {history[history.length - 1]?.lag || 0}ms
43
+ </span>
44
+ </div>
45
+ <ResponsiveContainer width="100%" height="100%">
46
+ <AreaChart
47
+ data={history}
48
+ margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
49
+ >
50
+ <defs>
51
+ <linearGradient id="lagColor" x1="0" y1="0" x2="0" y2="1">
52
+ <stop
53
+ offset="5%"
54
+ stopColor="var(--accent-cyan)"
55
+ stopOpacity={0.3}
56
+ />
57
+ <stop
58
+ offset="95%"
59
+ stopColor="var(--accent-cyan)"
60
+ stopOpacity={0}
61
+ />
62
+ </linearGradient>
63
+ </defs>
64
+ <Tooltip
65
+ contentStyle={{
66
+ background: "var(--bg-surface-glass)",
67
+ border: "1px solid var(--border)",
68
+ borderRadius: "8px",
69
+ fontSize: "0.8rem",
70
+ }}
71
+ itemStyle={{ color: "var(--text-100)" }}
72
+ />
73
+ <Area
74
+ type="monotone"
75
+ dataKey="lag"
76
+ stroke="var(--accent-cyan)"
77
+ fillOpacity={1}
78
+ fill="url(#lagColor)"
79
+ isAnimationActive={false}
80
+ />
81
+ </AreaChart>
82
+ </ResponsiveContainer>
83
+ </div>
84
+
85
+ <div style={{ height: "90px", width: "100%" }}>
86
+ <div
87
+ style={{
88
+ fontSize: "0.75rem",
89
+ color: "var(--text-400)",
90
+ marginBottom: "4px",
91
+ display: "flex",
92
+ justifyContent: "space-between",
93
+ }}
94
+ >
95
+ <span>Heap Memory Used (MB)</span>
96
+ <span style={{ color: "var(--accent-indigo)" }}>
97
+ Latest: {history[history.length - 1]?.memory || 0}MB
98
+ </span>
99
+ </div>
100
+ <ResponsiveContainer width="100%" height="100%">
101
+ <AreaChart
102
+ data={history}
103
+ margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
104
+ >
105
+ <defs>
106
+ <linearGradient id="memColor" x1="0" y1="0" x2="0" y2="1">
107
+ <stop
108
+ offset="5%"
109
+ stopColor="var(--accent-indigo)"
110
+ stopOpacity={0.3}
111
+ />
112
+ <stop
113
+ offset="95%"
114
+ stopColor="var(--accent-indigo)"
115
+ stopOpacity={0}
116
+ />
117
+ </linearGradient>
118
+ </defs>
119
+ <Tooltip
120
+ contentStyle={{
121
+ background: "var(--bg-surface-glass)",
122
+ border: "1px solid var(--border)",
123
+ borderRadius: "8px",
124
+ fontSize: "0.8rem",
125
+ }}
126
+ itemStyle={{ color: "var(--text-100)" }}
127
+ />
128
+ <Area
129
+ type="monotone"
130
+ dataKey="memory"
131
+ stroke="var(--accent-indigo)"
132
+ fillOpacity={1}
133
+ fill="url(#memColor)"
134
+ isAnimationActive={false}
135
+ />
136
+ </AreaChart>
137
+ </ResponsiveContainer>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }