@rulebricks/cli 2.0.0 → 2.0.2

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 (38) hide show
  1. package/README.md +3 -3
  2. package/benchmarks/README.md +98 -0
  3. package/benchmarks/Test Flow.rbf +4088 -0
  4. package/benchmarks/benchmark-flow.json +26 -0
  5. package/benchmarks/lib/payload.js +101 -0
  6. package/benchmarks/lib/report.js +929 -0
  7. package/benchmarks/qps-test.js +136 -0
  8. package/benchmarks/run-qps-test.sh +115 -0
  9. package/benchmarks/run-throughput-test.sh +123 -0
  10. package/benchmarks/throughput-report.html +632 -0
  11. package/benchmarks/throughput-results.json +298 -0
  12. package/benchmarks/throughput-test.js +159 -0
  13. package/dist/commands/benchmark.d.ts +11 -0
  14. package/dist/commands/benchmark.js +173 -0
  15. package/dist/commands/deploy.js +15 -4
  16. package/dist/commands/destroy.js +2 -2
  17. package/dist/commands/logs.js +1 -0
  18. package/dist/components/Wizard/steps/BenchmarkSteps.d.ts +31 -0
  19. package/dist/components/Wizard/steps/BenchmarkSteps.js +304 -0
  20. package/dist/components/Wizard/steps/DatabaseStep.js +49 -35
  21. package/dist/index.js +42 -6
  22. package/dist/lib/benchmark.d.ts +63 -0
  23. package/dist/lib/benchmark.js +466 -0
  24. package/dist/lib/dns.d.ts +3 -1
  25. package/dist/lib/dns.js +138 -56
  26. package/dist/lib/helm.d.ts +14 -1
  27. package/dist/lib/helm.js +36 -1
  28. package/dist/lib/kubernetes.js +2 -0
  29. package/dist/types/index.d.ts +90 -0
  30. package/dist/types/index.js +51 -0
  31. package/package.json +8 -6
  32. package/terraform/aws/main.tf +22 -0
  33. package/terraform/azure/main.tf +45 -0
  34. package/terraform/gcp/main.tf +34 -0
  35. /package/{email-templates → templates}/email_change.html +0 -0
  36. /package/{email-templates → templates}/invite.html +0 -0
  37. /package/{email-templates → templates}/password_change.html +0 -0
  38. /package/{email-templates → templates}/verify.html +0 -0
