···
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
19
-
background: #f8fafc;
19
+
background: #f9fafb;
26
+
padding: 1.5rem 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
30
-
justify-content: space-between;
31
-
align-items: center;
32
-
border-bottom: 1px solid #e2e8f0;
29
+
border-bottom: 1px solid #e5e7eb;
34
+
font-size: 1.875rem;
36
+
margin-bottom: 0.5rem;
text-decoration: underline;
···
72
-
border-color: #3b82f6;
78
+
border-color: #6366f1;
74
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
80
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
78
-
background: #3b82f6;
84
+
background: #6366f1;
84
-
background: #2563eb;
85
-
transform: translateY(-1px);
90
+
background: #4f46e5;
.controls button:disabled {
···
102
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
104
-
margin-bottom: 2rem;
106
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
108
+
margin-bottom: 3rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
113
-
border: 1px solid #e2e8f0;
117
+
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
115
-
position: relative;
···
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
128
-
.stat-card.loading {
132
-
.stat-card.loading::after {
134
-
position: absolute;
139
-
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
140
-
animation: shimmer 1.5s infinite;
143
-
@keyframes shimmer {
144
-
0% { left: -100%; }
145
-
100% { left: 100%; }
151
-
margin-bottom: 0.25rem;
152
-
word-break: break-word;
153
-
overflow-wrap: break-word;
156
-
/* Responsive font sizing using clamp() */
157
-
font-size: clamp(1.25rem, 4vw, 2.5rem);
133
+
margin-bottom: 0.5rem;
162
-
margin-top: 0.5rem;
166
-
/* Responsive font sizing for labels */
167
-
font-size: clamp(0.75rem, 2vw, 0.875rem);
141
+
font-size: 0.875rem;
142
+
text-transform: uppercase;
143
+
letter-spacing: 0.05em;
170
-
/* Container query support for modern browsers */
171
-
@supports (container-type: inline-size) {
173
-
container-type: inline-size;
176
-
@container (max-width: 250px) {
178
-
font-size: 1.25rem;
181
-
font-size: 0.75rem;
185
-
@container (min-width: 300px) {
190
-
font-size: 0.875rem;
194
-
@container (min-width: 400px) {
148
+
grid-template-columns: 1fr;
150
+
margin-bottom: 3rem;
206
-
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
155
+
grid-template-columns: 1fr 1fr;
208
-
margin-bottom: 2rem;
211
-
@media (max-width: 480px) {
159
+
@media (max-width: 768px) {
grid-template-columns: 1fr;
221
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
165
+
grid-template-columns: 1fr;
230
-
font-size: clamp(0.9rem, 4vw, 1.5rem) !important;
234
-
font-size: clamp(0.65rem, 2.5vw, 0.75rem) !important;
···
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
243
-
border: 1px solid #e2e8f0;
182
+
border: 1px solid #e5e7eb;
184
+
padding-bottom: 5rem;
188
+
font-size: 1.25rem;
189
+
margin-bottom: 1.5rem;
194
+
.user-agents-table {
197
+
border-radius: 12px;
198
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
199
+
border: 1px solid #e5e7eb;
202
+
.search-container {
203
+
margin-bottom: 1.5rem;
249
-
flex-direction: column;
252
-
.chart-container.loading {
254
-
align-items: center;
255
-
justify-content: center;
209
+
padding: 0.75rem 1rem;
210
+
border: 1px solid #d1d5db;
211
+
border-radius: 8px;
212
+
font-size: 0.875rem;
213
+
background: #f9fafb;
214
+
transition: border-color 0.2s ease;
217
+
.search-input:focus {
219
+
border-color: #6366f1;
221
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
258
-
.chart-container.loading::before {
259
-
content: 'Loading chart...';
226
+
border-collapse: collapse;
265
-
font-size: 1.125rem;
266
-
margin-bottom: 1rem;
232
+
padding: 0.75rem 1rem;
233
+
background: #f9fafb;
234
+
border-bottom: 2px solid #e5e7eb;
242
+
padding: 0.75rem 1rem;
243
+
border-bottom: 1px solid #f3f4f6;
244
+
vertical-align: top;
247
+
.ua-table tbody tr:hover {
248
+
background: #f9fafb;
274
-
overflow-wrap: break-word;
277
-
.chart-title-with-indicator {
279
-
align-items: center;
280
-
justify-content: space-between;
283
-
margin-bottom: 1rem;
260
+
font-family: monospace;
261
+
font-size: 0.75rem;
263
+
margin-top: 0.25rem;
265
+
word-break: break-all;
286
-
.chart-title-with-indicator .chart-title {
273
+
white-space: nowrap;
279
+
font-size: 0.75rem;
296
-
font-size: 0.875rem;
286
+
font-style: italic;
309
-
border: 3px solid #e2e8f0;
299
+
border: 3px solid #e5e7eb;
311
-
border-top-color: #3b82f6;
301
+
border-top-color: #6366f1;
animation: spin 1s ease-in-out infinite;
···
.auto-refresh input[type="checkbox"] {
339
-
accent-color: #3b82f6;
342
-
.performance-indicator {
343
-
display: inline-flex;
344
-
align-items: center;
346
-
font-size: 0.75rem;
350
-
.performance-indicator.good { color: #059669; }
351
-
.performance-indicator.warning { color: #d97706; }
352
-
.performance-indicator.error { color: #dc2626; }
357
-
align-items: center;
358
-
justify-content: center;
359
-
background: #f8fafc;
360
-
border-radius: 8px;
364
-
.lazy-chart.visible {
365
-
background: transparent;
368
-
@media (max-width: 768px) {
370
-
grid-template-columns: 1fr;
378
-
flex-direction: column;
380
-
text-align: center;
385
-
flex-direction: column;
386
-
align-items: stretch;
395
-
font-size: clamp(1rem, 5vw, 1.75rem) !important;
399
-
font-size: clamp(0.7rem, 3vw, 0.8rem) !important;
409
-
flex-direction: column;
410
-
align-items: flex-start;
418
-
background: #1f2937;
420
-
padding: 0.75rem 1rem;
421
-
border-radius: 8px;
422
-
font-size: 0.875rem;
424
-
transform: translateX(100%);
425
-
transition: transform 0.3s ease;
429
-
transform: translateX(0);
433
-
background: #059669;
437
-
background: #dc2626;
329
+
accent-color: #6366f1;
···
<div id="error" class="error" style="display: none"></div>
<div id="content" style="display: none">
472
-
<!-- Key Metrics Overview -->
473
-
<div class="chart-container" style="margin-bottom: 2rem; height: 450px">
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>
479
-
id="trafficOverviewChart"
480
-
style="padding-bottom: 2rem"
484
-
<!-- Stats Grid -->
364
+
<!-- Key Metrics -->
486
-
<div class="stat-card" id="totalRequestsCard">
366
+
<div class="stat-card">
<div class="stat-number" id="totalRequests">-</div>
<div class="stat-label">Total Requests</div>
490
-
<div class="stat-card" id="avgResponseTimeCard">
491
-
<div class="stat-number" id="avgResponseTime">-</div>
492
-
<div class="stat-label">Avg Response Time (ms)</div>
494
-
<div class="stat-card" id="p95ResponseTimeCard">
495
-
<div class="stat-number" id="p95ResponseTime">-</div>
496
-
<div class="stat-label">P95 Response Time (ms)</div>
498
-
<div class="stat-card" id="uniqueEndpointsCard">
499
-
<div class="stat-number" id="uniqueEndpoints">-</div>
500
-
<div class="stat-label">Unique Endpoints</div>
502
-
<div class="stat-card" id="errorRateCard">
503
-
<div class="stat-number" id="errorRate">-</div>
504
-
<div class="stat-label">Error Rate (%)</div>
506
-
<div class="stat-card" id="fastRequestsCard">
507
-
<div class="stat-number" id="fastRequests">-</div>
508
-
<div class="stat-label">Fast Requests (<100ms)</div>
510
-
<div class="stat-card" id="uptimeCard">
370
+
<div class="stat-card">
<div class="stat-number" id="uptime">-</div>
512
-
<div class="stat-label">Uptime (%)</div>
372
+
<div class="stat-label">Uptime</div>
514
-
<div class="stat-card" id="throughputCard">
515
-
<div class="stat-number" id="throughput">-</div>
516
-
<div class="stat-label">Throughput (req/hr)</div>
518
-
<div class="stat-card" id="apdexCard">
519
-
<div class="stat-number" id="apdex">-</div>
520
-
<div class="stat-label">APDEX Score</div>
522
-
<div class="stat-card" id="cacheHitRateCard">
523
-
<div class="stat-number" id="cacheHitRate">-</div>
524
-
<div class="stat-label">Cache Hit Rate (%)</div>
374
+
<div class="stat-card">
375
+
<div class="stat-number" id="avgResponseTime">-</div>
376
+
<div class="stat-label">Avg Response Time</div>
528
-
<!-- Peak Traffic Stats -->
529
-
<div class="stats-grid">
530
-
<div class="stat-card" id="peakHourCard">
531
-
<div class="stat-number" id="peakHour">-</div>
532
-
<div class="stat-label">Peak Hour</div>
534
-
<div class="stat-card" id="peakHourRequestsCard">
535
-
<div class="stat-number" id="peakHourRequests">-</div>
536
-
<div class="stat-label">Peak Hour Requests</div>
538
-
<div class="stat-card" id="peakDayCard">
539
-
<div class="stat-number" id="peakDay">-</div>
540
-
<div class="stat-label">Peak Day</div>
542
-
<div class="stat-card" id="peakDayRequestsCard">
543
-
<div class="stat-number" id="peakDayRequests">-</div>
544
-
<div class="stat-label">Peak Day Requests</div>
546
-
<div class="stat-card" id="dashboardRequestsCard">
547
-
<div class="stat-number" id="dashboardRequests">-</div>
548
-
<div class="stat-label">Dashboard Requests</div>
552
-
<!-- Charts Grid with Lazy Loading -->
380
+
<!-- Main Charts -->
<div class="charts-grid">
554
-
<div class="chart-container lazy-chart" data-chart="timeChart">
555
-
<div class="chart-title">Requests Over Time</div>
556
-
<canvas id="timeChart"></canvas>
559
-
<div class="chart-container lazy-chart" data-chart="latencyTimeChart">
560
-
<div class="chart-title">Latency Over Time (Hourly)</div>
561
-
<canvas id="latencyTimeChart"></canvas>
564
-
<div class="chart-container lazy-chart" data-chart="latencyDistributionChart">
565
-
<div class="chart-title">Response Time Distribution</div>
566
-
<canvas id="latencyDistributionChart"></canvas>
569
-
<div class="chart-container lazy-chart" data-chart="percentilesChart">
570
-
<div class="chart-title">Latency Percentiles</div>
571
-
<canvas id="percentilesChart"></canvas>
574
-
<div class="chart-container lazy-chart" data-chart="endpointChart">
575
-
<div class="chart-title">Top Endpoints</div>
576
-
<canvas id="endpointChart"></canvas>
579
-
<div class="chart-container lazy-chart" data-chart="slowestEndpointsChart">
580
-
<div class="chart-title">Slowest Endpoints</div>
581
-
<canvas id="slowestEndpointsChart"></canvas>
382
+
<div class="charts-row">
383
+
<div class="chart-container">
384
+
<div class="chart-title">Requests Over Time</div>
385
+
<canvas id="requestsChart"></canvas>
387
+
<div class="chart-container">
388
+
<div class="chart-title">Latency Over Time</div>
389
+
<canvas id="latencyChart"></canvas>
584
-
<div class="chart-container lazy-chart" data-chart="statusChart">
585
-
<div class="chart-title">Status Codes</div>
586
-
<canvas id="statusChart"></canvas>
394
+
<!-- User Agents Table -->
395
+
<div class="user-agents-table">
396
+
<div class="chart-title">User Agents</div>
397
+
<div class="search-container">
398
+
<input type="text" id="userAgentSearch" class="search-input" placeholder="Search user agents...">
589
-
<div class="chart-container lazy-chart" data-chart="userAgentChart">
590
-
<div class="chart-title">Top User Agents</div>
591
-
<canvas id="userAgentChart"></canvas>
400
+
<div id="userAgentsTable">
401
+
<div class="loading">Loading user agents...</div>
···
602
-
let visibleCharts = new Set();
603
-
let intersectionObserver;
605
-
// Performance monitoring
606
-
const performance = {
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;
620
-
if (!visibleCharts.has(chartType) && currentData) {
621
-
visibleCharts.add(chartType);
622
-
chartContainer.classList.add('visible');
623
-
loadChart(chartType, currentData);
628
-
rootMargin: '50px',
632
-
// Observe all lazy chart containers
633
-
document.querySelectorAll('.lazy-chart').forEach(container => {
634
-
intersectionObserver.observe(container);
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);
645
-
setTimeout(() => toast.classList.add('show'), 100);
647
-
toast.classList.remove('show');
648
-
setTimeout(() => document.body.removeChild(toast), 300);
652
-
// Update loading states for stat cards
653
-
function setStatCardLoading(cardId, loading) {
654
-
const card = document.getElementById(cardId);
657
-
card.classList.add('loading');
659
-
card.classList.remove('loading');
412
+
let currentRequestId = 0;
413
+
let abortController = null;
// Debounced resize handler for charts
···
window.addEventListener('resize', handleResize);
async function loadData() {
680
-
if (isLoading) return;
431
+
// Cancel any existing requests
432
+
if (abortController) {
433
+
abortController.abort();
436
+
// Create new abort controller for this request
437
+
abortController = new AbortController();
438
+
const requestId = ++currentRequestId;
439
+
const signal = abortController.signal;
683
-
performance.startTime = Date.now();
442
+
const startTime = Date.now();
444
+
// Capture the days value at the start to ensure consistency
const days = document.getElementById("daysSelect").value;
const loading = document.getElementById("loading");
const error = document.getElementById("error");
const content = document.getElementById("content");
const refreshBtn = document.getElementById("refreshBtn");
451
+
console.log(`Starting request ${requestId} for ${days} days`);
loading.style.display = "block";
···
refreshBtn.disabled = true;
refreshBtn.textContent = "Loading...";
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'
706
-
statCards.forEach(cardId => setStatCardLoading(cardId, true));
709
-
const response = await fetch(`/stats?days=${days}`);
710
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
461
+
// Step 1: Load essential stats first (fastest)
462
+
console.log(`[${requestId}] Loading essential stats...`);
463
+
const essentialResponse = await fetch(`/api/stats/essential?days=${days}`, { signal });
712
-
const data = await response.json();
713
-
currentData = data;
715
-
performance.endTime = Date.now();
716
-
performance.loadTime = performance.endTime - performance.startTime;
718
-
updateDashboard(data);
465
+
// Check if this request is still current
466
+
if (requestId !== currentRequestId) {
467
+
console.log(`[${requestId}] Request cancelled (essential stats)`);
471
+
if (!essentialResponse.ok) throw new Error(`HTTP ${essentialResponse.status}`);
473
+
const essentialData = await essentialResponse.json();
475
+
// Double-check we're still the current request
476
+
if (requestId !== currentRequestId) {
477
+
console.log(`[${requestId}] Request cancelled (essential stats after response)`);
481
+
updateEssentialStats(essentialData);
483
+
// Show content immediately with essential stats
loading.style.display = "none";
content.style.display = "block";
723
-
showToast(`Dashboard updated in ${performance.loadTime}ms`, 'success');
725
-
loading.style.display = "none";
726
-
error.style.display = "block";
727
-
error.textContent = `Failed to load data: ${err.message}`;
728
-
showToast(`Error: ${err.message}`, 'error');
731
-
refreshBtn.disabled = false;
732
-
refreshBtn.textContent = "Refresh";
734
-
// Remove loading state from stat cards
735
-
statCards.forEach(cardId => setStatCardLoading(cardId, false));
486
+
refreshBtn.textContent = "Loading Charts...";
739
-
function updateDashboard(data) {
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);
488
+
console.log(`[${requestId}] Essential stats loaded in ${Date.now() - startTime}ms`);
748
-
const errorRequests = data.requestsByStatus
749
-
.filter((s) => s.status >= 400)
750
-
.reduce((sum, s) => sum + s.count, 0);
751
-
const errorRate = data.totalRequests > 0
752
-
? ((errorRequests / data.totalRequests) * 100).toFixed(1)
754
-
updateStatWithAnimation("errorRate", errorRate);
490
+
// Step 2: Load chart data (medium speed)
491
+
console.log(`[${requestId}] Loading chart data...`);
492
+
const chartResponse = await fetch(`/api/stats/charts?days=${days}`, { signal });
756
-
// Calculate fast requests percentage
757
-
const fastRequestsData = data.latencyAnalytics.distribution
758
-
.filter((d) => d.range === "0-50ms" || d.range === "50-100ms")
759
-
.reduce((sum, d) => sum + d.percentage, 0);
760
-
updateStatWithAnimation("fastRequests", fastRequestsData.toFixed(1) + "%");
494
+
if (requestId !== currentRequestId) {
495
+
console.log(`[${requestId}] Request cancelled (chart data)`);
762
-
// Performance metrics
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));
499
+
if (!chartResponse.ok) throw new Error(`HTTP ${chartResponse.status}`);
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());
501
+
const chartData = await chartResponse.json();
775
-
// Update performance indicator
776
-
updatePerformanceIndicator(data);
503
+
if (requestId !== currentRequestId) {
504
+
console.log(`[${requestId}] Request cancelled (chart data after response)`);
778
-
// Load main traffic overview chart immediately
779
-
const days = parseInt(document.getElementById("daysSelect").value);
780
-
updateTrafficOverviewChart(data.trafficOverview, days);
508
+
updateCharts(chartData, parseInt(days));
509
+
refreshBtn.textContent = "Loading User Agents...";
782
-
// Other charts will be loaded lazily when they come into view
511
+
console.log(`[${requestId}] Charts loaded in ${Date.now() - startTime}ms`);
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';
792
-
element.textContent = value;
793
-
element.style.transform = 'scale(1)';
513
+
// Step 3: Load user agents last (slowest)
514
+
console.log(`[${requestId}] Loading user agents...`);
515
+
const userAgentsResponse = await fetch(`/api/stats/useragents?days=${days}`, { signal });
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;
517
+
if (requestId !== currentRequestId) {
518
+
console.log(`[${requestId}] Request cancelled (user agents)`);
806
-
if (avgResponseTime < 100 && errorRate < 1) {
808
-
text = '🟢 Excellent';
809
-
} else if (avgResponseTime < 300 && errorRate < 5) {
810
-
status = 'warning';
814
-
text = '🔴 Needs Attention';
522
+
if (!userAgentsResponse.ok) throw new Error(`HTTP ${userAgentsResponse.status}`);
524
+
const userAgentsData = await userAgentsResponse.json();
817
-
indicator.className = `performance-indicator ${status}`;
818
-
indicator.textContent = text;
526
+
if (requestId !== currentRequestId) {
527
+
console.log(`[${requestId}] Request cancelled (user agents after response)`);
821
-
// Load individual charts (called by intersection observer)
822
-
function loadChart(chartType, data) {
823
-
const days = parseInt(document.getElementById("daysSelect").value);
824
-
const isHourly = days === 1;
531
+
updateUserAgentsTable(userAgentsData);
827
-
switch(chartType) {
829
-
updateTimeChart(data.requestsByDay, isHourly);
831
-
case 'latencyTimeChart':
832
-
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
834
-
case 'latencyDistributionChart':
835
-
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
837
-
case 'percentilesChart':
838
-
updatePercentilesChart(data.latencyAnalytics.percentiles);
840
-
case 'endpointChart':
841
-
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
843
-
case 'slowestEndpointsChart':
844
-
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
846
-
case 'statusChart':
847
-
updateStatusChart(data.requestsByStatus);
849
-
case 'userAgentChart':
850
-
updateUserAgentChart(data.topUserAgents.slice(0, 5));
533
+
const totalTime = Date.now() - startTime;
534
+
console.log(`[${requestId}] All data loaded in ${totalTime}ms`);
536
+
// Only show error if this is still the current request
537
+
if (requestId === currentRequestId) {
538
+
if (err.name === 'AbortError') {
539
+
console.log(`[${requestId}] Request aborted`);
541
+
loading.style.display = "none";
542
+
error.style.display = "block";
543
+
error.textContent = `Failed to load data: ${err.message}`;
544
+
console.error(`[${requestId}] Error: ${err.message}`);
854
-
console.error(`Error loading chart ${chartType}:`, error);
855
-
const container = document.querySelector(`[data-chart="${chartType}"]`);
857
-
container.innerHTML = `<div class="chart-error">Error loading chart: ${error.message}</div>`;
548
+
// Only update UI if this is still the current request
549
+
if (requestId === currentRequestId) {
551
+
refreshBtn.disabled = false;
552
+
refreshBtn.textContent = "Refresh";
553
+
abortController = null;
862
-
function updateTrafficOverviewChart(data, days) {
863
-
const canvas = document.getElementById("trafficOverviewChart");
865
-
console.warn('trafficOverviewChart canvas not found');
869
-
const ctx = canvas.getContext("2d");
871
-
console.warn('Could not get 2d context for trafficOverviewChart');
558
+
// Update just the essential stats (fast)
559
+
function updateEssentialStats(data) {
560
+
document.getElementById("totalRequests").textContent = data.totalRequests.toLocaleString();
561
+
document.getElementById("uptime").textContent = data.uptime.toFixed(1) + "%";
562
+
document.getElementById("avgResponseTime").textContent =
563
+
data.averageResponseTime ? Math.round(data.averageResponseTime) + "ms" : "N/A";
875
-
if (charts.trafficOverview) {
876
-
charts.trafficOverview.destroy();
566
+
// Update charts (medium speed)
567
+
function updateCharts(data, days) {
568
+
updateRequestsChart(data.requestsByDay, days === 1);
569
+
updateLatencyChart(data.latencyOverTime, days === 1);
879
-
// Update chart title based on granularity
880
-
const chartTitleElement = document.querySelector(".chart-title-with-indicator .chart-title");
881
-
let titleText = "Traffic Overview - All Routes Over Time";
883
-
titleText += " (Hourly)";
884
-
} else if (days <= 7) {
885
-
titleText += " (4-Hour Intervals)";
887
-
titleText += " (Daily)";
889
-
if (chartTitleElement) {
890
-
chartTitleElement.textContent = titleText;
893
-
// Get all unique routes across all time periods
894
-
const allRoutes = new Set();
895
-
data.forEach((timePoint) => {
896
-
Object.keys(timePoint.routes).forEach((route) =>
897
-
allRoutes.add(route),
573
+
// Requests Over Time Chart
574
+
function updateRequestsChart(data, isHourly) {
575
+
const ctx = document.getElementById("requestsChart").getContext("2d");
576
+
const days = parseInt(document.getElementById("daysSelect").value);
901
-
// Define colors for different route types
902
-
const routeColors = {
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",
578
+
if (charts.requests) charts.requests.destroy();
914
-
// Create datasets for each route
915
-
const datasets = Array.from(allRoutes).map((route) => {
916
-
const color = routeColors[route] || "#9ca3af";
919
-
data: data.map((timePoint) => timePoint.routes[route] || 0),
920
-
borderColor: color,
921
-
backgroundColor: color + "20",
925
-
pointHoverRadius: 4,
929
-
// Format labels based on time granularity
930
-
const labels = data.map((timePoint) => {
580
+
// Format labels based on granularity
581
+
const labels = data.map((d) => {
932
-
return timePoint.time.split(" ")[1] || timePoint.time;
583
+
// 15-minute intervals: show just time
584
+
return d.date.split(" ")[1] || d.date;
934
-
const parts = timePoint.time.split(" ");
935
-
const date = parts[0].split("-")[2];
586
+
// Hourly: show date + hour
587
+
const parts = d.date.split(" ");
588
+
const date = parts[0].split("-")[2]; // Get day
const hour = parts[1] || "00:00";
return `${date} ${hour}`;
939
-
return timePoint.time;
592
+
// 4-hour intervals: show abbreviated
593
+
return d.date.split(" ")[0];
943
-
charts.trafficOverview = new Chart(ctx, {
597
+
charts.requests = new Chart(ctx, {
947
-
datasets: datasets,
603
+
data: data.map((d) => d.count),
604
+
borderColor: "#6366f1",
605
+
backgroundColor: "rgba(99, 102, 241, 0.1)",
610
+
pointBackgroundColor: "#6366f1",
maintainAspectRatio: false,
962
-
easing: 'easeInOutQuart'
972
-
usePointStyle: true,
979
-
generateLabels: function(chart) {
980
-
const original = Chart.defaults.plugins.legend.labels.generateLabels;
981
-
const labels = original.call(this, chart);
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()})`;
617
+
legend: { display: false },
997
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
998
-
titleColor: 'white',
999
-
bodyColor: 'white',
1000
-
borderColor: 'rgba(255, 255, 255, 0.1)',
title: function(context) {
1004
-
return `Time: ${context[0].label}`;
621
+
const original = data[context[0].dataIndex];
622
+
if (days === 1) return `Time: ${original.date}`;
623
+
if (days <= 7) return `DateTime: ${original.date}`;
624
+
return `Interval: ${original.date}`;
1006
-
afterBody: function(context) {
1007
-
const timePoint = data[context[0].dataIndex];
1009
-
`Total Requests: ${timePoint.total.toLocaleString()}`,
1010
-
`Peak Route: ${Object.entries(timePoint.routes).sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A'}`
626
+
label: function(context) {
627
+
return `Requests: ${context.parsed.y.toLocaleString()}`;
1021
-
text: days === 1 ? "Hour" : days <= 7 ? "Day & Hour" : "Date",
636
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
638
+
grid: { color: 'rgba(0, 0, 0, 0.05)' },
1027
-
maxTicksLimit: window.innerWidth < 768 ? 8 : 20,
1028
-
maxRotation: 0, // Don't rotate labels
1030
-
callback: function(value, index, values) {
1031
-
const label = this.getLabelForValue(value);
1032
-
// Truncate long labels
1033
-
if (label.length > 8) {
1035
-
return label; // Hours are usually short
1037
-
// For longer periods, abbreviate
1038
-
return label.substring(0, 6) + '...';
1045
-
color: 'rgba(0, 0, 0, 0.05)',
640
+
maxTicksLimit: days === 1 ? 12 : 20,
646
+
title: { display: true, text: 'Requests' },
1059
-
color: 'rgba(0, 0, 0, 0.05)',
1062
-
callback: function(value) {
1063
-
return value.toLocaleString();
648
+
grid: { color: 'rgba(0, 0, 0, 0.05)' }
1082
-
function updateTimeChart(data, isHourly) {
1083
-
const canvas = document.getElementById("timeChart");
1085
-
console.warn('timeChart canvas not found');
1089
-
const ctx = canvas.getContext("2d");
1091
-
console.warn('Could not get 2d context for timeChart');
655
+
// Latency Over Time Chart
656
+
function updateLatencyChart(data, isHourly) {
657
+
const ctx = document.getElementById("latencyChart").getContext("2d");
658
+
const days = parseInt(document.getElementById("daysSelect").value);
1095
-
if (charts.time) charts.time.destroy();
660
+
if (charts.latency) charts.latency.destroy();
1097
-
const chartTitle = document
1098
-
.querySelector("#timeChart")
1099
-
.parentElement.querySelector(".chart-title");
1101
-
chartTitle.textContent = isHourly
1102
-
? "Requests Over Time (Hourly)"
1103
-
: "Requests Over Time (Daily)";
662
+
// Format labels based on granularity
663
+
const labels = data.map((d) => {
665
+
// 15-minute intervals: show just time
666
+
return d.time.split(" ")[1] || d.time;
667
+
} else if (days <= 7) {
668
+
// Hourly: show date + hour
669
+
const parts = d.time.split(" ");
670
+
const date = parts[0].split("-")[2]; // Get day
671
+
const hour = parts[1] || "00:00";
672
+
return `${date} ${hour}`;
674
+
// 4-hour intervals: show abbreviated
675
+
return d.time.split(" ")[0];
1106
-
charts.time = new Chart(ctx, {
679
+
charts.latency = new Chart(ctx, {
1109
-
labels: data.map((d) => (isHourly ? d.date.split(" ")[1] : d.date)),
1112
-
label: "Requests",
1113
-
data: data.map((d) => d.count),
1114
-
borderColor: "#3b82f6",
1115
-
backgroundColor: "rgba(59, 130, 246, 0.1)",
1119
-
pointHoverRadius: 6,
1120
-
pointBackgroundColor: "#3b82f6",
1121
-
pointBorderColor: "#ffffff",
1122
-
pointBorderWidth: 2,
684
+
label: "Average Response Time",
685
+
data: data.map((d) => d.averageResponseTime),
686
+
borderColor: "#10b981",
687
+
backgroundColor: "rgba(16, 185, 129, 0.1)",
692
+
pointBackgroundColor: "#10b981",
maintainAspectRatio: false,
1134
-
bottom: 50 // Extra space for rotated labels
1139
-
easing: 'easeInOutQuart'
699
+
legend: { display: false },
1143
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1144
-
titleColor: 'white',
1145
-
bodyColor: 'white',
1146
-
borderColor: 'rgba(255, 255, 255, 0.1)',
title: function(context) {
1150
-
const point = data[context[0].dataIndex];
1151
-
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
703
+
const original = data[context[0].dataIndex];
704
+
if (days === 1) return `Time: ${original.time}`;
705
+
if (days <= 7) return `DateTime: ${original.time}`;
706
+
return `Interval: ${original.time}`;
label: function(context) {
const point = data[context.dataIndex];
1156
-
`Requests: ${context.parsed.y.toLocaleString()}`,
1157
-
`Avg Response Time: ${Math.round(point.averageResponseTime || 0)}ms`
711
+
`Response Time: ${Math.round(context.parsed.y)}ms`,
712
+
`Request Count: ${point.count.toLocaleString()}`
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);
1168
-
text: `Requests (Total: ${total.toLocaleString()}, Avg RT: ${avg}ms)`,
1169
-
fillStyle: '#3b82f6',
1170
-
strokeStyle: '#3b82f6',
1172
-
pointStyle: 'circle'
1182
-
text: isHourly ? 'Hour of Day' : 'Date',
1183
-
font: { weight: 'bold' }
1186
-
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
1187
-
maxRotation: 0, // Don't rotate labels
1189
-
callback: function(value, index, values) {
1190
-
const label = this.getLabelForValue(value);
1191
-
// Truncate long labels for better fit
1193
-
return label; // Hours are short
1195
-
// For dates, show abbreviated format
1196
-
const date = new Date(label);
1197
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
1202
-
color: 'rgba(0, 0, 0, 0.05)',
1208
-
text: 'Number of Requests',
1209
-
font: { weight: 'bold' }
722
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
1211
-
beginAtZero: true,
1213
-
color: 'rgba(0, 0, 0, 0.05)',
724
+
grid: { color: 'rgba(0, 0, 0, 0.05)' },
1216
-
callback: function(value) {
1217
-
return value.toLocaleString();
726
+
maxTicksLimit: days === 1 ? 12 : 20,
1226
-
function updateEndpointChart(data) {
1227
-
const canvas = document.getElementById("endpointChart");
1229
-
console.warn('endpointChart canvas not found');
1233
-
const ctx = canvas.getContext("2d");
1235
-
console.warn('Could not get 2d context for endpointChart');
1239
-
if (charts.endpoint) charts.endpoint.destroy();
1241
-
charts.endpoint = new Chart(ctx, {
1244
-
labels: data.map((d) => d.endpoint),
1247
-
label: "Requests",
1248
-
data: data.map((d) => d.count),
1249
-
backgroundColor: "#10b981",
1256
-
maintainAspectRatio: false,
1262
-
bottom: 60 // Extra space for labels
1267
-
easing: 'easeInOutQuart'
1274
-
text: 'Endpoints',
1275
-
font: { weight: 'bold' }
1278
-
maxRotation: 0, // Don't rotate
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;
1287
-
color: 'rgba(0, 0, 0, 0.05)',
1293
-
text: 'Number of Requests',
1294
-
font: { weight: 'bold' }
1296
-
beginAtZero: true,
1298
-
color: 'rgba(0, 0, 0, 0.05)',
732
+
type: 'logarithmic',
733
+
title: { display: true, text: 'Response Time (ms, log scale)' },
735
+
grid: { color: 'rgba(0, 0, 0, 0.05)' },
callback: function(value) {
1302
-
return value.toLocaleString();
1309
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1310
-
titleColor: 'white',
1311
-
bodyColor: 'white',
1312
-
borderColor: 'rgba(255, 255, 255, 0.1)',
1315
-
title: function(context) {
1316
-
return context[0].label; // Show full label in tooltip
1318
-
label: function(context) {
1319
-
return `Requests: ${context.parsed.y.toLocaleString()}`;
1325
-
generateLabels: function(chart) {
1326
-
const total = data.reduce((sum, d) => sum + d.count, 0);
1328
-
text: `Total Requests: ${total.toLocaleString()}`,
1329
-
fillStyle: '#10b981',
1330
-
strokeStyle: '#10b981',
1332
-
pointStyle: 'rect'
738
+
// Show clean numbers: 1, 10, 100, 1000, etc.
739
+
if (value === 1 || value === 10 || value === 100 || value === 1000 || value === 10000) {
740
+
return value + 'ms';
1342
-
function updateStatusChart(data) {
1343
-
const ctx = document.getElementById("statusChart").getContext("2d");
751
+
// User Agents Table
752
+
let allUserAgents = [];
1345
-
if (charts.status) charts.status.destroy();
754
+
function updateUserAgentsTable(userAgents) {
755
+
allUserAgents = userAgents;
756
+
renderUserAgentsTable(userAgents);
757
+
setupUserAgentSearch();
1347
-
const colors = data.map((d) => {
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";
760
+
function parseUserAgent(ua) {
761
+
// Keep strange/unique ones as-is
762
+
if (ua.length < 50 ||
763
+
!ua.includes('Mozilla/') ||
764
+
ua.includes('bot') ||
765
+
ua.includes('crawler') ||
766
+
ua.includes('spider') ||
767
+
!ua.includes('AppleWebKit') ||
768
+
ua.includes('Shiba-Arcade') ||
770
+
ua.includes('curl') ||
771
+
ua.includes('python') ||
772
+
ua.includes('PostmanRuntime')) {
1354
-
charts.status = new Chart(ctx, {
1357
-
labels: data.map((d) => `${d.status}`),
1360
-
data: data.map((d) => d.count),
1361
-
backgroundColor: colors,
1363
-
borderColor: '#fff'
1371
-
easing: 'easeInOutQuart'
776
+
// Parse common browsers
777
+
const os = ua.includes('Macintosh') ? 'macOS' :
778
+
ua.includes('Windows NT 10.0') ? 'Windows 10' :
779
+
ua.includes('Windows NT') ? 'Windows' :
780
+
ua.includes('X11; Linux') ? 'Linux' :
781
+
ua.includes('iPhone') ? 'iOS' :
782
+
ua.includes('Android') ? 'Android' : 'Unknown OS';
1377
-
function updateUserAgentChart(data) {
1378
-
const ctx = document.getElementById("userAgentChart").getContext("2d");
784
+
// Detect browser and version
785
+
let browser = 'Unknown Browser';
1380
-
if (charts.userAgent) charts.userAgent.destroy();
787
+
if (ua.includes('Edg/')) {
788
+
const match = ua.match(/Edg\/(\d+\.\d+)/);
789
+
const version = match ? match[1] : '';
790
+
browser = `Edge ${version}`;
791
+
} else if (ua.includes('Chrome/')) {
792
+
const match = ua.match(/Chrome\/(\d+\.\d+)/);
793
+
const version = match ? match[1] : '';
794
+
browser = `Chrome ${version}`;
795
+
} else if (ua.includes('Firefox/')) {
796
+
const match = ua.match(/Firefox\/(\d+\.\d+)/);
797
+
const version = match ? match[1] : '';
798
+
browser = `Firefox ${version}`;
799
+
} else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
800
+
browser = 'Safari';
1382
-
charts.userAgent = new Chart(ctx, {
1385
-
labels: data.map((d) => d.userAgent),
1388
-
data: data.map((d) => d.count),
1389
-
backgroundColor: [
1402
-
borderColor: '#fff'
1410
-
easing: 'easeInOutQuart'
803
+
return `${browser} (${os})`;
1416
-
function updateLatencyTimeChart(data, isHourly) {
1417
-
const ctx = document.getElementById("latencyTimeChart").getContext("2d");
1419
-
if (charts.latencyTime) charts.latencyTime.destroy();
806
+
function renderUserAgentsTable(userAgents) {
807
+
const container = document.getElementById("userAgentsTable");
1421
-
const chartTitle = document
1422
-
.querySelector("#latencyTimeChart")
1423
-
.parentElement.querySelector(".chart-title");
1425
-
chartTitle.textContent = isHourly
1426
-
? "Latency Over Time (Hourly)"
1427
-
: "Latency Over Time (Daily)";
809
+
if (userAgents.length === 0) {
810
+
container.innerHTML = '<div class="no-results">No user agents found</div>';
1430
-
charts.latencyTime = new Chart(ctx, {
1433
-
labels: data.map((d) => (isHourly ? d.time.split(" ")[1] : d.time)),
1436
-
label: "Average Response Time",
1437
-
data: data.map((d) => d.averageResponseTime),
1438
-
borderColor: "#3b82f6",
1439
-
backgroundColor: "rgba(59, 130, 246, 0.1)",
1443
-
pointHoverRadius: 6,
1444
-
pointBackgroundColor: "#3b82f6",
1445
-
pointBorderColor: "#ffffff",
1446
-
pointBorderWidth: 2,
1449
-
label: "P95 Response Time",
1450
-
data: data.map((d) => d.p95),
1451
-
borderColor: "#ef4444",
1452
-
backgroundColor: "rgba(239, 68, 68, 0.1)",
1456
-
pointHoverRadius: 6,
1457
-
pointBackgroundColor: "#ef4444",
1458
-
pointBorderColor: "#ffffff",
1459
-
pointBorderWidth: 2,
1465
-
maintainAspectRatio: false,
1476
-
easing: 'easeInOutQuart'
1480
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1481
-
titleColor: 'white',
1482
-
bodyColor: 'white',
1483
-
borderColor: 'rgba(255, 255, 255, 0.1)',
1486
-
title: function(context) {
1487
-
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
1489
-
afterBody: function(context) {
1490
-
const point = data[context[0].dataIndex];
1492
-
`Request Count: ${point.count.toLocaleString()}`,
1493
-
`Performance: ${point.averageResponseTime < 100 ? 'Excellent' : point.averageResponseTime < 300 ? 'Good' : 'Needs Attention'}`
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);
1505
-
text: `Average Response Time (Overall: ${avgAvg}ms)`,
1506
-
fillStyle: '#3b82f6',
1507
-
strokeStyle: '#3b82f6',
1509
-
pointStyle: 'circle'
1512
-
text: `P95 Response Time (Overall: ${avgP95}ms)`,
1513
-
fillStyle: '#ef4444',
1514
-
strokeStyle: '#ef4444',
1516
-
pointStyle: 'circle'
1527
-
text: isHourly ? 'Hour of Day' : 'Date',
1528
-
font: { weight: 'bold' }
1531
-
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
1534
-
callback: function(value, index, values) {
1535
-
const label = this.getLabelForValue(value);
1539
-
const date = new Date(label);
1540
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
1545
-
color: 'rgba(0, 0, 0, 0.05)',
1551
-
text: "Response Time (ms)",
1552
-
font: { weight: 'bold' }
1554
-
beginAtZero: true,
1556
-
color: 'rgba(0, 0, 0, 0.05)',
1559
-
callback: function(value) {
1560
-
return Math.round(value) + 'ms';
814
+
const totalRequests = userAgents.reduce((sum, ua) => sum + ua.count, 0);
1569
-
function updateLatencyDistributionChart(data) {
1570
-
const ctx = document.getElementById("latencyDistributionChart").getContext("2d");
816
+
const tableHTML = `
817
+
<table class="ua-table">
820
+
<th style="width: 50%">User Agent</th>
821
+
<th style="width: 20%">Requests</th>
822
+
<th style="width: 15%">Percentage</th>
826
+
${userAgents.map(ua => {
827
+
const displayName = parseUserAgent(ua.userAgent);
828
+
const percentage = ((ua.count / totalRequests) * 100).toFixed(1);
1572
-
if (charts.latencyDistribution) charts.latencyDistribution.destroy();
833
+
<div class="ua-name">${displayName}</div>
834
+
<div class="ua-raw">${ua.userAgent}</div>
836
+
<td class="ua-count">${ua.count.toLocaleString()}</td>
837
+
<td class="ua-percentage">${percentage}%</td>
1574
-
charts.latencyDistribution = new Chart(ctx, {
1577
-
labels: data.map((d) => d.range),
1580
-
label: "Requests",
1581
-
data: data.map((d) => d.count),
1582
-
backgroundColor: "#10b981",
1591
-
easing: 'easeInOutQuart'
1595
-
beginAtZero: true,
845
+
container.innerHTML = tableHTML;
1602
-
function updatePercentilesChart(percentiles) {
1603
-
const ctx = document.getElementById("percentilesChart").getContext("2d");
848
+
function setupUserAgentSearch() {
849
+
const searchInput = document.getElementById('userAgentSearch');
1605
-
if (charts.percentiles) charts.percentiles.destroy();
851
+
searchInput.addEventListener('input', function() {
852
+
const searchTerm = this.value.toLowerCase().trim();
1608
-
{ label: "P50 (Median)", value: percentiles.p50 },
1609
-
{ label: "P75", value: percentiles.p75 },
1610
-
{ label: "P90", value: percentiles.p90 },
1611
-
{ label: "P95", value: percentiles.p95 },
1612
-
{ label: "P99", value: percentiles.p99 },
1613
-
].filter((d) => d.value !== null);
854
+
if (searchTerm === '') {
855
+
renderUserAgentsTable(allUserAgents);
1615
-
charts.percentiles = new Chart(ctx, {
1618
-
labels: data.map((d) => d.label),
1621
-
label: "Response Time (ms)",
1622
-
data: data.map((d) => d.value),
1623
-
backgroundColor: [
1624
-
"#10b981", // P50 - Green (good)
1625
-
"#3b82f6", // P75 - Blue
1626
-
"#f59e0b", // P90 - Yellow (warning)
1627
-
"#ef4444", // P95 - Red (concerning)
1628
-
"#8b5cf6", // P99 - Purple (critical)
1632
-
borderColor: '#ffffff',
1638
-
maintainAspectRatio: false,
1641
-
easing: 'easeInOutQuart'
1645
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1646
-
titleColor: 'white',
1647
-
bodyColor: 'white',
1648
-
borderColor: 'rgba(255, 255, 255, 0.1)',
1651
-
label: function(context) {
1652
-
const percentile = context.label;
1653
-
const value = Math.round(context.parsed.y);
1654
-
let interpretation = '';
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';
1665
-
`${percentile}: ${value}ms`,
1672
-
display: false // Hide legend since colors are self-explanatory
1679
-
text: 'Response Time Percentiles',
1680
-
font: { weight: 'bold' }
1683
-
color: 'rgba(0, 0, 0, 0.05)',
1689
-
text: 'Response Time (ms)',
1690
-
font: { weight: 'bold' }
1692
-
beginAtZero: true,
1694
-
color: 'rgba(0, 0, 0, 0.05)',
1697
-
callback: function(value) {
1698
-
return Math.round(value) + 'ms';
859
+
const filtered = allUserAgents.filter(ua => {
860
+
const displayName = parseUserAgent(ua.userAgent).toLowerCase();
861
+
const rawUA = ua.userAgent.toLowerCase();
862
+
return displayName.includes(searchTerm) || rawUA.includes(searchTerm);
1707
-
function updateSlowestEndpointsChart(data) {
1708
-
const ctx = document.getElementById("slowestEndpointsChart").getContext("2d");
1710
-
if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
1712
-
charts.slowestEndpoints = new Chart(ctx, {
1715
-
labels: data.map((d) => d.endpoint),
1718
-
label: "Avg Response Time (ms)",
1719
-
data: data.map((d) => d.averageResponseTime),
1720
-
backgroundColor: "#ef4444",
1727
-
maintainAspectRatio: false,
1730
-
easing: 'easeInOutQuart'
1732
-
indexAxis: "x", // Changed from "y" to "x" to put labels on top
1737
-
text: 'Endpoints',
1738
-
font: { weight: 'bold' }
1743
-
callback: function(value, index, values) {
1744
-
const label = this.getLabelForValue(value);
1745
-
return label.length > 15 ? label.substring(0, 12) + '...' : label;
1749
-
color: 'rgba(0, 0, 0, 0.05)',
1755
-
text: 'Response Time (ms)',
1756
-
font: { weight: 'bold' }
1758
-
beginAtZero: true,
1760
-
color: 'rgba(0, 0, 0, 0.05)',
1763
-
callback: function(value) {
1764
-
return Math.round(value) + 'ms';
1771
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
1772
-
titleColor: 'white',
1773
-
bodyColor: 'white',
1774
-
borderColor: 'rgba(255, 255, 255, 0.1)',
1777
-
title: function(context) {
1778
-
return context[0].label; // Show full label in tooltip
1780
-
label: function(context) {
1781
-
const point = data[context.dataIndex];
1783
-
`Avg Response Time: ${Math.round(context.parsed.y)}ms`,
1784
-
`Request Count: ${point.count.toLocaleString()}`
1791
-
generateLabels: function(chart) {
1792
-
const avgTime = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
1794
-
text: `Average Response Time: ${avgTime}ms`,
1795
-
fillStyle: '#ef4444',
1796
-
strokeStyle: '#ef4444',
1798
-
pointStyle: 'rect'
865
+
renderUserAgentsTable(filtered);
document.getElementById("autoRefresh").addEventListener("change", function () {
autoRefreshInterval = setInterval(loadData, 30000);
1811
-
showToast('Auto-refresh enabled', 'success');
clearInterval(autoRefreshInterval);
1814
-
showToast('Auto-refresh disabled', 'info');
1818
-
// Days selector change handler
1819
-
document.getElementById("daysSelect").addEventListener("change", function() {
1820
-
// Reset visible charts when changing time period
1821
-
visibleCharts.clear();
878
+
document.getElementById("daysSelect").addEventListener("change", loadData);
1826
-
document.addEventListener('DOMContentLoaded', function() {
1827
-
initLazyLoading();
881
+
document.addEventListener('DOMContentLoaded', loadData);
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
1833
-
if (intersectionObserver) {
1834
-
intersectionObserver.disconnect();
clearInterval(autoRefreshInterval);
1838
-
// Destroy all charts to prevent memory leaks
Object.values(charts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {