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,26 @@
1
+ import { LiveLogs } from "../components/LiveLogs";
2
+ import type { MetricsData } from "../hooks/useMetrics";
3
+ import { Terminal } from "lucide-react";
4
+
5
+ interface LogsPageProps {
6
+ data: MetricsData;
7
+ }
8
+
9
+ export function LogsPage({ data }: LogsPageProps) {
10
+ return (
11
+ <div className="page-content animate-in" style={{ height: 'calc(100vh - 160px)', display: 'flex', flexDirection: 'column' }}>
12
+ <div className="panel-header" style={{ marginBottom: '1rem', justifyContent: 'flex-start', gap: '0.75rem' }}>
13
+ <div className="brand-icon" style={{ width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center' }}>
14
+ <Terminal size={18} />
15
+ </div>
16
+ <div>
17
+ <h2 style={{ fontSize: '1.25rem' }}>System Logs</h2>
18
+ <p style={{ color: 'var(--text-400)', fontSize: '0.9rem' }}>Real-time request stream and performance events.</p>
19
+ </div>
20
+ </div>
21
+ <div style={{ flex: 1, minHeight: 0 }}>
22
+ <LiveLogs logs={data.recentLogs} />
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,32 @@
1
+ import { KpiGrid } from "../components/KpiGrid";
2
+ import { HealthCharts } from "../components/HealthCharts";
3
+ import { CachePanel } from "../components/CachePanel";
4
+ import type { MetricsData, HistoryData } from "../hooks/useMetrics";
5
+ import { Zap } from "lucide-react";
6
+
7
+ interface OverviewPageProps {
8
+ data: MetricsData;
9
+ history: HistoryData[];
10
+ }
11
+
12
+ export function OverviewPage({ data, history }: OverviewPageProps) {
13
+ return (
14
+ <div className="page-content animate-in">
15
+ <div className="panel-header" style={{ marginBottom: '1rem', justifyContent: 'flex-start', gap: '0.75rem' }}>
16
+ <div className="brand-icon" style={{ width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center' }}>
17
+ <Zap size={18} />
18
+ </div>
19
+ <div>
20
+ <h2 style={{ fontSize: '1.25rem' }}>System Overview</h2>
21
+ <p style={{ color: 'var(--text-400)', fontSize: '0.9rem' }}>Real-time performance metrics and system health.</p>
22
+ </div>
23
+ </div>
24
+ <KpiGrid data={data} />
25
+
26
+ <div className="middle-grid">
27
+ <HealthCharts history={history} />
28
+ <CachePanel data={data} />
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,26 @@
1
+ import { RoutesTable } from "../components/RoutesTable";
2
+ import type { MetricsData } from "../hooks/useMetrics";
3
+ import { Route } from "lucide-react";
4
+
5
+ interface RoutesPageProps {
6
+ data: MetricsData;
7
+ }
8
+
9
+ export function RoutesPage({ data }: RoutesPageProps) {
10
+ return (
11
+ <div className="page-content animate-in">
12
+ <div className="panel-header" style={{ marginBottom: '1rem', justifyContent: 'flex-start', gap: '0.75rem' }}>
13
+ <div className="brand-icon" style={{ width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center' }}>
14
+ <Route size={18} />
15
+ </div>
16
+ <div>
17
+ <h2 style={{ fontSize: '1.25rem' }}>API Routes</h2>
18
+ <p style={{ color: 'var(--text-400)', fontSize: '0.9rem' }}>Detailed breakdown of performance per endpoint.</p>
19
+ </div>
20
+ </div>
21
+ <div className="panel">
22
+ <RoutesTable routes={data.routes} />
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,27 @@
1
+ export const fNum = (n: number) => n?.toLocaleString() || '0';
2
+ export function fPct(val: number, total: number): string {
3
+ if (total === 0) return "0";
4
+ return ((val / total) * 100).toFixed(1);
5
+ }
6
+
7
+ export function fBytes(bytes: number): string {
8
+ if (bytes === 0) return "0 B";
9
+ const k = 1024;
10
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
11
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
12
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
13
+ }
14
+
15
+ export const formatUptime = (ms: number) => {
16
+ if (!ms) return "00:00:00";
17
+ const s = Math.floor((ms / 1000) % 60).toString().padStart(2, "0");
18
+ const m = Math.floor((ms / (1000 * 60)) % 60).toString().padStart(2, "0");
19
+ const h = Math.floor(ms / (1000 * 60 * 60)).toString().padStart(2, "0");
20
+ return `${h}:${m}:${s}`;
21
+ };
22
+
23
+ export const fTime = (ts: number) => new Date(ts).toISOString().split("T")[1].slice(0, 12);
24
+
25
+ export const getTimeClass = (ms: number) => ms < 200 ? 'time-fast' : (ms < 1000 ? 'time-med' : 'time-slow');
26
+
27
+ export const getStatusClass = (code: number) => code < 300 ? 's2xx' : (code < 400 ? 's3xx' : (code < 500 ? 's4xx' : 's5xx'));
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ base: './',
7
+ server: {
8
+ proxy: {
9
+ '/api': 'http://localhost:3000/__perf/api',
10
+ },
11
+ },
12
+ });
@@ -0,0 +1,6 @@
1
+ import { Metrics, Insight } from "./types";
2
+ /**
3
+ * Heuristic-based analysis engine that transforms raw metrics into actionable advice.
4
+ */
5
+ export declare function analyzeMetrics(metrics: Metrics): Insight[];
6
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAE3C;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,CAwE1D"}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeMetrics = analyzeMetrics;
4
+ /**
5
+ * Heuristic-based analysis engine that transforms raw metrics into actionable advice.
6
+ */
7
+ function analyzeMetrics(metrics) {
8
+ const insights = [];
9
+ // 1. Caching Suggestions
10
+ Object.entries(metrics.routes).forEach(([path, stats]) => {
11
+ // If a route is slow and has low/zero cache hits
12
+ if (stats.avgTime > 500 && stats.count > 5) {
13
+ if (metrics.cacheHitRate < 10) {
14
+ insights.push({
15
+ type: "warning",
16
+ title: "Slow Route: Caching Opportunity",
17
+ message: `Route "${path}" is slow (avg ${stats.avgTime}ms).`,
18
+ action: "Consider enabling the cache middleware for this route.",
19
+ });
20
+ }
21
+ }
22
+ // 2. N+1 Query Detection
23
+ if (stats.highQueryCount > stats.count * 0.3) {
24
+ insights.push({
25
+ type: "error",
26
+ title: "Potential N+1 Query Detected",
27
+ message: `Route "${path}" triggers high query counts in ${Math.round((stats.highQueryCount / stats.count) * 100)}% of calls.`,
28
+ action: "Review your database logic and use eager loading (JOINs).",
29
+ });
30
+ }
31
+ // 3. Payload Size Alerts
32
+ if (stats.avgSize > 1024 * 1024) {
33
+ // > 1MB
34
+ insights.push({
35
+ type: "warning",
36
+ title: "Heavy Payload Detected",
37
+ message: `Route "${path}" sends large responses (avg ${(stats.avgSize / 1024 / 1024).toFixed(1)} MB).`,
38
+ action: "Consider pagination or reducing the number of returned fields.",
39
+ });
40
+ }
41
+ // 4. Rate Limiting Insights
42
+ if (stats.rateLimitHits > stats.count * 0.1) {
43
+ insights.push({
44
+ type: "info",
45
+ title: "High Rate Limiting Activity",
46
+ message: `Route "${path}" has a high block rate (${Math.round((stats.rateLimitHits / stats.count) * 100)}%).`,
47
+ action: "Verify if your Rate Limit 'max' is too low or if an IP is scraping you.",
48
+ });
49
+ }
50
+ });
51
+ // 5. System Health
52
+ if (metrics.eventLoopLag > 100) {
53
+ insights.push({
54
+ type: "error",
55
+ title: "Event Loop Lagging",
56
+ message: `Critical event loop delay detected (${metrics.eventLoopLag}ms).`,
57
+ action: "Identify blocking synchronous operations in your code.",
58
+ });
59
+ }
60
+ if (metrics.memoryUsage.heapUsed > metrics.memoryUsage.heapLimit * 0.8) {
61
+ insights.push({
62
+ type: "warning",
63
+ title: "High Memory Pressure",
64
+ message: "Node.js heap usage is above 80% of total limit.",
65
+ action: "Monitor for memory leaks or increase memory limit (--max-old-space-size).",
66
+ });
67
+ }
68
+ return insights;
69
+ }
70
+ //# sourceMappingURL=analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":";;AAKA,wCAwEC;AA3ED;;GAEG;AACH,SAAgB,cAAc,CAAC,OAAgB;IAC7C,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,yBAAyB;IACzB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;QACvD,iDAAiD;QACjD,IAAI,KAAK,CAAC,OAAO,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;YAC3C,IAAI,OAAO,CAAC,YAAY,GAAG,EAAE,EAAE,CAAC;gBAC9B,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI,EAAE,SAAS;oBACf,KAAK,EAAE,iCAAiC;oBACxC,OAAO,EAAE,UAAU,IAAI,kBAAkB,KAAK,CAAC,OAAO,MAAM;oBAC5D,MAAM,EAAE,wDAAwD;iBACjE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,yBAAyB;QACzB,IAAI,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC;YAC7C,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,8BAA8B;gBACrC,OAAO,EAAE,UAAU,IAAI,mCAAmC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,aAAa;gBAC7H,MAAM,EAAE,2DAA2D;aACpE,CAAC,CAAC;QACL,CAAC;QAED,yBAAyB;QACzB,IAAI,KAAK,CAAC,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;YAChC,QAAQ;YACR,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,wBAAwB;gBAC/B,OAAO,EAAE,UAAU,IAAI,gCAAgC,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;gBACtG,MAAM,EACJ,gEAAgE;aACnE,CAAC,CAAC;QACL,CAAC;QAED,4BAA4B;QAC5B,IAAI,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC;YAC5C,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,MAAM;gBACZ,KAAK,EAAE,6BAA6B;gBACpC,OAAO,EAAE,UAAU,IAAI,4BAA4B,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,KAAK;gBAC7G,MAAM,EACJ,yEAAyE;aAC5E,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,oBAAoB;YAC3B,OAAO,EAAE,uCAAuC,OAAO,CAAC,YAAY,MAAM;YAC1E,MAAM,EAAE,wDAAwD;SACjE,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,CAAC,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,sBAAsB;YAC7B,OAAO,EAAE,iDAAiD;YAC1D,MAAM,EACJ,2EAA2E;SAC9E,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -1,9 +1,9 @@
1
- import { Router } from 'express';
2
- import { MetricsStore } from '../store';
3
- import { DashboardOptions } from '../types';
1
+ import { Router } from "express";
2
+ import { MetricsStore } from "../store";
3
+ import { DashboardOptions } from "../types";
4
4
  /**
5
5
  * Create the dashboard Express router.
6
6
  * Serves the HTML dashboard and JSON metrics API.
7
7
  */
8
- export declare function createDashboardRouter(store: MetricsStore, _options?: DashboardOptions): Router;
8
+ export declare function createDashboardRouter(store: MetricsStore, options?: DashboardOptions): Router;
9
9
  //# sourceMappingURL=dashboardRouter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboardRouter.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboardRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,QAAQ,GAAE,gBAAqB,GAC9B,MAAM,CA+BR"}
1
+ {"version":3,"file":"dashboardRouter.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboardRouter.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,MAAM,EAAmC,MAAM,SAAS,CAAC;AAE3E,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAU5C;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAyFR"}
@@ -34,38 +34,84 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createDashboardRouter = createDashboardRouter;
37
- const express_1 = require("express");
37
+ const express_1 = __importStar(require("express"));
38
38
  const path = __importStar(require("path"));
39
- const fs = __importStar(require("fs"));
39
+ /**
40
+ * Simple session-less auth token based on the secret.
41
+ * In a real-world scenario, you'd use a more robust session manager or JWT.
42
+ */
43
+ const generateToken = (secret) => {
44
+ return Buffer.from(`auth:${secret}`).toString("base64");
45
+ };
40
46
  /**
41
47
  * Create the dashboard Express router.
42
48
  * Serves the HTML dashboard and JSON metrics API.
43
49
  */
44
- function createDashboardRouter(store, _options = {}) {
50
+ function createDashboardRouter(store, options = {}) {
45
51
  const router = (0, express_1.Router)();
46
- // Cache the HTML file in memory
47
- const htmlPath = path.join(__dirname, 'dashboard.html');
48
- let dashboardHtml;
49
- try {
50
- dashboardHtml = fs.readFileSync(htmlPath, 'utf-8');
51
- }
52
- catch {
53
- dashboardHtml = '<h1>Dashboard HTML not found</h1><p>Ensure dashboard.html is in the dist/dashboard/ directory.</p>';
54
- }
55
- // Serve dashboard HTML
56
- router.get('/', (_req, res) => {
57
- res.set('Content-Type', 'text/html');
58
- res.send(dashboardHtml);
52
+ // Default auth settings if none provided (Security by default)
53
+ const auth = options.auth || {
54
+ username: "admin",
55
+ password: "perf-toolkit",
56
+ secret: "toolkit-secret",
57
+ };
58
+ const mountPath = options.path || "/__perf";
59
+ // Use JSON parsing for login endpoint
60
+ router.use(express_1.default.json());
61
+ // Authentication Middleware
62
+ const requireAuth = (req, res, next) => {
63
+ if (!auth)
64
+ return next();
65
+ const expectedToken = generateToken(auth.secret || "toolkit-secret");
66
+ const cookie = req.headers.cookie || "";
67
+ const hasToken = cookie.includes(`perf-auth=${expectedToken}`);
68
+ if (hasToken) {
69
+ return next();
70
+ }
71
+ res.status(401).json({ success: false, message: "Unauthorized" });
72
+ };
73
+ // Login Endpoint
74
+ router.post("/api/login", (req, res) => {
75
+ if (!auth) {
76
+ return res.json({ success: true, message: "Auth disabled" });
77
+ }
78
+ const { username, password } = req.body;
79
+ const defaultUser = auth.username || "admin";
80
+ const defaultPass = auth.password || "perf-toolkit";
81
+ if (username === defaultUser && password === defaultPass) {
82
+ const token = generateToken(auth.secret || "toolkit-secret");
83
+ // Set cookie with HttpOnly and reasonable maxAge
84
+ res.setHeader("Set-Cookie", `perf-auth=${token}; Path=${mountPath}; HttpOnly; Max-Age=86400; SameSite=Lax`);
85
+ return res.json({ success: true });
86
+ }
87
+ res.status(401).json({ success: false, message: "Invalid credentials" });
88
+ });
89
+ // Logout Endpoint
90
+ router.post("/api/logout", (_req, res) => {
91
+ res.setHeader("Set-Cookie", `perf-auth=; Path=${mountPath}; HttpOnly; Max-Age=0`);
92
+ res.json({ success: true });
93
+ });
94
+ // Check auth status endpoint
95
+ router.get("/api/auth-check", (req, res) => {
96
+ if (!auth)
97
+ return res.json({ authenticated: true, required: false });
98
+ const expectedToken = generateToken(auth.secret || "toolkit-secret");
99
+ const cookie = req.headers.cookie || "";
100
+ const authenticated = cookie.includes(`perf-auth=${expectedToken}`);
101
+ res.json({ authenticated, required: true });
59
102
  });
60
- // JSON metrics API endpoint
61
- router.get('/api/metrics', (_req, res) => {
103
+ // JSON metrics API endpoint (Protected)
104
+ router.get("/api/metrics", requireAuth, (_req, res) => {
62
105
  res.json(store.getMetrics());
63
106
  });
64
- // Reset metrics endpoint
65
- router.post('/api/reset', (_req, res) => {
107
+ // Reset metrics endpoint (Protected)
108
+ router.post("/api/reset", requireAuth, (_req, res) => {
66
109
  store.reset();
67
- res.json({ success: true, message: 'Metrics reset' });
110
+ res.json({ success: true, message: "Metrics reset" });
68
111
  });
112
+ // Serve React Dashboard UI bundle
113
+ const uiPath = path.resolve(__dirname, "../../dist/dashboard-ui");
114
+ router.use("/", express_1.default.static(uiPath));
69
115
  return router;
70
116
  }
71
117
  //# sourceMappingURL=dashboardRouter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboardRouter.js","sourceRoot":"","sources":["../../src/dashboard/dashboardRouter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,sDAkCC;AA5CD,qCAAoD;AACpD,2CAA6B;AAC7B,uCAAyB;AAIzB;;;GAGG;AACH,SAAgB,qBAAqB,CACnC,KAAmB,EACnB,WAA6B,EAAE;IAE/B,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,gCAAgC;IAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IACxD,IAAI,aAAqB,CAAC;IAE1B,IAAI,CAAC;QACH,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,aAAa,GAAG,oGAAoG,CAAC;IACvH,CAAC;IAED,uBAAuB;IACvB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QAC/C,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACrC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,4BAA4B;IAC5B,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QAC1D,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,yBAAyB;IACzB,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QACzD,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"dashboardRouter.js","sourceRoot":"","sources":["../../src/dashboard/dashboardRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ :root{--bg-base:#09090b;--bg-surface:#18181b;--bg-surface-glass:#18181bb3;--bg-hover:#27272a;--border:#27272a;--text-100:#f4f4f5;--text-200:#e4e4e7;--text-300:#a1a1aa;--text-400:#71717a;--accent-cyan:#06b6d4;--accent-emerald:#10b981;--accent-indigo:#6366f1;--accent-rose:#f43f5e;--accent-amber:#f59e0b;--grad-primary:linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo));--grad-success:linear-gradient(135deg, var(--accent-emerald), var(--accent-cyan));--grad-warning:linear-gradient(135deg, var(--accent-amber), var(--accent-rose));--grad-danger:linear-gradient(135deg, var(--accent-rose), #9f1239);--shadow-sm:0 1px 2px #00000080;--shadow-md:0 4px 6px -1px #00000080, 0 2px 4px -1px #0006;--shadow-glow:0 0 20px #06b6d426;--font-sans:"Inter", sans-serif;--font-display:"Outfit", sans-serif;--font-mono:"JetBrains Mono", monospace}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font-sans);background-color:var(--bg-base);color:var(--text-200);background-image:radial-gradient(circle at 15%,#6366f114,#0000 25%),radial-gradient(circle at 85% 30%,#06b6d414,#0000 25%);background-attachment:fixed;flex-direction:column;min-height:100vh;display:flex;overflow-x:hidden}.navbar{z-index:50;background:var(--bg-surface-glass);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);justify-content:space-between;align-items:center;padding:.75rem 2rem;transition:all .3s;display:flex;position:sticky;top:0}.brand{align-items:center;gap:.75rem;display:flex}.brand-icon{background:var(--grad-primary);color:#fff;border-radius:8px;place-items:center;width:32px;height:32px;font-size:18px;animation:3s infinite alternate pulse-glow;display:grid;box-shadow:0 0 15px #6366f166}.brand-title{font-family:var(--font-display);color:var(--text-100);letter-spacing:-.02em;font-size:1.25rem;font-weight:700}.brand-title span{background:var(--grad-primary);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text}.nav-actions{align-items:center;gap:1.5rem;display:flex}.live-indicator{color:var(--text-300);border:1px solid var(--border);background:#ffffff08;border-radius:99px;align-items:center;gap:.5rem;padding:.25rem .75rem;font-size:.875rem;font-weight:500;display:flex}.pulse-dot{background:var(--accent-emerald);width:8px;height:8px;box-shadow:0 0 8px var(--accent-emerald);border-radius:50%;animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes pulse-glow{0%{box-shadow:0 0 10px #6366f133}to{box-shadow:0 0 20px #06b6d480}}.dashboard-wrapper{flex-direction:column;gap:1.5rem;width:100%;max-width:1400px;margin:0 auto;padding:2rem;display:flex}.kpi-grid{grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.25rem;display:grid}.kpi-card{background:var(--bg-surface-glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:16px;flex-direction:column;padding:1.5rem;transition:transform .25s cubic-bezier(.4,0,.2,1),border-color .25s,box-shadow .25s;display:flex;position:relative;overflow:hidden}.kpi-card:hover{box-shadow:var(--shadow-glow);border-color:#6366f166;transform:translateY(-4px)}.kpi-card:before{content:"";background:var(--bg-hover);height:3px;transition:background .3s;position:absolute;top:0;left:0;right:0}.kpi-card.grad-1:hover:before{background:var(--grad-primary)}.kpi-card.grad-2:hover:before{background:var(--grad-success)}.kpi-card.grad-3:hover:before{background:var(--grad-warning)}.kpi-card.grad-4:hover:before{background:var(--grad-danger)}.kpi-card.grad-5:hover:before{background:linear-gradient(135deg,#10b981,#f59e0b)}.kpi-title{color:var(--text-400);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem;font-size:.8125rem;font-weight:600}.kpi-value{font-family:var(--font-display);color:var(--text-100);margin-bottom:.25rem;font-size:2.5rem;font-weight:800;line-height:1.1}.kpi-subtext{color:var(--text-300);font-size:.875rem;font-weight:500}.val-emerald{text-shadow:0 0 15px #10b9814d;color:var(--accent-emerald)!important}.val-rose{text-shadow:0 0 15px #f43f5e4d;color:var(--accent-rose)!important}.val-amber{text-shadow:0 0 15px #f59e0b4d;color:var(--accent-amber)!important}.middle-grid{grid-template-columns:3fr 2fr;gap:1.5rem;display:grid}@media (width<=1024px){.middle-grid{grid-template-columns:1fr}}.panel{background:var(--bg-surface-glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--border);box-shadow:var(--shadow-md);border-radius:16px;flex-direction:column;display:flex;overflow:hidden}.panel-header{border-bottom:1px solid var(--border);background:#ffffff03;justify-content:space-between;align-items:center;padding:1.25rem 1.5rem;display:flex}.panel-title{font-family:var(--font-display);color:var(--text-100);align-items:center;gap:.5rem;font-size:1.125rem;font-weight:600;display:flex}.panel-body{flex:1;padding:1.5rem;overflow-y:auto}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--text-400)}.status-bars{flex-direction:column;gap:1rem;display:flex}.status-row{align-items:center;gap:1rem;display:flex}.status-code{font-family:var(--font-mono);text-align:right;width:40px;font-size:.875rem;font-weight:600}.status-code.s2xx{color:var(--accent-emerald)}.status-code.s3xx{color:var(--accent-cyan)}.status-code.s4xx{color:var(--accent-amber)}.status-code.s5xx{color:var(--accent-rose)}.track{background:#ffffff0d;border-radius:99px;flex:1;height:8px;overflow:hidden}.fill{border-radius:99px;height:100%;transition:width .8s cubic-bezier(.4,0,.2,1)}.fill.s2xx{background:var(--grad-success);box-shadow:0 0 10px #10b98166}.fill.s3xx{background:var(--grad-primary)}.fill.s4xx{background:var(--grad-warning)}.fill.s5xx{background:var(--grad-danger);box-shadow:0 0 10px #f43f5e66}.status-count{font-family:var(--font-mono);color:var(--text-300);width:40px;font-size:.875rem}.cache-viz{justify-content:center;align-items:center;gap:2.5rem;height:100%;display:flex}.donut-wrap{filter:drop-shadow(0 0 10px #06b6d433);width:140px;height:140px;position:relative}.donut-center{flex-direction:column;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.donut-val{font-family:var(--font-display);color:var(--text-100);font-size:1.75rem;font-weight:800}.donut-lbl{color:var(--text-400);text-transform:uppercase;letter-spacing:.05em;font-size:.75rem;font-weight:600}.table-container{width:100%;overflow-x:auto}table{border-collapse:separate;border-spacing:0;width:100%}th{text-align:left;color:var(--text-400);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);background:var(--bg-surface);z-index:10;padding:.75rem 1rem;font-size:.75rem;font-weight:600;position:sticky;top:0}td{border-bottom:1px solid #ffffff08;padding:.875rem 1rem;font-size:.875rem;transition:background .2s}tr:hover td{background:#ffffff05}.routes-table td{font-family:var(--font-mono)}.route-path{color:var(--text-100);font-weight:500}.badge{font-size:.75rem;font-weight:600;font-family:var(--font-mono);letter-spacing:.02em;border-radius:6px;align-items:center;padding:.25rem .6rem;display:inline-flex}.badge-GET{color:var(--accent-emerald);background:#10b9811a;border:1px solid #10b98133}.badge-POST{color:var(--accent-indigo);background:#6366f11a;border:1px solid #6366f133}.badge-PUT,.badge-PATCH{color:var(--accent-amber);background:#f59e0b1a;border:1px solid #f59e0b33}.badge-DELETE{color:var(--accent-rose);background:#f43f5e1a;border:1px solid #f43f5e33}.time-fast{color:var(--accent-emerald)}.time-med{color:var(--accent-amber)}.time-slow{color:var(--accent-rose);text-shadow:0 0 10px #f43f5e66;font-weight:700}.cache-hit{color:var(--accent-cyan);background:#06b6d41a;border:1px solid #06b6d433}.cache-miss{color:var(--text-400);background:#71717a1a;border:1px solid #71717a33}.fire-icon{color:var(--accent-rose);animation:2s infinite alternate flicker}@keyframes flicker{0%,to{opacity:1;filter:drop-shadow(0 0 4px var(--accent-rose))}50%{opacity:.7;filter:drop-shadow(0 0 2px var(--accent-rose))}}.filters{gap:.5rem;display:flex}.filter-btn{border:1px solid var(--border);color:var(--text-300);cursor:pointer;background:0 0;border-radius:8px;padding:.4rem .8rem;font-size:.8125rem;font-weight:500;transition:all .2s}.filter-btn:hover{background:var(--bg-hover);color:var(--text-100)}.filter-btn.active{color:var(--accent-indigo);border-color:var(--accent-indigo);background:#6366f11a;box-shadow:0 0 10px #6366f133}.empty-state{text-align:center;color:var(--text-400);padding:3rem 1rem}.empty-icon{opacity:.5;margin-bottom:1rem;font-size:2rem}.animate-in{opacity:0;animation:.5s cubic-bezier(.16,1,.3,1) forwards slideUp;transform:translateY(10px)}.delay-1{animation-delay:50ms}.delay-2{animation-delay:.1s}.delay-3{animation-delay:.15s}.delay-4{animation-delay:.2s}.delay-5{animation-delay:.25s}@keyframes slideUp{to{opacity:1;transform:translateY(0)}}.modal-overlay{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:1000;background:#000c;place-items:center;padding:2rem;display:grid;position:fixed;inset:0}.modal-content{background:var(--bg-surface);border:1px solid var(--border);width:100%;max-width:800px;max-height:80vh;box-shadow:0 25px 50px -12px #00000080, var(--shadow-glow);border-radius:20px;flex-direction:column;display:flex}.modal-header{border-bottom:1px solid var(--border);justify-content:space-between;align-items:center;padding:1.5rem;display:flex}.modal-title{font-family:var(--font-display);color:var(--text-100);align-items:center;font-size:1.25rem;font-weight:700;display:flex}.modal-close{color:var(--text-400);cursor:pointer;background:0 0;border:none;border-radius:8px;padding:.5rem;transition:all .2s}.modal-close:hover{background:var(--bg-hover);color:var(--text-100)}.modal-body{padding:1.5rem;overflow-y:auto}.modal-footer{border-top:1px solid var(--border);background:#0003;padding:1.25rem 1.5rem}.blocked-list table{width:100%}.method-badge{text-transform:uppercase;background:#ffffff1a;border-radius:4px;padding:2px 6px;font-size:.7rem;font-weight:700}.method-badge.get{color:var(--accent-emerald);background:#10b9811a}.method-badge.post{color:var(--accent-indigo);background:#6366f11a}.method-badge.put{color:var(--accent-amber);background:#f59e0b1a}.method-badge.delete{color:var(--accent-rose);background:#f43f5e1a}.insights-list{flex-direction:column;display:flex}.insight-item{border-bottom:1px solid var(--border);gap:16px;padding:16px;display:flex}.insight-item:last-child{border-bottom:none}.insight-icon{flex-shrink:0;padding-top:2px}.insight-content{flex-grow:1}.insight-title{margin-bottom:4px;font-size:.95rem;font-weight:600}.insight-message{color:var(--text-300);font-size:.85rem}.insight-action{color:var(--text-200);border:1px solid var(--border);background:#ffffff0d;border-radius:4px;align-items:center;margin-top:10px;padding:4px 10px;font-size:.8rem;display:inline-flex}.type-error .insight-title{color:var(--accent-rose)}.type-warning .insight-title{color:var(--accent-amber)}.type-info .insight-title{color:var(--accent-emerald)}.type-error{background:#f43f5e05}.type-warning{background:#f59e0b05}.type-info{background:#10b98105}.login-wrapper{background:radial-gradient(circle at 0 0,#10b9810d 0%,#0000 40%),radial-gradient(circle at 100% 100%,#f43f5e0d 0%,#0000 40%);justify-content:center;align-items:center;min-height:100vh;padding:20px;display:flex}.login-card{text-align:center;width:100%;max-width:400px;padding:3rem}.login-header h1{font-family:var(--font-display);color:var(--text-100);margin-bottom:.5rem;font-size:1.5rem}.login-header p{color:var(--text-400);margin-bottom:2rem;font-size:.9rem}.login-form{flex-direction:column;gap:1rem;display:flex}.input-group{align-items:center;display:flex;position:relative}.input-icon{color:var(--text-400);position:absolute;left:12px}.login-form input{border:1px solid var(--border);width:100%;color:var(--text-100);background:#ffffff08;border-radius:8px;padding:12px 12px 12px 40px;font-size:.9rem;transition:all .2s}.login-form input:focus{border-color:var(--accent-emerald);background:#ffffff0d;outline:none;box-shadow:0 0 0 4px #10b9811a}.login-button{background:var(--accent-emerald);color:#fff;cursor:pointer;border:none;border-radius:8px;justify-content:center;align-items:center;width:100%;margin-top:1rem;padding:12px;font-size:.95rem;font-weight:600;transition:all .2s;display:flex}.login-button:hover:not(:disabled){background:#10b981;transform:translateY(-1px);box-shadow:0 4px 12px #10b98133}.login-button:active:not(:disabled){transform:translateY(0)}.login-button:disabled{opacity:.6;cursor:not-allowed}.login-error{color:var(--accent-rose);background:#f43f5e1a;border:1px solid #f43f5e33;border-radius:6px;padding:10px;font-size:.85rem}.nav-links{gap:.5rem;margin:0 1.5rem;display:flex}.nav-link{color:var(--text-400);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:.6rem;padding:.5rem .85rem;font-size:.85rem;font-weight:500;transition:all .2s;display:flex;position:relative}.nav-link:hover{color:var(--text-100);background:#ffffff0d}.nav-link.active{color:var(--accent-emerald);background:#10b9811a}.nav-link .badge{background:var(--accent-rose);color:#fff;text-align:center;border-radius:10px;min-width:16px;height:16px;padding:0 5px;font-size:.65rem;font-weight:700;line-height:16px;position:absolute;top:2px;right:-4px;box-shadow:0 2px 4px #f43f5e4d}.page-content{flex-direction:column;gap:1.25rem;display:flex}.centered{justify-content:center;align-items:center;min-height:calc(100vh - 160px);display:flex}.error-state{text-align:center;max-width:500px;padding:3rem}.uptime-mono,.lag-mono{font-family:var(--font-mono);margin-left:6px;font-weight:600}@media (width<=1024px){.nav-link span{display:none}.nav-links{gap:.25rem;margin:0 .5rem}}@media (width<=768px){.nav-links{display:none}}