@@ -0,0 +1,929 @@
1
+ /**
2
+ * HTML Report Generation for Rulebricks Benchmarking
3
+ *
4
+ * Generates standalone HTML reports with Chart.js visualizations
5
+ */
6
+
7
+ /**
8
+ * Format a number with appropriate precision
9
+ */
10
+ function formatNumber(num, decimals = 2) {
11
+ if (num === null || num === undefined || isNaN(num)) return "N/A";
12
+ return num.toFixed(decimals);
13
+ }
14
+
15
+ /**
16
+ * Format bytes to human readable string
17
+ */
18
+ function formatBytes(bytes) {
19
+ if (bytes === 0) return "0 B";
20
+ const k = 1024;
21
+ const sizes = ["B", "KB", "MB", "GB"];
22
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
23
+ return formatNumber(bytes / Math.pow(k, i), 2) + " " + sizes[i];
24
+ }
25
+
26
+ /**
27
+ * Format duration in milliseconds to human readable string
28
+ */
29
+ function formatDuration(ms) {
30
+ if (ms < 1000) return formatNumber(ms, 2) + " ms";
31
+ if (ms < 60000) return formatNumber(ms / 1000, 2) + " s";
32
+ return formatNumber(ms / 60000, 2) + " min";
33
+ }
34
+
35
+ /**
36
+ * Get status color based on value and thresholds (using neon/pastel palette)
37
+ */
38
+ function getStatusColor(
39
+ value,
40
+ goodThreshold,
41
+ warningThreshold,
42
+ inverse = false
43
+ ) {
44
+ if (inverse) {
45
+ if (value <= goodThreshold) return "#4ade80"; // neon green
46
+ if (value <= warningThreshold) return "#fbbf24"; // neon yellow
47
+ return "#f87171"; // neon red/coral
48
+ } else {
49
+ if (value >= goodThreshold) return "#4ade80"; // neon green
50
+ if (value >= warningThreshold) return "#fbbf24"; // neon yellow
51
+ return "#f87171"; // neon red/coral
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Generate HTML report for QPS test
57
+ */
58
+ export function generateQpsReport(data, config) {
59
+ const metrics = data.metrics || {};
60
+
61
+ // Extract key metrics
62
+ const totalRequests = metrics.http_reqs?.values?.count || 0;
63
+ const testDuration = (data.state?.testRunDurationMs || 0) / 1000;
64
+ const actualRps = testDuration > 0 ? totalRequests / testDuration : 0;
65
+ const rpsEfficiency =
66
+ config.targetRps > 0 ? (actualRps / config.targetRps) * 100 : 0;
67
+
68
+ const successRate = (metrics.successes?.values?.rate || 0) * 100;
69
+ const errorRate = (metrics.errors?.values?.rate || 0) * 100;
70
+ const failedRequests =
71
+ metrics.dropped_requests?.values?.count ||
72
+ Math.round(totalRequests * (errorRate / 100));
73
+
74
+ const p50 = metrics.http_req_duration?.values?.med || 0;
75
+ const p90 = metrics.http_req_duration?.values?.["p(90)"] || 0;
76
+ const p95 = metrics.http_req_duration?.values?.["p(95)"] || 0;
77
+ const p99 = metrics.http_req_duration?.values?.["p(99)"] || 0;
78
+ const avgLatency = metrics.http_req_duration?.values?.avg || 0;
79
+ const minLatency = metrics.http_req_duration?.values?.min || 0;
80
+ const maxLatency = metrics.http_req_duration?.values?.max || 0;
81
+
82
+ // Connection metrics
83
+ const avgConnecting = metrics.http_req_connecting?.values?.avg || 0;
84
+ const avgTlsHandshake = metrics.http_req_tls_handshaking?.values?.avg || 0;
85
+ const avgWaiting = metrics.http_req_waiting?.values?.avg || 0;
86
+ const avgReceiving = metrics.http_req_receiving?.values?.avg || 0;
87
+ const avgSending = metrics.http_req_sending?.values?.avg || 0;
88
+
89
+ const dataReceived = metrics.data_received?.values?.count || 0;
90
+ const dataSent = metrics.data_sent?.values?.count || 0;
91
+ const avgRequestSize = totalRequests > 0 ? dataSent / totalRequests : 0;
92
+ const avgResponseSize = totalRequests > 0 ? dataReceived / totalRequests : 0;
93
+
94
+ // VU metrics
95
+ const maxVUs = metrics.vus_max?.values?.max || metrics.vus?.values?.max || 0;
96
+
97
+ const successColor = getStatusColor(successRate, 99, 95);
98
+ const p95Color = getStatusColor(p95, 200, 500, true);
99
+
100
+ return generateHtmlTemplate({
101
+ title: "QPS Benchmark Report",
102
+ testType: "QPS (Requests/Second)",
103
+ description: "Measures API responsiveness with individual payload requests",
104
+ config,
105
+ metrics: {
106
+ totalRequests,
107
+ testDuration,
108
+ actualRps,
109
+ rpsEfficiency,
110
+ successRate,
111
+ errorRate,
112
+ failedRequests,
113
+ p50,
114
+ p90,
115
+ p95,
116
+ p99,
117
+ avgLatency,
118
+ minLatency,
119
+ maxLatency,
120
+ avgConnecting,
121
+ avgTlsHandshake,
122
+ avgWaiting,
123
+ avgReceiving,
124
+ avgSending,
125
+ dataReceived,
126
+ dataSent,
127
+ avgRequestSize,
128
+ avgResponseSize,
129
+ maxVUs,
130
+ },
131
+ successColor,
132
+ p95Color,
133
+ showBulkMetrics: false,
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Generate HTML report for Throughput test
139
+ */
140
+ export function generateThroughputReport(data, config) {
141
+ const metrics = data.metrics || {};
142
+
143
+ // Extract key metrics
144
+ const totalRequests = metrics.http_reqs?.values?.count || 0;
145
+ const testDuration = (data.state?.testRunDurationMs || 0) / 1000;
146
+ const actualRps = testDuration > 0 ? totalRequests / testDuration : 0;
147
+ const actualThroughput = actualRps * config.bulkSize;
148
+ const rpsEfficiency =
149
+ config.targetRps > 0 ? (actualRps / config.targetRps) * 100 : 0;
150
+
151
+ const successRate = (metrics.successes?.values?.rate || 0) * 100;
152
+ const errorRate = (metrics.errors?.values?.rate || 0) * 100;
153
+ const failedRequests =
154
+ metrics.dropped_requests?.values?.count ||
155
+ Math.round(totalRequests * (errorRate / 100));
156
+
157
+ const totalPayloads =
158
+ metrics.total_payloads?.values?.count || totalRequests * config.bulkSize;
159
+ const failedPayloads = metrics.failed_payloads?.values?.count || 0;
160
+ const successfulPayloads = totalPayloads - failedPayloads;
161
+
162
+ const p50 = metrics.http_req_duration?.values?.med || 0;
163
+ const p90 = metrics.http_req_duration?.values?.["p(90)"] || 0;
164
+ const p95 = metrics.http_req_duration?.values?.["p(95)"] || 0;
165
+ const p99 = metrics.http_req_duration?.values?.["p(99)"] || 0;
166
+ const avgLatency = metrics.http_req_duration?.values?.avg || 0;
167
+ const minLatency = metrics.http_req_duration?.values?.min || 0;
168
+ const maxLatency = metrics.http_req_duration?.values?.max || 0;
169
+
170
+ // Connection metrics
171
+ const avgConnecting = metrics.http_req_connecting?.values?.avg || 0;
172
+ const avgTlsHandshake = metrics.http_req_tls_handshaking?.values?.avg || 0;
173
+ const avgWaiting = metrics.http_req_waiting?.values?.avg || 0;
174
+ const avgReceiving = metrics.http_req_receiving?.values?.avg || 0;
175
+ const avgSending = metrics.http_req_sending?.values?.avg || 0;
176
+
177
+ const dataReceived = metrics.data_received?.values?.count || 0;
178
+ const dataSent = metrics.data_sent?.values?.count || 0;
179
+ const avgRequestSize = totalRequests > 0 ? dataSent / totalRequests : 0;
180
+ const avgResponseSize = totalRequests > 0 ? dataReceived / totalRequests : 0;
181
+
182
+ // VU metrics
183
+ const maxVUs = metrics.vus_max?.values?.max || metrics.vus?.values?.max || 0;
184
+
185
+ const successColor = getStatusColor(successRate, 99, 95);
186
+ const p95Color = getStatusColor(p95, 500, 1000, true);
187
+
188
+ return generateHtmlTemplate({
189
+ title: "Throughput Benchmark Report",
190
+ testType: "Throughput (Solutions/Second)",
191
+ description: "Measures rule engine capacity with bulk payload requests",
192
+ config,
193
+ metrics: {
194
+ totalRequests,
195
+ testDuration,
196
+ actualRps,
197
+ actualThroughput,
198
+ rpsEfficiency,
199
+ successRate,
200
+ errorRate,
201
+ failedRequests,
202
+ totalPayloads,
203
+ successfulPayloads,
204
+ failedPayloads,
205
+ p50,
206
+ p90,
207
+ p95,
208
+ p99,
209
+ avgLatency,
210
+ minLatency,
211
+ maxLatency,
212
+ avgConnecting,
213
+ avgTlsHandshake,
214
+ avgWaiting,
215
+ avgReceiving,
216
+ avgSending,
217
+ dataReceived,
218
+ dataSent,
219
+ avgRequestSize,
220
+ avgResponseSize,
221
+ maxVUs,
222
+ },
223
+ successColor,
224
+ p95Color,
225
+ showBulkMetrics: true,
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Generate the HTML template
231
+ */
232
+ function generateHtmlTemplate({
233
+ title,
234
+ testType,
235
+ description,
236
+ config,
237
+ metrics,
238
+ successColor,
239
+ p95Color,
240
+ showBulkMetrics,
241
+ }) {
242
+ const timestamp = new Date().toISOString();
243
+ const formattedDate = new Date().toLocaleString();
244
+
245
+ return `<!DOCTYPE html>
246
+ <html lang="en">
247
+ <head>
248
+ <meta charset="UTF-8">
249
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
250
+ <title>${title} - Rulebricks</title>
251
+ <link rel="preconnect" href="https://fonts.googleapis.com">
252
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
253
+ <link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
254
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
255
+ <style>
256
+ :root {
257
+ --bg-primary: #0a0a0a;
258
+ --bg-secondary: #141414;
259
+ --bg-tertiary: #1f1f1f;
260
+ --bg-hover: #2a2a2a;
261
+ --text-primary: #fafafa;
262
+ --text-secondary: #a1a1a1;
263
+ --text-muted: #6b6b6b;
264
+ --border: #2a2a2a;
265
+ --border-light: #333;
266
+ --accent: #a78bfa;
267
+ --accent-cyan: #22d3ee;
268
+ --accent-green: #4ade80;
269
+ --accent-yellow: #fbbf24;
270
+ --accent-red: #f87171;
271
+ --accent-pink: #f472b6;
272
+ }
273
+
274
+ * {
275
+ margin: 0;
276
+ padding: 0;
277
+ box-sizing: border-box;
278
+ }
279
+
280
+ body {
281
+ font-family: 'Archivo', -apple-system, BlinkMacSystemFont, sans-serif;
282
+ background: var(--bg-primary);
283
+ color: var(--text-primary);
284
+ line-height: 1.6;
285
+ min-height: 100vh;
286
+ }
287
+
288
+ .container {
289
+ max-width: 1280px;
290
+ margin: 0 auto;
291
+ padding: 2.5rem;
292
+ }
293
+
294
+ header {
295
+ margin-bottom: 2.5rem;
296
+ padding-bottom: 2rem;
297
+ border-bottom: 1px solid var(--border);
298
+ }
299
+
300
+ .header-top {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ margin-bottom: 1.5rem;
305
+ }
306
+
307
+ .logo {
308
+ font-size: 0.75rem;
309
+ font-weight: 600;
310
+ letter-spacing: 0.15em;
311
+ text-transform: uppercase;
312
+ color: var(--text-muted);
313
+ }
314
+
315
+ .timestamp {
316
+ font-size: 0.8rem;
317
+ color: var(--text-muted);
318
+ font-family: 'JetBrains Mono', monospace;
319
+ }
320
+
321
+ h1 {
322
+ font-size: 2rem;
323
+ font-weight: 700;
324
+ margin-bottom: 0.5rem;
325
+ letter-spacing: -0.02em;
326
+ }
327
+
328
+ .subtitle {
329
+ color: var(--text-secondary);
330
+ font-size: 1rem;
331
+ }
332
+
333
+ .grid {
334
+ display: grid;
335
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
336
+ gap: 1rem;
337
+ margin-bottom: 1.5rem;
338
+ }
339
+
340
+ .grid-6 {
341
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
342
+ }
343
+
344
+ .card {
345
+ background: var(--bg-secondary);
346
+ border-radius: 4px;
347
+ padding: 1.25rem;
348
+ border: 1px solid var(--border);
349
+ transition: border-color 0.15s ease;
350
+ }
351
+
352
+ .card:hover {
353
+ border-color: var(--border-light);
354
+ }
355
+
356
+ .card-header {
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: space-between;
360
+ margin-bottom: 0.75rem;
361
+ }
362
+
363
+ .card-title {
364
+ font-size: 0.7rem;
365
+ font-weight: 600;
366
+ text-transform: uppercase;
367
+ letter-spacing: 0.08em;
368
+ color: var(--text-muted);
369
+ }
370
+
371
+ .card-value {
372
+ font-size: 2rem;
373
+ font-weight: 700;
374
+ line-height: 1.1;
375
+ letter-spacing: -0.02em;
376
+ }
377
+
378
+ .card-value-sm {
379
+ font-size: 1.5rem;
380
+ }
381
+
382
+ .card-subtitle {
383
+ color: var(--text-muted);
384
+ font-size: 0.8rem;
385
+ margin-top: 0.5rem;
386
+ }
387
+
388
+ .status-indicator {
389
+ width: 8px;
390
+ height: 8px;
391
+ border-radius: 2px;
392
+ display: inline-block;
393
+ }
394
+
395
+ .section {
396
+ margin-bottom: 1.5rem;
397
+ }
398
+
399
+ .section-title {
400
+ font-size: 0.7rem;
401
+ font-weight: 600;
402
+ text-transform: uppercase;
403
+ letter-spacing: 0.1em;
404
+ color: var(--text-muted);
405
+ margin-bottom: 1rem;
406
+ padding-bottom: 0.5rem;
407
+ border-bottom: 1px solid var(--border);
408
+ }
409
+
410
+ .chart-container {
411
+ background: var(--bg-secondary);
412
+ border-radius: 4px;
413
+ padding: 1.5rem;
414
+ border: 1px solid var(--border);
415
+ margin-bottom: 1.5rem;
416
+ }
417
+
418
+ .chart-header {
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: space-between;
422
+ margin-bottom: 1rem;
423
+ }
424
+
425
+ .chart-title {
426
+ font-size: 0.9rem;
427
+ font-weight: 600;
428
+ }
429
+
430
+ .chart-wrapper {
431
+ position: relative;
432
+ height: 280px;
433
+ }
434
+
435
+ .two-col {
436
+ display: grid;
437
+ grid-template-columns: 1fr 1fr;
438
+ gap: 1.5rem;
439
+ }
440
+
441
+ @media (max-width: 768px) {
442
+ .two-col {
443
+ grid-template-columns: 1fr;
444
+ }
445
+ }
446
+
447
+ .metrics-table {
448
+ width: 100%;
449
+ border-collapse: collapse;
450
+ font-size: 0.85rem;
451
+ }
452
+
453
+ .metrics-table th,
454
+ .metrics-table td {
455
+ padding: 0.75rem 1rem;
456
+ text-align: left;
457
+ border-bottom: 1px solid var(--border);
458
+ }
459
+
460
+ .metrics-table th {
461
+ color: var(--text-muted);
462
+ font-weight: 500;
463
+ font-size: 0.7rem;
464
+ text-transform: uppercase;
465
+ letter-spacing: 0.08em;
466
+ }
467
+
468
+ .metrics-table td {
469
+ font-family: 'JetBrains Mono', monospace;
470
+ font-size: 0.8rem;
471
+ }
472
+
473
+ .metrics-table tr:last-child td {
474
+ border-bottom: none;
475
+ }
476
+
477
+ .config-grid {
478
+ display: grid;
479
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
480
+ gap: 0.75rem;
481
+ }
482
+
483
+ .config-item {
484
+ display: flex;
485
+ flex-direction: column;
486
+ gap: 0.25rem;
487
+ padding: 0.75rem;
488
+ background: var(--bg-tertiary);
489
+ border-radius: 3px;
490
+ }
491
+
492
+ .config-key {
493
+ font-size: 0.7rem;
494
+ font-weight: 500;
495
+ text-transform: uppercase;
496
+ letter-spacing: 0.08em;
497
+ color: var(--text-muted);
498
+ }
499
+
500
+ .config-value {
501
+ font-family: 'JetBrains Mono', monospace;
502
+ font-size: 0.8rem;
503
+ color: var(--accent-cyan);
504
+ word-break: break-all;
505
+ }
506
+
507
+ footer {
508
+ text-align: center;
509
+ padding-top: 2rem;
510
+ margin-top: 1rem;
511
+ border-top: 1px solid var(--border);
512
+ color: var(--text-muted);
513
+ font-size: 0.75rem;
514
+ }
515
+
516
+ .tag {
517
+ display: inline-block;
518
+ padding: 0.2rem 0.5rem;
519
+ border-radius: 2px;
520
+ font-size: 0.65rem;
521
+ font-weight: 600;
522
+ text-transform: uppercase;
523
+ letter-spacing: 0.05em;
524
+ }
525
+
526
+ .tag-success { background: rgba(74, 222, 128, 0.15); color: var(--accent-green); }
527
+ .tag-warning { background: rgba(251, 191, 36, 0.15); color: var(--accent-yellow); }
528
+ .tag-error { background: rgba(248, 113, 113, 0.15); color: var(--accent-red); }
529
+
530
+ .highlight { color: var(--accent-cyan); }
531
+ .highlight-pink { color: var(--accent-pink); }
532
+ .highlight-purple { color: var(--accent); }
533
+ </style>
534
+ </head>
535
+ <body>
536
+ <div class="container">
537
+ <header>
538
+ <div class="header-top">
539
+ <div class="logo">Rulebricks Benchmark</div>
540
+ <div class="timestamp">${formattedDate}</div>
541
+ </div>
542
+ <h1>${title}</h1>
543
+ <p class="subtitle">${testType} · ${description}</p>
544
+ </header>
545
+
546
+ <div class="section">
547
+ <div class="section-title">Performance Overview</div>
548
+ <div class="grid">
549
+ <div class="card">
550
+ <div class="card-header">
551
+ <span class="card-title">Total Requests</span>
552
+ </div>
553
+ <div class="card-value">${metrics.totalRequests.toLocaleString()}</div>
554
+ <div class="card-subtitle">Over ${formatDuration(
555
+ metrics.testDuration * 1000
556
+ )}</div>
557
+ </div>
558
+
559
+ <div class="card">
560
+ <div class="card-header">
561
+ <span class="card-title">Success Rate</span>
562
+ <span class="status-indicator" style="background: ${successColor}"></span>
563
+ </div>
564
+ <div class="card-value" style="color: ${successColor}">${formatNumber(
565
+ metrics.successRate,
566
+ 1
567
+ )}%</div>
568
+ <div class="card-subtitle">${metrics.failedRequests.toLocaleString()} failed</div>
569
+ </div>
570
+
571
+ <div class="card">
572
+ <div class="card-header">
573
+ <span class="card-title">Actual RPS</span>
574
+ </div>
575
+ <div class="card-value"><span class="highlight">${formatNumber(
576
+ metrics.actualRps,
577
+ 1
578
+ )}</span></div>
579
+ <div class="card-subtitle">${formatNumber(
580
+ metrics.rpsEfficiency,
581
+ 0
582
+ )}% of target (${config.targetRps})</div>
583
+ </div>
584
+
585
+ <div class="card">
586
+ <div class="card-header">
587
+ <span class="card-title">P95 Latency</span>
588
+ <span class="status-indicator" style="background: ${p95Color}"></span>
589
+ </div>
590
+ <div class="card-value" style="color: ${p95Color}">${formatNumber(
591
+ metrics.p95,
592
+ 0
593
+ )}<span style="font-size: 1rem; opacity: 0.7">ms</span></div>
594
+ <div class="card-subtitle">P99: ${formatNumber(
595
+ metrics.p99,
596
+ 0
597
+ )}ms</div>
598
+ </div>
599
+
600
+ ${
601
+ showBulkMetrics
602
+ ? `
603
+ <div class="card">
604
+ <div class="card-header">
605
+ <span class="card-title">Throughput</span>
606
+ </div>
607
+ <div class="card-value"><span class="highlight-pink">${formatNumber(
608
+ metrics.actualThroughput,
609
+ 0
610
+ )}</span></div>
611
+ <div class="card-subtitle">Solutions/sec (${
612
+ config.bulkSize
613
+ }/req)</div>
614
+ </div>
615
+
616
+ <div class="card">
617
+ <div class="card-header">
618
+ <span class="card-title">Total Payloads</span>
619
+ </div>
620
+ <div class="card-value">${metrics.totalPayloads.toLocaleString()}</div>
621
+ <div class="card-subtitle">${metrics.successfulPayloads.toLocaleString()} processed</div>
622
+ </div>
623
+ `
624
+ : `
625
+ <div class="card">
626
+ <div class="card-header">
627
+ <span class="card-title">Peak VUs</span>
628
+ </div>
629
+ <div class="card-value">${metrics.maxVUs}</div>
630
+ <div class="card-subtitle">Virtual users</div>
631
+ </div>
632
+ `
633
+ }
634
+ </div>
635
+ </div>
636
+
637
+ <div class="two-col">
638
+ <div class="chart-container">
639
+ <div class="chart-header">
640
+ <h3 class="chart-title">Response Time Distribution</h3>
641
+ </div>
642
+ <div class="chart-wrapper">
643
+ <canvas id="latencyChart"></canvas>
644
+ </div>
645
+ </div>
646
+
647
+ <div class="chart-container">
648
+ <div class="chart-header">
649
+ <h3 class="chart-title">Request Timing Breakdown</h3>
650
+ </div>
651
+ <div class="chart-wrapper">
652
+ <canvas id="timingChart"></canvas>
653
+ </div>
654
+ </div>
655
+ </div>
656
+
657
+ <div class="section">
658
+ <div class="section-title">Detailed Metrics</div>
659
+ <div class="two-col">
660
+ <div class="card">
661
+ <table class="metrics-table">
662
+ <thead>
663
+ <tr>
664
+ <th>Latency Metric</th>
665
+ <th>Value</th>
666
+ </tr>
667
+ </thead>
668
+ <tbody>
669
+ <tr>
670
+ <td>Minimum</td>
671
+ <td>${formatNumber(metrics.minLatency, 2)} ms</td>
672
+ </tr>
673
+ <tr>
674
+ <td>Average</td>
675
+ <td>${formatNumber(metrics.avgLatency, 2)} ms</td>
676
+ </tr>
677
+ <tr>
678
+ <td>Median (P50)</td>
679
+ <td>${formatNumber(metrics.p50, 2)} ms</td>
680
+ </tr>
681
+ <tr>
682
+ <td>P90</td>
683
+ <td>${formatNumber(metrics.p90, 2)} ms</td>
684
+ </tr>
685
+ <tr>
686
+ <td>P95</td>
687
+ <td>${formatNumber(metrics.p95, 2)} ms</td>
688
+ </tr>
689
+ <tr>
690
+ <td>P99</td>
691
+ <td>${formatNumber(metrics.p99, 2)} ms</td>
692
+ </tr>
693
+ <tr>
694
+ <td>Maximum</td>
695
+ <td>${formatNumber(metrics.maxLatency, 2)} ms</td>
696
+ </tr>
697
+ </tbody>
698
+ </table>
699
+ </div>
700
+
701
+ <div class="card">
702
+ <table class="metrics-table">
703
+ <thead>
704
+ <tr>
705
+ <th>Transfer Metric</th>
706
+ <th>Value</th>
707
+ </tr>
708
+ </thead>
709
+ <tbody>
710
+ <tr>
711
+ <td>Data Sent</td>
712
+ <td>${formatBytes(metrics.dataSent)}</td>
713
+ </tr>
714
+ <tr>
715
+ <td>Data Received</td>
716
+ <td>${formatBytes(metrics.dataReceived)}</td>
717
+ </tr>
718
+ <tr>
719
+ <td>Avg Request Size</td>
720
+ <td>${formatBytes(metrics.avgRequestSize)}</td>
721
+ </tr>
722
+ <tr>
723
+ <td>Avg Response Size</td>
724
+ <td>${formatBytes(metrics.avgResponseSize)}</td>
725
+ </tr>
726
+ <tr>
727
+ <td>Avg Connecting</td>
728
+ <td>${formatNumber(metrics.avgConnecting, 2)} ms</td>
729
+ </tr>
730
+ <tr>
731
+ <td>Avg TLS Handshake</td>
732
+ <td>${formatNumber(metrics.avgTlsHandshake, 2)} ms</td>
733
+ </tr>
734
+ <tr>
735
+ <td>Avg Waiting (TTFB)</td>
736
+ <td>${formatNumber(metrics.avgWaiting, 2)} ms</td>
737
+ </tr>
738
+ </tbody>
739
+ </table>
740
+ </div>
741
+ </div>
742
+ </div>
743
+
744
+ <div class="section">
745
+ <div class="section-title">Test Configuration</div>
746
+ <div class="config-grid">
747
+ <div class="config-item">
748
+ <span class="config-key">API URL</span>
749
+ <span class="config-value">${config.apiUrl}</span>
750
+ </div>
751
+ <div class="config-item">
752
+ <span class="config-key">Test Duration</span>
753
+ <span class="config-value">${config.testDuration}</span>
754
+ </div>
755
+ <div class="config-item">
756
+ <span class="config-key">Target RPS</span>
757
+ <span class="config-value">${config.targetRps}</span>
758
+ </div>
759
+ ${
760
+ showBulkMetrics
761
+ ? `
762
+ <div class="config-item">
763
+ <span class="config-key">Bulk Size</span>
764
+ <span class="config-value">${config.bulkSize} payloads</span>
765
+ </div>
766
+ `
767
+ : ""
768
+ }
769
+ <div class="config-item">
770
+ <span class="config-key">Peak Virtual Users</span>
771
+ <span class="config-value">${metrics.maxVUs}</span>
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
+ <footer>
777
+ <p>Generated by Rulebricks Benchmarking Toolkit</p>
778
+ </footer>
779
+ </div>
780
+
781
+ <script>
782
+ Chart.defaults.font.family = "'Archivo', sans-serif";
783
+ Chart.defaults.color = '#a1a1a1';
784
+
785
+ // Latency Distribution Chart
786
+ const latencyCtx = document.getElementById('latencyChart').getContext('2d');
787
+ new Chart(latencyCtx, {
788
+ type: 'bar',
789
+ data: {
790
+ labels: ['Min', 'P50', 'P90', 'P95', 'P99', 'Max'],
791
+ datasets: [{
792
+ label: 'Response Time (ms)',
793
+ data: [
794
+ ${formatNumber(metrics.minLatency, 2)},
795
+ ${formatNumber(metrics.p50, 2)},
796
+ ${formatNumber(metrics.p90, 2)},
797
+ ${formatNumber(metrics.p95, 2)},
798
+ ${formatNumber(metrics.p99, 2)},
799
+ ${formatNumber(metrics.maxLatency, 2)}
800
+ ],
801
+ backgroundColor: [
802
+ 'rgba(74, 222, 128, 0.7)',
803
+ 'rgba(74, 222, 128, 0.7)',
804
+ 'rgba(251, 191, 36, 0.7)',
805
+ 'rgba(251, 191, 36, 0.7)',
806
+ 'rgba(248, 113, 113, 0.7)',
807
+ 'rgba(248, 113, 113, 0.7)'
808
+ ],
809
+ borderColor: [
810
+ 'rgb(74, 222, 128)',
811
+ 'rgb(74, 222, 128)',
812
+ 'rgb(251, 191, 36)',
813
+ 'rgb(251, 191, 36)',
814
+ 'rgb(248, 113, 113)',
815
+ 'rgb(248, 113, 113)'
816
+ ],
817
+ borderWidth: 1,
818
+ borderRadius: 2,
819
+ }]
820
+ },
821
+ options: {
822
+ responsive: true,
823
+ maintainAspectRatio: false,
824
+ plugins: {
825
+ legend: { display: false },
826
+ tooltip: {
827
+ backgroundColor: '#1f1f1f',
828
+ titleColor: '#fafafa',
829
+ bodyColor: '#a1a1a1',
830
+ borderColor: '#2a2a2a',
831
+ borderWidth: 1,
832
+ padding: 10,
833
+ cornerRadius: 3,
834
+ displayColors: false,
835
+ callbacks: {
836
+ label: function(context) {
837
+ return context.parsed.y.toFixed(2) + ' ms';
838
+ }
839
+ }
840
+ }
841
+ },
842
+ scales: {
843
+ y: {
844
+ beginAtZero: true,
845
+ grid: { color: '#2a2a2a', drawBorder: false },
846
+ ticks: {
847
+ callback: function(value) { return value + ' ms'; }
848
+ }
849
+ },
850
+ x: {
851
+ grid: { display: false }
852
+ }
853
+ }
854
+ }
855
+ });
856
+
857
+ // Timing Breakdown Chart
858
+ const timingCtx = document.getElementById('timingChart').getContext('2d');
859
+ new Chart(timingCtx, {
860
+ type: 'doughnut',
861
+ data: {
862
+ labels: ['Connecting', 'TLS Handshake', 'Sending', 'Waiting (TTFB)', 'Receiving'],
863
+ datasets: [{
864
+ data: [
865
+ ${formatNumber(metrics.avgConnecting, 2)},
866
+ ${formatNumber(metrics.avgTlsHandshake, 2)},
867
+ ${formatNumber(metrics.avgSending, 2)},
868
+ ${formatNumber(metrics.avgWaiting, 2)},
869
+ ${formatNumber(metrics.avgReceiving, 2)}
870
+ ],
871
+ backgroundColor: [
872
+ 'rgba(167, 139, 250, 0.8)',
873
+ 'rgba(34, 211, 238, 0.8)',
874
+ 'rgba(244, 114, 182, 0.8)',
875
+ 'rgba(74, 222, 128, 0.8)',
876
+ 'rgba(251, 191, 36, 0.8)'
877
+ ],
878
+ borderColor: '#0a0a0a',
879
+ borderWidth: 2,
880
+ }]
881
+ },
882
+ options: {
883
+ responsive: true,
884
+ maintainAspectRatio: false,
885
+ cutout: '60%',
886
+ plugins: {
887
+ legend: {
888
+ position: 'right',
889
+ labels: {
890
+ padding: 15,
891
+ usePointStyle: true,
892
+ pointStyle: 'rect'
893
+ }
894
+ },
895
+ tooltip: {
896
+ backgroundColor: '#1f1f1f',
897
+ titleColor: '#fafafa',
898
+ bodyColor: '#a1a1a1',
899
+ borderColor: '#2a2a2a',
900
+ borderWidth: 1,
901
+ padding: 10,
902
+ cornerRadius: 3,
903
+ callbacks: {
904
+ label: function(context) {
905
+ return context.label + ': ' + context.parsed.toFixed(2) + ' ms';
906
+ }
907
+ }
908
+ }
909
+ }
910
+ }
911
+ });
912
+ </script>
913
+ </body>
914
+ </html>`;
915
+ }
916
+
917
+ /**
918
+ * Generate console summary for QPS test
919
+ */
920
+ export function generateQpsConsoleSummary(data, config) {
921
+ return "\nResults saved to qps-report.html\n";
922
+ }
923
+
924
+ /**
925
+ * Generate console summary for Throughput test
926
+ */
927
+ export function generateThroughputConsoleSummary(data, config) {
928
+ return "\nResults saved to throughput-report.html\n";
929
+ }