+47
-2
src/cache.ts
+47
-2
src/cache.ts
···
46
46
private db: Database;
47
47
private defaultExpiration: number; // in hours
48
48
private onEmojiExpired?: () => void;
49
+
private analyticsCache: Map<string, { data: any; timestamp: number }> = new Map();
50
+
private analyticsCacheTTL = 60000; // 1 minute cache for analytics
49
51
50
52
/**
51
53
* Creates a new Cache instance
···
121
123
CREATE INDEX IF NOT EXISTS idx_request_analytics_endpoint
122
124
ON request_analytics(endpoint)
123
125
`);
126
+
127
+
this.db.run(`
128
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_status_timestamp
129
+
ON request_analytics(status_code, timestamp)
130
+
`);
131
+
132
+
this.db.run(`
133
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_response_time
134
+
ON request_analytics(response_time) WHERE response_time IS NOT NULL
135
+
`);
136
+
137
+
this.db.run(`
138
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_composite
139
+
ON request_analytics(timestamp, endpoint, status_code)
140
+
`);
141
+
142
+
// Enable WAL mode for better concurrent performance
143
+
this.db.run('PRAGMA journal_mode = WAL');
144
+
this.db.run('PRAGMA synchronous = NORMAL');
145
+
this.db.run('PRAGMA cache_size = 10000');
146
+
this.db.run('PRAGMA temp_store = memory');
124
147
125
148
// check if there are any emojis in the db
126
149
if (this.onEmojiExpired) {
···
488
511
}
489
512
490
513
/**
491
-
* Gets request analytics statistics
514
+
* Gets request analytics statistics with performance optimizations
492
515
* @param days Number of days to look back (default: 7)
493
516
* @returns Analytics data
494
517
*/
···
559
582
total: number;
560
583
}>;
561
584
}> {
585
+
// Check cache first
586
+
const cacheKey = `analytics_${days}`;
587
+
const cached = this.analyticsCache.get(cacheKey);
588
+
const now = Date.now();
589
+
590
+
if (cached && (now - cached.timestamp) < this.analyticsCacheTTL) {
591
+
return cached.data;
592
+
}
562
593
const cutoffTime = Date.now() - days * 24 * 60 * 60 * 1000;
563
594
564
595
// Total requests (excluding stats endpoint)
···
1329
1360
.sort((a, b) => a.time.localeCompare(b.time));
1330
1361
}
1331
1362
1332
-
return {
1363
+
const result = {
1333
1364
totalRequests: totalResult.count,
1334
1365
requestsByEndpoint: requestsByEndpoint,
1335
1366
requestsByStatus: statusResults,
···
1361
1392
},
1362
1393
trafficOverview,
1363
1394
};
1395
+
1396
+
// Cache the result
1397
+
this.analyticsCache.set(cacheKey, {
1398
+
data: result,
1399
+
timestamp: now
1400
+
});
1401
+
1402
+
// Clean up old cache entries (keep only last 5)
1403
+
if (this.analyticsCache.size > 5) {
1404
+
const oldestKey = Array.from(this.analyticsCache.keys())[0];
1405
+
this.analyticsCache.delete(oldestKey);
1406
+
}
1407
+
1408
+
return result;
1364
1409
}
1365
1410
}
1366
1411
+1191
-199
src/dashboard.html
+1191
-199
src/dashboard.html
···
4
4
<meta charset="UTF-8" />
5
5
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
<title>Cachet Analytics Dashboard</title>
7
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
7
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
8
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
8
9
<style>
9
10
* {
10
11
margin: 0;
···
15
16
body {
16
17
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
17
18
sans-serif;
18
-
background: #f5f5f5;
19
-
color: #333;
19
+
background: #f8fafc;
20
+
color: #1e293b;
21
+
line-height: 1.6;
20
22
}
21
23
22
24
.header {
23
25
background: #fff;
24
26
padding: 1rem 2rem;
25
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
27
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
26
28
margin-bottom: 2rem;
27
29
display: flex;
28
30
justify-content: space-between;
29
31
align-items: center;
32
+
border-bottom: 1px solid #e2e8f0;
30
33
}
31
34
32
35
.header h1 {
···
45
48
46
49
.controls {
47
50
margin-bottom: 2rem;
48
-
text-align: center;
51
+
display: flex;
52
+
justify-content: center;
53
+
align-items: center;
54
+
gap: 1rem;
55
+
flex-wrap: wrap;
49
56
}
50
57
51
58
.controls select,
52
59
.controls button {
53
-
padding: 0.5rem 1rem;
54
-
margin: 0 0.5rem;
55
-
border: 1px solid #ddd;
56
-
border-radius: 4px;
60
+
padding: 0.75rem 1.25rem;
61
+
border: 1px solid #d1d5db;
62
+
border-radius: 8px;
57
63
background: white;
58
64
cursor: pointer;
65
+
font-size: 0.875rem;
66
+
font-weight: 500;
67
+
transition: all 0.2s ease;
68
+
}
69
+
70
+
.controls select:hover,
71
+
.controls select:focus {
72
+
border-color: #3b82f6;
73
+
outline: none;
74
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
59
75
}
60
76
61
77
.controls button {
62
-
background: #3498db;
78
+
background: #3b82f6;
63
79
color: white;
64
80
border: none;
65
81
}
66
82
67
83
.controls button:hover {
68
-
background: #2980b9;
84
+
background: #2563eb;
85
+
transform: translateY(-1px);
86
+
}
87
+
88
+
.controls button:disabled {
89
+
background: #9ca3af;
90
+
cursor: not-allowed;
91
+
transform: none;
69
92
}
70
93
71
94
.dashboard {
···
84
107
.stat-card {
85
108
background: white;
86
109
padding: 1.5rem;
87
-
border-radius: 8px;
88
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
110
+
border-radius: 12px;
111
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
89
112
text-align: center;
113
+
border: 1px solid #e2e8f0;
114
+
transition: all 0.2s ease;
115
+
position: relative;
116
+
overflow: hidden;
117
+
display: flex;
118
+
flex-direction: column;
119
+
justify-content: center;
120
+
min-height: 120px;
121
+
}
122
+
123
+
.stat-card:hover {
124
+
transform: translateY(-2px);
125
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
126
+
}
127
+
128
+
.stat-card.loading {
129
+
opacity: 0.6;
130
+
}
131
+
132
+
.stat-card.loading::after {
133
+
content: '';
134
+
position: absolute;
135
+
top: 0;
136
+
left: -100%;
137
+
width: 100%;
138
+
height: 100%;
139
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
140
+
animation: shimmer 1.5s infinite;
141
+
}
142
+
143
+
@keyframes shimmer {
144
+
0% { left: -100%; }
145
+
100% { left: 100%; }
90
146
}
91
147
92
148
.stat-number {
93
-
font-size: 2rem;
94
-
font-weight: bold;
95
-
color: #3498db;
149
+
font-weight: 700;
150
+
color: #1f2937;
151
+
margin-bottom: 0.25rem;
152
+
word-break: break-word;
153
+
overflow-wrap: break-word;
154
+
line-height: 1.1;
155
+
156
+
/* Responsive font sizing using clamp() */
157
+
font-size: clamp(1.25rem, 4vw, 2.5rem);
96
158
}
97
159
98
160
.stat-label {
99
-
color: #666;
161
+
color: #4b5563;
100
162
margin-top: 0.5rem;
163
+
font-weight: 500;
164
+
line-height: 1.3;
165
+
166
+
/* Responsive font sizing for labels */
167
+
font-size: clamp(0.75rem, 2vw, 0.875rem);
168
+
}
169
+
170
+
/* Container query support for modern browsers */
171
+
@supports (container-type: inline-size) {
172
+
.stats-grid {
173
+
container-type: inline-size;
174
+
}
175
+
176
+
@container (max-width: 250px) {
177
+
.stat-number {
178
+
font-size: 1.25rem;
179
+
}
180
+
.stat-label {
181
+
font-size: 0.75rem;
182
+
}
183
+
}
184
+
185
+
@container (min-width: 300px) {
186
+
.stat-number {
187
+
font-size: 2rem;
188
+
}
189
+
.stat-label {
190
+
font-size: 0.875rem;
191
+
}
192
+
}
193
+
194
+
@container (min-width: 400px) {
195
+
.stat-number {
196
+
font-size: 2.5rem;
197
+
}
198
+
.stat-label {
199
+
font-size: 1rem;
200
+
}
201
+
}
101
202
}
102
203
103
204
.charts-grid {
···
107
208
margin-bottom: 2rem;
108
209
}
109
210
211
+
@media (max-width: 480px) {
212
+
.charts-grid {
213
+
grid-template-columns: 1fr;
214
+
}
215
+
216
+
.chart-container {
217
+
min-height: 250px;
218
+
}
219
+
220
+
.stats-grid {
221
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
222
+
}
223
+
224
+
.stat-card {
225
+
padding: 1rem;
226
+
min-height: 100px;
227
+
}
228
+
229
+
.stat-number {
230
+
font-size: clamp(0.9rem, 4vw, 1.5rem) !important;
231
+
}
232
+
233
+
.stat-label {
234
+
font-size: clamp(0.65rem, 2.5vw, 0.75rem) !important;
235
+
}
236
+
}
237
+
110
238
.chart-container {
111
239
background: white;
112
240
padding: 1.5rem;
113
-
border-radius: 8px;
114
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
241
+
border-radius: 12px;
242
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
243
+
border: 1px solid #e2e8f0;
244
+
position: relative;
245
+
min-height: 350px;
246
+
max-height: 500px;
247
+
overflow: hidden;
248
+
display: flex;
249
+
flex-direction: column;
250
+
}
251
+
252
+
.chart-container.loading {
253
+
display: flex;
254
+
align-items: center;
255
+
justify-content: center;
256
+
}
257
+
258
+
.chart-container.loading::before {
259
+
content: 'Loading chart...';
260
+
color: #64748b;
261
+
font-size: 0.875rem;
115
262
}
116
263
117
264
.chart-title {
118
-
font-size: 1.2rem;
265
+
font-size: 1.125rem;
119
266
margin-bottom: 1rem;
120
-
color: #2c3e50;
267
+
padding-top: 1rem;
268
+
color: #1e293b;
269
+
font-weight: 600;
270
+
display: block;
271
+
width: 100%;
272
+
text-align: left;
273
+
word-break: break-word;
274
+
overflow-wrap: break-word;
275
+
}
276
+
277
+
.chart-title-with-indicator {
278
+
display: flex;
279
+
align-items: center;
280
+
justify-content: space-between;
281
+
flex-wrap: wrap;
282
+
gap: 0.5rem;
283
+
margin-bottom: 1rem;
284
+
}
285
+
286
+
.chart-title-with-indicator .chart-title {
287
+
margin-bottom: 0;
288
+
flex: 1;
289
+
min-width: 0;
290
+
}
291
+
292
+
.chart-error {
293
+
color: #dc2626;
294
+
text-align: center;
295
+
padding: 2rem;
296
+
font-size: 0.875rem;
121
297
}
122
298
123
299
.loading {
124
300
text-align: center;
125
-
padding: 2rem;
126
-
color: #666;
301
+
padding: 3rem;
302
+
color: #64748b;
303
+
}
304
+
305
+
.loading-spinner {
306
+
display: inline-block;
307
+
width: 2rem;
308
+
height: 2rem;
309
+
border: 3px solid #e2e8f0;
310
+
border-radius: 50%;
311
+
border-top-color: #3b82f6;
312
+
animation: spin 1s ease-in-out infinite;
313
+
margin-bottom: 1rem;
314
+
}
315
+
316
+
@keyframes spin {
317
+
to { transform: rotate(360deg); }
127
318
}
128
319
129
320
.error {
130
-
background: #e74c3c;
131
-
color: white;
321
+
background: #fef2f2;
322
+
color: #dc2626;
132
323
padding: 1rem;
133
-
border-radius: 4px;
324
+
border-radius: 8px;
134
325
margin: 1rem 0;
326
+
border: 1px solid #fecaca;
135
327
}
136
328
137
329
.auto-refresh {
138
330
display: flex;
139
331
align-items: center;
140
332
gap: 0.5rem;
141
-
justify-content: center;
142
-
margin-top: 1rem;
333
+
font-size: 0.875rem;
334
+
color: #64748b;
143
335
}
144
336
145
337
.auto-refresh input[type="checkbox"] {
146
-
transform: scale(1.2);
338
+
transform: scale(1.1);
339
+
accent-color: #3b82f6;
340
+
}
341
+
342
+
.performance-indicator {
343
+
display: inline-flex;
344
+
align-items: center;
345
+
gap: 0.25rem;
346
+
font-size: 0.75rem;
347
+
color: #64748b;
348
+
}
349
+
350
+
.performance-indicator.good { color: #059669; }
351
+
.performance-indicator.warning { color: #d97706; }
352
+
.performance-indicator.error { color: #dc2626; }
353
+
354
+
.lazy-chart {
355
+
min-height: 300px;
356
+
display: flex;
357
+
align-items: center;
358
+
justify-content: center;
359
+
background: #f8fafc;
360
+
border-radius: 8px;
361
+
margin: 1rem 0;
362
+
}
363
+
364
+
.lazy-chart.visible {
365
+
background: transparent;
147
366
}
148
367
149
368
@media (max-width: 768px) {
···
159
378
flex-direction: column;
160
379
gap: 1rem;
161
380
text-align: center;
381
+
padding: 1rem;
382
+
}
383
+
384
+
.controls {
385
+
flex-direction: column;
386
+
align-items: stretch;
162
387
}
388
+
389
+
.controls select,
390
+
.controls button {
391
+
margin: 0.25rem 0;
392
+
}
393
+
394
+
.stat-number {
395
+
font-size: clamp(1rem, 5vw, 1.75rem) !important;
396
+
}
397
+
398
+
.stat-label {
399
+
font-size: clamp(0.7rem, 3vw, 0.8rem) !important;
400
+
}
401
+
402
+
.chart-container {
403
+
padding: 1rem;
404
+
min-height: 250px;
405
+
}
406
+
407
+
.chart-title {
408
+
font-size: 1rem;
409
+
flex-direction: column;
410
+
align-items: flex-start;
411
+
}
412
+
}
413
+
414
+
.toast {
415
+
position: fixed;
416
+
top: 1rem;
417
+
right: 1rem;
418
+
background: #1f2937;
419
+
color: white;
420
+
padding: 0.75rem 1rem;
421
+
border-radius: 8px;
422
+
font-size: 0.875rem;
423
+
z-index: 1000;
424
+
transform: translateX(100%);
425
+
transition: transform 0.3s ease;
426
+
}
427
+
428
+
.toast.show {
429
+
transform: translateX(0);
430
+
}
431
+
432
+
.toast.success {
433
+
background: #059669;
434
+
}
435
+
436
+
.toast.error {
437
+
background: #dc2626;
163
438
}
164
439
</style>
165
440
</head>
···
180
455
<option value="7" selected>Last 7 days</option>
181
456
<option value="30">Last 30 days</option>
182
457
</select>
183
-
<button onclick="loadData()">Refresh</button>
458
+
<button id="refreshBtn" onclick="loadData()">Refresh</button>
184
459
<div class="auto-refresh">
185
460
<input type="checkbox" id="autoRefresh" />
186
461
<label for="autoRefresh">Auto-refresh (30s)</label>
187
462
</div>
188
463
</div>
189
464
190
-
<div id="loading" class="loading">Loading analytics data...</div>
465
+
<div id="loading" class="loading">
466
+
<div class="loading-spinner"></div>
467
+
Loading analytics data...
468
+
</div>
191
469
<div id="error" class="error" style="display: none"></div>
192
470
193
471
<div id="content" style="display: none">
472
+
<!-- Key Metrics Overview -->
194
473
<div class="chart-container" style="margin-bottom: 2rem; height: 450px">
195
-
<div class="chart-title">Traffic Overview - All Routes Over Time</div>
474
+
<div class="chart-title-with-indicator">
475
+
<div class="chart-title">Traffic Overview - All Routes Over Time</div>
476
+
<span class="performance-indicator" id="trafficPerformance"></span>
477
+
</div>
196
478
<canvas
197
479
id="trafficOverviewChart"
198
480
style="padding-bottom: 2rem"
199
481
></canvas>
200
482
</div>
201
483
484
+
<!-- Stats Grid -->
202
485
<div class="stats-grid">
203
-
<div class="stat-card">
486
+
<div class="stat-card" id="totalRequestsCard">
204
487
<div class="stat-number" id="totalRequests">-</div>
205
488
<div class="stat-label">Total Requests</div>
206
489
</div>
207
-
<div class="stat-card">
490
+
<div class="stat-card" id="avgResponseTimeCard">
208
491
<div class="stat-number" id="avgResponseTime">-</div>
209
492
<div class="stat-label">Avg Response Time (ms)</div>
210
493
</div>
211
-
<div class="stat-card">
494
+
<div class="stat-card" id="p95ResponseTimeCard">
212
495
<div class="stat-number" id="p95ResponseTime">-</div>
213
496
<div class="stat-label">P95 Response Time (ms)</div>
214
497
</div>
215
-
<div class="stat-card">
498
+
<div class="stat-card" id="uniqueEndpointsCard">
216
499
<div class="stat-number" id="uniqueEndpoints">-</div>
217
500
<div class="stat-label">Unique Endpoints</div>
218
501
</div>
219
-
<div class="stat-card">
502
+
<div class="stat-card" id="errorRateCard">
220
503
<div class="stat-number" id="errorRate">-</div>
221
504
<div class="stat-label">Error Rate (%)</div>
222
505
</div>
223
-
<div class="stat-card">
506
+
<div class="stat-card" id="fastRequestsCard">
224
507
<div class="stat-number" id="fastRequests">-</div>
225
508
<div class="stat-label">Fast Requests (<100ms)</div>
226
509
</div>
227
-
<div class="stat-card">
510
+
<div class="stat-card" id="uptimeCard">
228
511
<div class="stat-number" id="uptime">-</div>
229
512
<div class="stat-label">Uptime (%)</div>
230
513
</div>
231
-
<div class="stat-card">
514
+
<div class="stat-card" id="throughputCard">
232
515
<div class="stat-number" id="throughput">-</div>
233
516
<div class="stat-label">Throughput (req/hr)</div>
234
517
</div>
235
-
<div class="stat-card">
518
+
<div class="stat-card" id="apdexCard">
236
519
<div class="stat-number" id="apdex">-</div>
237
520
<div class="stat-label">APDEX Score</div>
238
521
</div>
239
-
<div class="stat-card">
522
+
<div class="stat-card" id="cacheHitRateCard">
240
523
<div class="stat-number" id="cacheHitRate">-</div>
241
524
<div class="stat-label">Cache Hit Rate (%)</div>
242
525
</div>
243
526
</div>
244
527
528
+
<!-- Peak Traffic Stats -->
245
529
<div class="stats-grid">
246
-
<div class="stat-card">
530
+
<div class="stat-card" id="peakHourCard">
247
531
<div class="stat-number" id="peakHour">-</div>
248
532
<div class="stat-label">Peak Hour</div>
249
533
</div>
250
-
<div class="stat-card">
534
+
<div class="stat-card" id="peakHourRequestsCard">
251
535
<div class="stat-number" id="peakHourRequests">-</div>
252
536
<div class="stat-label">Peak Hour Requests</div>
253
537
</div>
254
-
<div class="stat-card">
538
+
<div class="stat-card" id="peakDayCard">
255
539
<div class="stat-number" id="peakDay">-</div>
256
540
<div class="stat-label">Peak Day</div>
257
541
</div>
258
-
<div class="stat-card">
542
+
<div class="stat-card" id="peakDayRequestsCard">
259
543
<div class="stat-number" id="peakDayRequests">-</div>
260
544
<div class="stat-label">Peak Day Requests</div>
261
545
</div>
262
-
<div class="stat-card">
546
+
<div class="stat-card" id="dashboardRequestsCard">
263
547
<div class="stat-number" id="dashboardRequests">-</div>
264
548
<div class="stat-label">Dashboard Requests</div>
265
549
</div>
266
550
</div>
267
551
552
+
<!-- Charts Grid with Lazy Loading -->
268
553
<div class="charts-grid">
269
-
<div class="chart-container">
554
+
<div class="chart-container lazy-chart" data-chart="timeChart">
270
555
<div class="chart-title">Requests Over Time</div>
271
556
<canvas id="timeChart"></canvas>
272
557
</div>
273
558
274
-
<div class="chart-container">
559
+
<div class="chart-container lazy-chart" data-chart="latencyTimeChart">
275
560
<div class="chart-title">Latency Over Time (Hourly)</div>
276
561
<canvas id="latencyTimeChart"></canvas>
277
562
</div>
278
563
279
-
<div class="chart-container">
564
+
<div class="chart-container lazy-chart" data-chart="latencyDistributionChart">
280
565
<div class="chart-title">Response Time Distribution</div>
281
566
<canvas id="latencyDistributionChart"></canvas>
282
567
</div>
283
568
284
-
<div class="chart-container">
569
+
<div class="chart-container lazy-chart" data-chart="percentilesChart">
285
570
<div class="chart-title">Latency Percentiles</div>
286
571
<canvas id="percentilesChart"></canvas>
287
572
</div>
288
573
289
-
<div class="chart-container">
574
+
<div class="chart-container lazy-chart" data-chart="endpointChart">
290
575
<div class="chart-title">Top Endpoints</div>
291
576
<canvas id="endpointChart"></canvas>
292
577
</div>
293
578
294
-
<div class="chart-container">
579
+
<div class="chart-container lazy-chart" data-chart="slowestEndpointsChart">
295
580
<div class="chart-title">Slowest Endpoints</div>
296
581
<canvas id="slowestEndpointsChart"></canvas>
297
582
</div>
298
583
299
-
<div class="chart-container">
584
+
<div class="chart-container lazy-chart" data-chart="statusChart">
300
585
<div class="chart-title">Status Codes</div>
301
586
<canvas id="statusChart"></canvas>
302
587
</div>
303
588
304
-
<div class="chart-container">
589
+
<div class="chart-container lazy-chart" data-chart="userAgentChart">
305
590
<div class="chart-title">Top User Agents</div>
306
591
<canvas id="userAgentChart"></canvas>
307
592
</div>
···
312
597
<script>
313
598
let charts = {};
314
599
let autoRefreshInterval;
600
+
let currentData = null;
601
+
let isLoading = false;
602
+
let visibleCharts = new Set();
603
+
let intersectionObserver;
604
+
605
+
// Performance monitoring
606
+
const performance = {
607
+
startTime: 0,
608
+
endTime: 0,
609
+
loadTime: 0
610
+
};
611
+
612
+
// Initialize intersection observer for lazy loading
613
+
function initLazyLoading() {
614
+
intersectionObserver = new IntersectionObserver((entries) => {
615
+
entries.forEach(entry => {
616
+
if (entry.isIntersecting) {
617
+
const chartContainer = entry.target;
618
+
const chartType = chartContainer.dataset.chart;
619
+
620
+
if (!visibleCharts.has(chartType) && currentData) {
621
+
visibleCharts.add(chartType);
622
+
chartContainer.classList.add('visible');
623
+
loadChart(chartType, currentData);
624
+
}
625
+
}
626
+
});
627
+
}, {
628
+
rootMargin: '50px',
629
+
threshold: 0.1
630
+
});
631
+
632
+
// Observe all lazy chart containers
633
+
document.querySelectorAll('.lazy-chart').forEach(container => {
634
+
intersectionObserver.observe(container);
635
+
});
636
+
}
637
+
638
+
// Show toast notification
639
+
function showToast(message, type = 'info') {
640
+
const toast = document.createElement('div');
641
+
toast.className = `toast ${type}`;
642
+
toast.textContent = message;
643
+
document.body.appendChild(toast);
644
+
645
+
setTimeout(() => toast.classList.add('show'), 100);
646
+
setTimeout(() => {
647
+
toast.classList.remove('show');
648
+
setTimeout(() => document.body.removeChild(toast), 300);
649
+
}, 3000);
650
+
}
651
+
652
+
// Update loading states for stat cards
653
+
function setStatCardLoading(cardId, loading) {
654
+
const card = document.getElementById(cardId);
655
+
if (card) {
656
+
if (loading) {
657
+
card.classList.add('loading');
658
+
} else {
659
+
card.classList.remove('loading');
660
+
}
661
+
}
662
+
}
663
+
664
+
// Debounced resize handler for charts
665
+
let resizeTimeout;
666
+
function handleResize() {
667
+
clearTimeout(resizeTimeout);
668
+
resizeTimeout = setTimeout(() => {
669
+
Object.values(charts).forEach(chart => {
670
+
if (chart && typeof chart.resize === 'function') {
671
+
chart.resize();
672
+
}
673
+
});
674
+
}, 250);
675
+
}
676
+
677
+
window.addEventListener('resize', handleResize);
315
678
316
679
async function loadData() {
680
+
if (isLoading) return;
681
+
682
+
isLoading = true;
683
+
performance.startTime = Date.now();
684
+
317
685
const days = document.getElementById("daysSelect").value;
318
686
const loading = document.getElementById("loading");
319
687
const error = document.getElementById("error");
320
688
const content = document.getElementById("content");
689
+
const refreshBtn = document.getElementById("refreshBtn");
321
690
691
+
// Update UI state
322
692
loading.style.display = "block";
323
693
error.style.display = "none";
324
694
content.style.display = "none";
695
+
refreshBtn.disabled = true;
696
+
refreshBtn.textContent = "Loading...";
697
+
698
+
// Set all stat cards to loading state
699
+
const statCards = [
700
+
'totalRequestsCard', 'avgResponseTimeCard', 'p95ResponseTimeCard',
701
+
'uniqueEndpointsCard', 'errorRateCard', 'fastRequestsCard',
702
+
'uptimeCard', 'throughputCard', 'apdexCard', 'cacheHitRateCard',
703
+
'peakHourCard', 'peakHourRequestsCard', 'peakDayCard',
704
+
'peakDayRequestsCard', 'dashboardRequestsCard'
705
+
];
706
+
statCards.forEach(cardId => setStatCardLoading(cardId, true));
325
707
326
708
try {
327
709
const response = await fetch(`/stats?days=${days}`);
328
710
if (!response.ok) throw new Error(`HTTP ${response.status}`);
329
711
330
712
const data = await response.json();
713
+
currentData = data;
714
+
715
+
performance.endTime = Date.now();
716
+
performance.loadTime = performance.endTime - performance.startTime;
717
+
331
718
updateDashboard(data);
332
-
719
+
333
720
loading.style.display = "none";
334
721
content.style.display = "block";
722
+
723
+
showToast(`Dashboard updated in ${performance.loadTime}ms`, 'success');
335
724
} catch (err) {
336
725
loading.style.display = "none";
337
726
error.style.display = "block";
338
727
error.textContent = `Failed to load data: ${err.message}`;
728
+
showToast(`Error: ${err.message}`, 'error');
729
+
} finally {
730
+
isLoading = false;
731
+
refreshBtn.disabled = false;
732
+
refreshBtn.textContent = "Refresh";
733
+
734
+
// Remove loading state from stat cards
735
+
statCards.forEach(cardId => setStatCardLoading(cardId, false));
339
736
}
340
737
}
341
738
342
739
function updateDashboard(data) {
343
-
// Main metrics
344
-
document.getElementById("totalRequests").textContent =
345
-
data.totalRequests.toLocaleString();
346
-
document.getElementById("avgResponseTime").textContent =
347
-
data.averageResponseTime
348
-
? Math.round(data.averageResponseTime)
349
-
: "N/A";
350
-
document.getElementById("p95ResponseTime").textContent = data
351
-
.latencyAnalytics.percentiles.p95
352
-
? Math.round(data.latencyAnalytics.percentiles.p95)
353
-
: "N/A";
354
-
document.getElementById("uniqueEndpoints").textContent =
355
-
data.requestsByEndpoint.length;
740
+
// Update main metrics with animation
741
+
updateStatWithAnimation("totalRequests", data.totalRequests.toLocaleString());
742
+
updateStatWithAnimation("avgResponseTime",
743
+
data.averageResponseTime ? Math.round(data.averageResponseTime) : "N/A");
744
+
updateStatWithAnimation("p95ResponseTime",
745
+
data.latencyAnalytics.percentiles.p95 ? Math.round(data.latencyAnalytics.percentiles.p95) : "N/A");
746
+
updateStatWithAnimation("uniqueEndpoints", data.requestsByEndpoint.length);
356
747
357
748
const errorRequests = data.requestsByStatus
358
749
.filter((s) => s.status >= 400)
359
750
.reduce((sum, s) => sum + s.count, 0);
360
-
const errorRate =
361
-
data.totalRequests > 0
362
-
? ((errorRequests / data.totalRequests) * 100).toFixed(1)
363
-
: "0.0";
364
-
document.getElementById("errorRate").textContent = errorRate;
751
+
const errorRate = data.totalRequests > 0
752
+
? ((errorRequests / data.totalRequests) * 100).toFixed(1)
753
+
: "0.0";
754
+
updateStatWithAnimation("errorRate", errorRate);
365
755
366
756
// Calculate fast requests percentage
367
757
const fastRequestsData = data.latencyAnalytics.distribution
368
758
.filter((d) => d.range === "0-50ms" || d.range === "50-100ms")
369
759
.reduce((sum, d) => sum + d.percentage, 0);
370
-
document.getElementById("fastRequests").textContent =
371
-
fastRequestsData.toFixed(1) + "%";
760
+
updateStatWithAnimation("fastRequests", fastRequestsData.toFixed(1) + "%");
372
761
373
762
// Performance metrics
374
-
document.getElementById("uptime").textContent =
375
-
data.performanceMetrics.uptime.toFixed(1);
376
-
document.getElementById("throughput").textContent = Math.round(
377
-
data.performanceMetrics.throughput,
378
-
);
379
-
document.getElementById("apdex").textContent =
380
-
data.performanceMetrics.apdex.toFixed(2);
381
-
document.getElementById("cacheHitRate").textContent =
382
-
data.performanceMetrics.cachehitRate.toFixed(1);
763
+
updateStatWithAnimation("uptime", data.performanceMetrics.uptime.toFixed(1));
764
+
updateStatWithAnimation("throughput", Math.round(data.performanceMetrics.throughput));
765
+
updateStatWithAnimation("apdex", data.performanceMetrics.apdex.toFixed(2));
766
+
updateStatWithAnimation("cacheHitRate", data.performanceMetrics.cachehitRate.toFixed(1));
383
767
384
768
// Peak traffic
385
-
document.getElementById("peakHour").textContent =
386
-
data.peakTraffic.peakHour;
387
-
document.getElementById("peakHourRequests").textContent =
388
-
data.peakTraffic.peakRequests.toLocaleString();
389
-
document.getElementById("peakDay").textContent =
390
-
data.peakTraffic.peakDay;
391
-
document.getElementById("peakDayRequests").textContent =
392
-
data.peakTraffic.peakDayRequests.toLocaleString();
769
+
updateStatWithAnimation("peakHour", data.peakTraffic.peakHour);
770
+
updateStatWithAnimation("peakHourRequests", data.peakTraffic.peakRequests.toLocaleString());
771
+
updateStatWithAnimation("peakDay", data.peakTraffic.peakDay);
772
+
updateStatWithAnimation("peakDayRequests", data.peakTraffic.peakDayRequests.toLocaleString());
773
+
updateStatWithAnimation("dashboardRequests", data.dashboardMetrics.statsRequests.toLocaleString());
774
+
775
+
// Update performance indicator
776
+
updatePerformanceIndicator(data);
777
+
778
+
// Load main traffic overview chart immediately
779
+
const days = parseInt(document.getElementById("daysSelect").value);
780
+
updateTrafficOverviewChart(data.trafficOverview, days);
781
+
782
+
// Other charts will be loaded lazily when they come into view
783
+
}
784
+
785
+
function updateStatWithAnimation(elementId, value) {
786
+
const element = document.getElementById(elementId);
787
+
if (element && element.textContent !== value.toString()) {
788
+
element.style.transform = 'scale(1.1)';
789
+
element.style.transition = 'transform 0.2s ease';
790
+
791
+
setTimeout(() => {
792
+
element.textContent = value;
793
+
element.style.transform = 'scale(1)';
794
+
}, 100);
795
+
}
796
+
}
797
+
798
+
function updatePerformanceIndicator(data) {
799
+
const indicator = document.getElementById('trafficPerformance');
800
+
const avgResponseTime = data.averageResponseTime || 0;
801
+
const errorRate = data.requestsByStatus
802
+
.filter((s) => s.status >= 400)
803
+
.reduce((sum, s) => sum + s.count, 0) / data.totalRequests * 100;
804
+
805
+
let status, text;
806
+
if (avgResponseTime < 100 && errorRate < 1) {
807
+
status = 'good';
808
+
text = '🟢 Excellent';
809
+
} else if (avgResponseTime < 300 && errorRate < 5) {
810
+
status = 'warning';
811
+
text = '🟡 Good';
812
+
} else {
813
+
status = 'error';
814
+
text = '🔴 Needs Attention';
815
+
}
393
816
394
-
// Dashboard metrics
395
-
document.getElementById("dashboardRequests").textContent =
396
-
data.dashboardMetrics.statsRequests.toLocaleString();
817
+
indicator.className = `performance-indicator ${status}`;
818
+
indicator.textContent = text;
819
+
}
397
820
398
-
// Determine if we're showing hourly or daily data
821
+
// Load individual charts (called by intersection observer)
822
+
function loadChart(chartType, data) {
399
823
const days = parseInt(document.getElementById("daysSelect").value);
400
824
const isHourly = days === 1;
401
825
402
-
updateTrafficOverviewChart(data.trafficOverview, days);
403
-
updateTimeChart(data.requestsByDay, isHourly);
404
-
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
405
-
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
406
-
updatePercentilesChart(data.latencyAnalytics.percentiles);
407
-
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
408
-
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
409
-
updateStatusChart(data.requestsByStatus);
410
-
updateUserAgentChart(data.topUserAgents.slice(0, 5));
826
+
try {
827
+
switch(chartType) {
828
+
case 'timeChart':
829
+
updateTimeChart(data.requestsByDay, isHourly);
830
+
break;
831
+
case 'latencyTimeChart':
832
+
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
833
+
break;
834
+
case 'latencyDistributionChart':
835
+
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
836
+
break;
837
+
case 'percentilesChart':
838
+
updatePercentilesChart(data.latencyAnalytics.percentiles);
839
+
break;
840
+
case 'endpointChart':
841
+
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
842
+
break;
843
+
case 'slowestEndpointsChart':
844
+
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
845
+
break;
846
+
case 'statusChart':
847
+
updateStatusChart(data.requestsByStatus);
848
+
break;
849
+
case 'userAgentChart':
850
+
updateUserAgentChart(data.topUserAgents.slice(0, 5));
851
+
break;
852
+
}
853
+
} catch (error) {
854
+
console.error(`Error loading chart ${chartType}:`, error);
855
+
const container = document.querySelector(`[data-chart="${chartType}"]`);
856
+
if (container) {
857
+
container.innerHTML = `<div class="chart-error">Error loading chart: ${error.message}</div>`;
858
+
}
859
+
}
411
860
}
412
861
413
862
function updateTrafficOverviewChart(data, days) {
414
-
const ctx = document
415
-
.getElementById("trafficOverviewChart")
416
-
.getContext("2d");
863
+
const canvas = document.getElementById("trafficOverviewChart");
864
+
if (!canvas) {
865
+
console.warn('trafficOverviewChart canvas not found');
866
+
return;
867
+
}
868
+
869
+
const ctx = canvas.getContext("2d");
870
+
if (!ctx) {
871
+
console.warn('Could not get 2d context for trafficOverviewChart');
872
+
return;
873
+
}
417
874
418
-
if (charts.trafficOverview) charts.trafficOverview.destroy();
875
+
if (charts.trafficOverview) {
876
+
charts.trafficOverview.destroy();
877
+
}
419
878
420
879
// Update chart title based on granularity
421
-
const chartTitle = document
422
-
.querySelector("#trafficOverviewChart")
423
-
.parentElement.querySelector(".chart-title");
880
+
const chartTitleElement = document.querySelector(".chart-title-with-indicator .chart-title");
424
881
let titleText = "Traffic Overview - All Routes Over Time";
425
882
if (days === 1) {
426
883
titleText += " (Hourly)";
···
429
886
} else {
430
887
titleText += " (Daily)";
431
888
}
432
-
chartTitle.textContent = titleText;
889
+
if (chartTitleElement) {
890
+
chartTitleElement.textContent = titleText;
891
+
}
433
892
434
893
// Get all unique routes across all time periods
435
894
const allRoutes = new Set();
···
441
900
442
901
// Define colors for different route types
443
902
const routeColors = {
444
-
Dashboard: "#3498db",
445
-
"User Data": "#2ecc71",
446
-
"User Redirects": "#27ae60",
447
-
"Emoji Data": "#e74c3c",
448
-
"Emoji Redirects": "#c0392b",
449
-
"Emoji List": "#e67e22",
450
-
"Health Check": "#f39c12",
451
-
"API Documentation": "#9b59b6",
452
-
"Cache Management": "#34495e",
903
+
Dashboard: "#3b82f6",
904
+
"User Data": "#10b981",
905
+
"User Redirects": "#059669",
906
+
"Emoji Data": "#ef4444",
907
+
"Emoji Redirects": "#dc2626",
908
+
"Emoji List": "#f97316",
909
+
"Health Check": "#f59e0b",
910
+
"API Documentation": "#8b5cf6",
911
+
"Cache Management": "#6b7280",
453
912
};
454
913
455
914
// Create datasets for each route
456
915
const datasets = Array.from(allRoutes).map((route) => {
457
-
const color = routeColors[route] || "#95a5a6";
916
+
const color = routeColors[route] || "#9ca3af";
458
917
return {
459
918
label: route,
460
919
data: data.map((timePoint) => timePoint.routes[route] || 0),
461
920
borderColor: color,
462
-
backgroundColor: color + "20", // Add transparency
921
+
backgroundColor: color + "20",
463
922
tension: 0.4,
464
923
fill: false,
465
924
pointRadius: 2,
···
470
929
// Format labels based on time granularity
471
930
const labels = data.map((timePoint) => {
472
931
if (days === 1) {
473
-
// Show just hour for 24h view
474
932
return timePoint.time.split(" ")[1] || timePoint.time;
475
933
} else if (days <= 7) {
476
-
// Show day and hour for 7-day view
477
934
const parts = timePoint.time.split(" ");
478
-
const date = parts[0].split("-")[2]; // Get day
935
+
const date = parts[0].split("-")[2];
479
936
const hour = parts[1] || "00:00";
480
937
return `${date} ${hour}`;
481
938
} else {
482
-
// Show full date for longer periods
483
939
return timePoint.time;
484
940
}
485
941
});
···
493
949
options: {
494
950
responsive: true,
495
951
maintainAspectRatio: false,
952
+
layout: {
953
+
padding: {
954
+
left: 10,
955
+
right: 10,
956
+
top: 10,
957
+
bottom: 50
958
+
}
959
+
},
960
+
animation: {
961
+
duration: 750,
962
+
easing: 'easeInOutQuart'
963
+
},
496
964
interaction: {
497
965
mode: "index",
498
966
intersect: false,
···
506
974
font: {
507
975
size: 11,
508
976
},
977
+
boxWidth: 12,
978
+
boxHeight: 12,
979
+
generateLabels: function(chart) {
980
+
const original = Chart.defaults.plugins.legend.labels.generateLabels;
981
+
const labels = original.call(this, chart);
982
+
983
+
// Add total request count to legend labels
984
+
labels.forEach((label, index) => {
985
+
const dataset = chart.data.datasets[index];
986
+
const total = dataset.data.reduce((sum, val) => sum + val, 0);
987
+
label.text += ` (${total.toLocaleString()})`;
988
+
});
989
+
990
+
return labels;
991
+
}
509
992
},
510
993
},
511
994
tooltip: {
512
995
mode: "index",
513
996
intersect: false,
997
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
998
+
titleColor: 'white',
999
+
bodyColor: 'white',
1000
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1001
+
borderWidth: 1,
514
1002
callbacks: {
515
-
afterLabel: function (context) {
516
-
const timePoint = data[context.dataIndex];
517
-
return `Total: ${timePoint.total} requests`;
1003
+
title: function(context) {
1004
+
return `Time: ${context[0].label}`;
1005
+
},
1006
+
afterBody: function(context) {
1007
+
const timePoint = data[context[0].dataIndex];
1008
+
return [
1009
+
`Total Requests: ${timePoint.total.toLocaleString()}`,
1010
+
`Peak Route: ${Object.entries(timePoint.routes).sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A'}`
1011
+
];
518
1012
},
519
1013
},
520
1014
},
···
525
1019
title: {
526
1020
display: true,
527
1021
text: days === 1 ? "Hour" : days <= 7 ? "Day & Hour" : "Date",
1022
+
font: {
1023
+
weight: 'bold'
1024
+
}
528
1025
},
529
1026
ticks: {
530
-
maxTicksLimit: 20,
1027
+
maxTicksLimit: window.innerWidth < 768 ? 8 : 20,
1028
+
maxRotation: 0, // Don't rotate labels
1029
+
minRotation: 0,
1030
+
callback: function(value, index, values) {
1031
+
const label = this.getLabelForValue(value);
1032
+
// Truncate long labels
1033
+
if (label.length > 8) {
1034
+
if (days === 1) {
1035
+
return label; // Hours are usually short
1036
+
} else {
1037
+
// For longer periods, abbreviate
1038
+
return label.substring(0, 6) + '...';
1039
+
}
1040
+
}
1041
+
return label;
1042
+
}
1043
+
},
1044
+
grid: {
1045
+
color: 'rgba(0, 0, 0, 0.05)',
531
1046
},
532
1047
},
533
1048
y: {
···
535
1050
title: {
536
1051
display: true,
537
1052
text: "Requests",
1053
+
font: {
1054
+
weight: 'bold'
1055
+
}
538
1056
},
539
1057
beginAtZero: true,
1058
+
grid: {
1059
+
color: 'rgba(0, 0, 0, 0.05)',
1060
+
},
1061
+
ticks: {
1062
+
callback: function(value) {
1063
+
return value.toLocaleString();
1064
+
}
1065
+
}
540
1066
},
541
1067
},
542
1068
elements: {
543
1069
line: {
544
1070
tension: 0.4,
545
1071
},
1072
+
point: {
1073
+
radius: 3,
1074
+
hoverRadius: 6,
1075
+
hitRadius: 10,
1076
+
},
546
1077
},
547
1078
},
548
1079
});
549
1080
}
550
1081
551
1082
function updateTimeChart(data, isHourly) {
552
-
const ctx = document.getElementById("timeChart").getContext("2d");
1083
+
const canvas = document.getElementById("timeChart");
1084
+
if (!canvas) {
1085
+
console.warn('timeChart canvas not found');
1086
+
return;
1087
+
}
1088
+
1089
+
const ctx = canvas.getContext("2d");
1090
+
if (!ctx) {
1091
+
console.warn('Could not get 2d context for timeChart');
1092
+
return;
1093
+
}
553
1094
554
1095
if (charts.time) charts.time.destroy();
555
1096
556
-
// Update chart title
557
1097
const chartTitle = document
558
1098
.querySelector("#timeChart")
559
1099
.parentElement.querySelector(".chart-title");
560
-
chartTitle.textContent = isHourly
561
-
? "Requests Over Time (Hourly)"
562
-
: "Requests Over Time (Daily)";
1100
+
if (chartTitle) {
1101
+
chartTitle.textContent = isHourly
1102
+
? "Requests Over Time (Hourly)"
1103
+
: "Requests Over Time (Daily)";
1104
+
}
563
1105
564
1106
charts.time = new Chart(ctx, {
565
1107
type: "line",
···
569
1111
{
570
1112
label: "Requests",
571
1113
data: data.map((d) => d.count),
572
-
borderColor: "#3498db",
573
-
backgroundColor: "rgba(52, 152, 219, 0.1)",
1114
+
borderColor: "#3b82f6",
1115
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
574
1116
tension: 0.4,
575
1117
fill: true,
1118
+
pointRadius: 4,
1119
+
pointHoverRadius: 6,
1120
+
pointBackgroundColor: "#3b82f6",
1121
+
pointBorderColor: "#ffffff",
1122
+
pointBorderWidth: 2,
576
1123
},
577
1124
],
578
1125
},
579
1126
options: {
580
1127
responsive: true,
1128
+
maintainAspectRatio: false,
1129
+
layout: {
1130
+
padding: {
1131
+
left: 10,
1132
+
right: 10,
1133
+
top: 10,
1134
+
bottom: 50 // Extra space for rotated labels
1135
+
}
1136
+
},
1137
+
animation: {
1138
+
duration: 500,
1139
+
easing: 'easeInOutQuart'
1140
+
},
1141
+
plugins: {
1142
+
tooltip: {
1143
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1144
+
titleColor: 'white',
1145
+
bodyColor: 'white',
1146
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1147
+
borderWidth: 1,
1148
+
callbacks: {
1149
+
title: function(context) {
1150
+
const point = data[context[0].dataIndex];
1151
+
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
1152
+
},
1153
+
label: function(context) {
1154
+
const point = data[context.dataIndex];
1155
+
return [
1156
+
`Requests: ${context.parsed.y.toLocaleString()}`,
1157
+
`Avg Response Time: ${Math.round(point.averageResponseTime || 0)}ms`
1158
+
];
1159
+
}
1160
+
}
1161
+
},
1162
+
legend: {
1163
+
labels: {
1164
+
generateLabels: function(chart) {
1165
+
const total = data.reduce((sum, d) => sum + d.count, 0);
1166
+
const avg = Math.round(data.reduce((sum, d) => sum + (d.averageResponseTime || 0), 0) / data.length);
1167
+
return [{
1168
+
text: `Requests (Total: ${total.toLocaleString()}, Avg RT: ${avg}ms)`,
1169
+
fillStyle: '#3b82f6',
1170
+
strokeStyle: '#3b82f6',
1171
+
lineWidth: 2,
1172
+
pointStyle: 'circle'
1173
+
}];
1174
+
}
1175
+
}
1176
+
}
1177
+
},
581
1178
scales: {
1179
+
x: {
1180
+
title: {
1181
+
display: true,
1182
+
text: isHourly ? 'Hour of Day' : 'Date',
1183
+
font: { weight: 'bold' }
1184
+
},
1185
+
ticks: {
1186
+
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
1187
+
maxRotation: 0, // Don't rotate labels
1188
+
minRotation: 0,
1189
+
callback: function(value, index, values) {
1190
+
const label = this.getLabelForValue(value);
1191
+
// Truncate long labels for better fit
1192
+
if (isHourly) {
1193
+
return label; // Hours are short
1194
+
} else {
1195
+
// For dates, show abbreviated format
1196
+
const date = new Date(label);
1197
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
1198
+
}
1199
+
}
1200
+
},
1201
+
grid: {
1202
+
color: 'rgba(0, 0, 0, 0.05)',
1203
+
},
1204
+
},
582
1205
y: {
1206
+
title: {
1207
+
display: true,
1208
+
text: 'Number of Requests',
1209
+
font: { weight: 'bold' }
1210
+
},
583
1211
beginAtZero: true,
1212
+
grid: {
1213
+
color: 'rgba(0, 0, 0, 0.05)',
1214
+
},
1215
+
ticks: {
1216
+
callback: function(value) {
1217
+
return value.toLocaleString();
1218
+
}
1219
+
}
584
1220
},
585
1221
},
586
1222
},
···
588
1224
}
589
1225
590
1226
function updateEndpointChart(data) {
591
-
const ctx = document.getElementById("endpointChart").getContext("2d");
1227
+
const canvas = document.getElementById("endpointChart");
1228
+
if (!canvas) {
1229
+
console.warn('endpointChart canvas not found');
1230
+
return;
1231
+
}
1232
+
1233
+
const ctx = canvas.getContext("2d");
1234
+
if (!ctx) {
1235
+
console.warn('Could not get 2d context for endpointChart');
1236
+
return;
1237
+
}
592
1238
593
1239
if (charts.endpoint) charts.endpoint.destroy();
594
1240
···
600
1246
{
601
1247
label: "Requests",
602
1248
data: data.map((d) => d.count),
603
-
backgroundColor: "#2ecc71",
1249
+
backgroundColor: "#10b981",
1250
+
borderRadius: 4,
604
1251
},
605
1252
],
606
1253
},
607
1254
options: {
608
1255
responsive: true,
609
-
indexAxis: "y",
1256
+
maintainAspectRatio: false,
1257
+
layout: {
1258
+
padding: {
1259
+
left: 10,
1260
+
right: 10,
1261
+
top: 10,
1262
+
bottom: 60 // Extra space for labels
1263
+
}
1264
+
},
1265
+
animation: {
1266
+
duration: 500,
1267
+
easing: 'easeInOutQuart'
1268
+
},
1269
+
indexAxis: "x",
610
1270
scales: {
611
1271
x: {
1272
+
title: {
1273
+
display: true,
1274
+
text: 'Endpoints',
1275
+
font: { weight: 'bold' }
1276
+
},
1277
+
ticks: {
1278
+
maxRotation: 0, // Don't rotate
1279
+
minRotation: 0,
1280
+
callback: function(value, index, values) {
1281
+
const label = this.getLabelForValue(value);
1282
+
// Truncate long labels but show full in tooltip
1283
+
return label.length > 12 ? label.substring(0, 9) + '...' : label;
1284
+
}
1285
+
},
1286
+
grid: {
1287
+
color: 'rgba(0, 0, 0, 0.05)',
1288
+
},
1289
+
},
1290
+
y: {
1291
+
title: {
1292
+
display: true,
1293
+
text: 'Number of Requests',
1294
+
font: { weight: 'bold' }
1295
+
},
612
1296
beginAtZero: true,
1297
+
grid: {
1298
+
color: 'rgba(0, 0, 0, 0.05)',
1299
+
},
1300
+
ticks: {
1301
+
callback: function(value) {
1302
+
return value.toLocaleString();
1303
+
}
1304
+
}
613
1305
},
614
1306
},
1307
+
plugins: {
1308
+
tooltip: {
1309
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1310
+
titleColor: 'white',
1311
+
bodyColor: 'white',
1312
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1313
+
borderWidth: 1,
1314
+
callbacks: {
1315
+
title: function(context) {
1316
+
return context[0].label; // Show full label in tooltip
1317
+
},
1318
+
label: function(context) {
1319
+
return `Requests: ${context.parsed.y.toLocaleString()}`;
1320
+
}
1321
+
}
1322
+
},
1323
+
legend: {
1324
+
labels: {
1325
+
generateLabels: function(chart) {
1326
+
const total = data.reduce((sum, d) => sum + d.count, 0);
1327
+
return [{
1328
+
text: `Total Requests: ${total.toLocaleString()}`,
1329
+
fillStyle: '#10b981',
1330
+
strokeStyle: '#10b981',
1331
+
lineWidth: 2,
1332
+
pointStyle: 'rect'
1333
+
}];
1334
+
}
1335
+
}
1336
+
}
1337
+
},
615
1338
},
616
1339
});
617
1340
}
···
622
1345
if (charts.status) charts.status.destroy();
623
1346
624
1347
const colors = data.map((d) => {
625
-
if (d.status >= 200 && d.status < 300) return "#2ecc71";
626
-
if (d.status >= 300 && d.status < 400) return "#f39c12";
627
-
if (d.status >= 400 && d.status < 500) return "#e74c3c";
628
-
return "#9b59b6";
1348
+
if (d.status >= 200 && d.status < 300) return "#10b981";
1349
+
if (d.status >= 300 && d.status < 400) return "#f59e0b";
1350
+
if (d.status >= 400 && d.status < 500) return "#ef4444";
1351
+
return "#8b5cf6";
629
1352
});
630
1353
631
1354
charts.status = new Chart(ctx, {
···
636
1359
{
637
1360
data: data.map((d) => d.count),
638
1361
backgroundColor: colors,
1362
+
borderWidth: 2,
1363
+
borderColor: '#fff'
639
1364
},
640
1365
],
641
1366
},
642
1367
options: {
643
1368
responsive: true,
1369
+
animation: {
1370
+
duration: 500,
1371
+
easing: 'easeInOutQuart'
1372
+
},
644
1373
},
645
1374
});
646
1375
}
···
658
1387
{
659
1388
data: data.map((d) => d.count),
660
1389
backgroundColor: [
661
-
"#3498db",
662
-
"#e74c3c",
663
-
"#2ecc71",
664
-
"#f39c12",
665
-
"#9b59b6",
666
-
"#34495e",
667
-
"#16a085",
668
-
"#8e44ad",
669
-
"#d35400",
670
-
"#7f8c8d",
1390
+
"#3b82f6",
1391
+
"#ef4444",
1392
+
"#10b981",
1393
+
"#f59e0b",
1394
+
"#8b5cf6",
1395
+
"#6b7280",
1396
+
"#06b6d4",
1397
+
"#84cc16",
1398
+
"#f97316",
1399
+
"#64748b",
671
1400
],
1401
+
borderWidth: 2,
1402
+
borderColor: '#fff'
672
1403
},
673
1404
],
674
1405
},
675
1406
options: {
676
1407
responsive: true,
1408
+
animation: {
1409
+
duration: 500,
1410
+
easing: 'easeInOutQuart'
1411
+
},
677
1412
},
678
1413
});
679
1414
}
680
1415
681
1416
function updateLatencyTimeChart(data, isHourly) {
682
-
const ctx = document
683
-
.getElementById("latencyTimeChart")
684
-
.getContext("2d");
1417
+
const ctx = document.getElementById("latencyTimeChart").getContext("2d");
685
1418
686
1419
if (charts.latencyTime) charts.latencyTime.destroy();
687
1420
688
-
// Update chart title
689
1421
const chartTitle = document
690
1422
.querySelector("#latencyTimeChart")
691
1423
.parentElement.querySelector(".chart-title");
692
-
chartTitle.textContent = isHourly
693
-
? "Latency Over Time (Hourly)"
694
-
: "Latency Over Time (Daily)";
1424
+
if (chartTitle) {
1425
+
chartTitle.textContent = isHourly
1426
+
? "Latency Over Time (Hourly)"
1427
+
: "Latency Over Time (Daily)";
1428
+
}
695
1429
696
1430
charts.latencyTime = new Chart(ctx, {
697
1431
type: "line",
···
701
1435
{
702
1436
label: "Average Response Time",
703
1437
data: data.map((d) => d.averageResponseTime),
704
-
borderColor: "#3498db",
705
-
backgroundColor: "rgba(52, 152, 219, 0.1)",
1438
+
borderColor: "#3b82f6",
1439
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
706
1440
tension: 0.4,
707
1441
yAxisID: "y",
1442
+
pointRadius: 4,
1443
+
pointHoverRadius: 6,
1444
+
pointBackgroundColor: "#3b82f6",
1445
+
pointBorderColor: "#ffffff",
1446
+
pointBorderWidth: 2,
708
1447
},
709
1448
{
710
1449
label: "P95 Response Time",
711
1450
data: data.map((d) => d.p95),
712
-
borderColor: "#e74c3c",
713
-
backgroundColor: "rgba(231, 76, 60, 0.1)",
1451
+
borderColor: "#ef4444",
1452
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
714
1453
tension: 0.4,
715
1454
yAxisID: "y",
1455
+
pointRadius: 4,
1456
+
pointHoverRadius: 6,
1457
+
pointBackgroundColor: "#ef4444",
1458
+
pointBorderColor: "#ffffff",
1459
+
pointBorderWidth: 2,
716
1460
},
717
1461
],
718
1462
},
719
1463
options: {
720
1464
responsive: true,
1465
+
maintainAspectRatio: false,
1466
+
layout: {
1467
+
padding: {
1468
+
left: 10,
1469
+
right: 10,
1470
+
top: 10,
1471
+
bottom: 50
1472
+
}
1473
+
},
1474
+
animation: {
1475
+
duration: 500,
1476
+
easing: 'easeInOutQuart'
1477
+
},
1478
+
plugins: {
1479
+
tooltip: {
1480
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1481
+
titleColor: 'white',
1482
+
bodyColor: 'white',
1483
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1484
+
borderWidth: 1,
1485
+
callbacks: {
1486
+
title: function(context) {
1487
+
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
1488
+
},
1489
+
afterBody: function(context) {
1490
+
const point = data[context[0].dataIndex];
1491
+
return [
1492
+
`Request Count: ${point.count.toLocaleString()}`,
1493
+
`Performance: ${point.averageResponseTime < 100 ? 'Excellent' : point.averageResponseTime < 300 ? 'Good' : 'Needs Attention'}`
1494
+
];
1495
+
}
1496
+
}
1497
+
},
1498
+
legend: {
1499
+
labels: {
1500
+
generateLabels: function(chart) {
1501
+
const avgAvg = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
1502
+
const avgP95 = Math.round(data.reduce((sum, d) => sum + (d.p95 || 0), 0) / data.length);
1503
+
return [
1504
+
{
1505
+
text: `Average Response Time (Overall: ${avgAvg}ms)`,
1506
+
fillStyle: '#3b82f6',
1507
+
strokeStyle: '#3b82f6',
1508
+
lineWidth: 2,
1509
+
pointStyle: 'circle'
1510
+
},
1511
+
{
1512
+
text: `P95 Response Time (Overall: ${avgP95}ms)`,
1513
+
fillStyle: '#ef4444',
1514
+
strokeStyle: '#ef4444',
1515
+
lineWidth: 2,
1516
+
pointStyle: 'circle'
1517
+
}
1518
+
];
1519
+
}
1520
+
}
1521
+
}
1522
+
},
721
1523
scales: {
1524
+
x: {
1525
+
title: {
1526
+
display: true,
1527
+
text: isHourly ? 'Hour of Day' : 'Date',
1528
+
font: { weight: 'bold' }
1529
+
},
1530
+
ticks: {
1531
+
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
1532
+
maxRotation: 0,
1533
+
minRotation: 0,
1534
+
callback: function(value, index, values) {
1535
+
const label = this.getLabelForValue(value);
1536
+
if (isHourly) {
1537
+
return label;
1538
+
} else {
1539
+
const date = new Date(label);
1540
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
1541
+
}
1542
+
}
1543
+
},
1544
+
grid: {
1545
+
color: 'rgba(0, 0, 0, 0.05)',
1546
+
},
1547
+
},
722
1548
y: {
723
-
beginAtZero: true,
724
1549
title: {
725
1550
display: true,
726
1551
text: "Response Time (ms)",
1552
+
font: { weight: 'bold' }
727
1553
},
1554
+
beginAtZero: true,
1555
+
grid: {
1556
+
color: 'rgba(0, 0, 0, 0.05)',
1557
+
},
1558
+
ticks: {
1559
+
callback: function(value) {
1560
+
return Math.round(value) + 'ms';
1561
+
}
1562
+
}
728
1563
},
729
1564
},
730
1565
},
···
732
1567
}
733
1568
734
1569
function updateLatencyDistributionChart(data) {
735
-
const ctx = document
736
-
.getElementById("latencyDistributionChart")
737
-
.getContext("2d");
1570
+
const ctx = document.getElementById("latencyDistributionChart").getContext("2d");
738
1571
739
1572
if (charts.latencyDistribution) charts.latencyDistribution.destroy();
740
1573
···
746
1579
{
747
1580
label: "Requests",
748
1581
data: data.map((d) => d.count),
749
-
backgroundColor: "#2ecc71",
1582
+
backgroundColor: "#10b981",
1583
+
borderRadius: 4,
750
1584
},
751
1585
],
752
1586
},
753
1587
options: {
754
1588
responsive: true,
1589
+
animation: {
1590
+
duration: 500,
1591
+
easing: 'easeInOutQuart'
1592
+
},
755
1593
scales: {
756
1594
y: {
757
1595
beginAtZero: true,
···
762
1600
}
763
1601
764
1602
function updatePercentilesChart(percentiles) {
765
-
const ctx = document
766
-
.getElementById("percentilesChart")
767
-
.getContext("2d");
1603
+
const ctx = document.getElementById("percentilesChart").getContext("2d");
768
1604
769
1605
if (charts.percentiles) charts.percentiles.destroy();
770
1606
771
1607
const data = [
772
-
{ label: "P50", value: percentiles.p50 },
1608
+
{ label: "P50 (Median)", value: percentiles.p50 },
773
1609
{ label: "P75", value: percentiles.p75 },
774
1610
{ label: "P90", value: percentiles.p90 },
775
1611
{ label: "P95", value: percentiles.p95 },
···
785
1621
label: "Response Time (ms)",
786
1622
data: data.map((d) => d.value),
787
1623
backgroundColor: [
788
-
"#3498db",
789
-
"#2ecc71",
790
-
"#f39c12",
791
-
"#e74c3c",
792
-
"#9b59b6",
1624
+
"#10b981", // P50 - Green (good)
1625
+
"#3b82f6", // P75 - Blue
1626
+
"#f59e0b", // P90 - Yellow (warning)
1627
+
"#ef4444", // P95 - Red (concerning)
1628
+
"#8b5cf6", // P99 - Purple (critical)
793
1629
],
1630
+
borderRadius: 4,
1631
+
borderWidth: 2,
1632
+
borderColor: '#ffffff',
794
1633
},
795
1634
],
796
1635
},
797
1636
options: {
798
1637
responsive: true,
1638
+
maintainAspectRatio: false,
1639
+
animation: {
1640
+
duration: 500,
1641
+
easing: 'easeInOutQuart'
1642
+
},
1643
+
plugins: {
1644
+
tooltip: {
1645
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1646
+
titleColor: 'white',
1647
+
bodyColor: 'white',
1648
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1649
+
borderWidth: 1,
1650
+
callbacks: {
1651
+
label: function(context) {
1652
+
const percentile = context.label;
1653
+
const value = Math.round(context.parsed.y);
1654
+
let interpretation = '';
1655
+
1656
+
if (percentile.includes('P50')) {
1657
+
interpretation = '50% of requests are faster than this';
1658
+
} else if (percentile.includes('P95')) {
1659
+
interpretation = '95% of requests are faster than this';
1660
+
} else if (percentile.includes('P99')) {
1661
+
interpretation = '99% of requests are faster than this';
1662
+
}
1663
+
1664
+
return [
1665
+
`${percentile}: ${value}ms`,
1666
+
interpretation
1667
+
];
1668
+
}
1669
+
}
1670
+
},
1671
+
legend: {
1672
+
display: false // Hide legend since colors are self-explanatory
1673
+
}
1674
+
},
799
1675
scales: {
1676
+
x: {
1677
+
title: {
1678
+
display: true,
1679
+
text: 'Response Time Percentiles',
1680
+
font: { weight: 'bold' }
1681
+
},
1682
+
grid: {
1683
+
color: 'rgba(0, 0, 0, 0.05)',
1684
+
},
1685
+
},
800
1686
y: {
1687
+
title: {
1688
+
display: true,
1689
+
text: 'Response Time (ms)',
1690
+
font: { weight: 'bold' }
1691
+
},
801
1692
beginAtZero: true,
1693
+
grid: {
1694
+
color: 'rgba(0, 0, 0, 0.05)',
1695
+
},
1696
+
ticks: {
1697
+
callback: function(value) {
1698
+
return Math.round(value) + 'ms';
1699
+
}
1700
+
}
802
1701
},
803
1702
},
804
1703
},
···
806
1705
}
807
1706
808
1707
function updateSlowestEndpointsChart(data) {
809
-
const ctx = document
810
-
.getElementById("slowestEndpointsChart")
811
-
.getContext("2d");
1708
+
const ctx = document.getElementById("slowestEndpointsChart").getContext("2d");
812
1709
813
1710
if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
814
1711
···
820
1717
{
821
1718
label: "Avg Response Time (ms)",
822
1719
data: data.map((d) => d.averageResponseTime),
823
-
backgroundColor: "#e74c3c",
1720
+
backgroundColor: "#ef4444",
1721
+
borderRadius: 4,
824
1722
},
825
1723
],
826
1724
},
827
1725
options: {
828
1726
responsive: true,
829
-
indexAxis: "y",
1727
+
maintainAspectRatio: false,
1728
+
animation: {
1729
+
duration: 500,
1730
+
easing: 'easeInOutQuart'
1731
+
},
1732
+
indexAxis: "x", // Changed from "y" to "x" to put labels on top
830
1733
scales: {
831
1734
x: {
1735
+
title: {
1736
+
display: true,
1737
+
text: 'Endpoints',
1738
+
font: { weight: 'bold' }
1739
+
},
1740
+
ticks: {
1741
+
maxRotation: 45,
1742
+
minRotation: 45,
1743
+
callback: function(value, index, values) {
1744
+
const label = this.getLabelForValue(value);
1745
+
return label.length > 15 ? label.substring(0, 12) + '...' : label;
1746
+
}
1747
+
},
1748
+
grid: {
1749
+
color: 'rgba(0, 0, 0, 0.05)',
1750
+
},
1751
+
},
1752
+
y: {
1753
+
title: {
1754
+
display: true,
1755
+
text: 'Response Time (ms)',
1756
+
font: { weight: 'bold' }
1757
+
},
832
1758
beginAtZero: true,
1759
+
grid: {
1760
+
color: 'rgba(0, 0, 0, 0.05)',
1761
+
},
1762
+
ticks: {
1763
+
callback: function(value) {
1764
+
return Math.round(value) + 'ms';
1765
+
}
1766
+
}
833
1767
},
834
1768
},
1769
+
plugins: {
1770
+
tooltip: {
1771
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1772
+
titleColor: 'white',
1773
+
bodyColor: 'white',
1774
+
borderColor: 'rgba(255, 255, 255, 0.1)',
1775
+
borderWidth: 1,
1776
+
callbacks: {
1777
+
title: function(context) {
1778
+
return context[0].label; // Show full label in tooltip
1779
+
},
1780
+
label: function(context) {
1781
+
const point = data[context.dataIndex];
1782
+
return [
1783
+
`Avg Response Time: ${Math.round(context.parsed.y)}ms`,
1784
+
`Request Count: ${point.count.toLocaleString()}`
1785
+
];
1786
+
}
1787
+
}
1788
+
},
1789
+
legend: {
1790
+
labels: {
1791
+
generateLabels: function(chart) {
1792
+
const avgTime = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
1793
+
return [{
1794
+
text: `Average Response Time: ${avgTime}ms`,
1795
+
fillStyle: '#ef4444',
1796
+
strokeStyle: '#ef4444',
1797
+
lineWidth: 2,
1798
+
pointStyle: 'rect'
1799
+
}];
1800
+
}
1801
+
}
1802
+
}
1803
+
},
835
1804
},
836
1805
});
837
1806
}
838
1807
839
-
document
840
-
.getElementById("autoRefresh")
841
-
.addEventListener("change", function () {
842
-
if (this.checked) {
843
-
autoRefreshInterval = setInterval(loadData, 30000);
844
-
} else {
845
-
clearInterval(autoRefreshInterval);
1808
+
document.getElementById("autoRefresh").addEventListener("change", function () {
1809
+
if (this.checked) {
1810
+
autoRefreshInterval = setInterval(loadData, 30000);
1811
+
showToast('Auto-refresh enabled', 'success');
1812
+
} else {
1813
+
clearInterval(autoRefreshInterval);
1814
+
showToast('Auto-refresh disabled', 'info');
1815
+
}
1816
+
});
1817
+
1818
+
// Days selector change handler
1819
+
document.getElementById("daysSelect").addEventListener("change", function() {
1820
+
// Reset visible charts when changing time period
1821
+
visibleCharts.clear();
1822
+
loadData();
1823
+
});
1824
+
1825
+
// Initialize dashboard
1826
+
document.addEventListener('DOMContentLoaded', function() {
1827
+
initLazyLoading();
1828
+
loadData();
1829
+
});
1830
+
1831
+
// Cleanup on page unload
1832
+
window.addEventListener('beforeunload', function() {
1833
+
if (intersectionObserver) {
1834
+
intersectionObserver.disconnect();
1835
+
}
1836
+
clearInterval(autoRefreshInterval);
1837
+
1838
+
// Destroy all charts to prevent memory leaks
1839
+
Object.values(charts).forEach(chart => {
1840
+
if (chart && typeof chart.destroy === 'function') {
1841
+
chart.destroy();
846
1842
}
847
1843
});
848
-
849
-
loadData();
850
-
document
851
-
.getElementById("daysSelect")
852
-
.addEventListener("change", loadData);
1844
+
});
853
1845
</script>
854
1846
</body>
855
1847
</html>