setu-trafficmonitor 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
  2. setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
  3. setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
  4. setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
  5. setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
  6. trafficmonitor/__init__.py +11 -0
  7. trafficmonitor/admin.py +217 -0
  8. trafficmonitor/analytics/__init__.py +0 -0
  9. trafficmonitor/analytics/enhanced_queries.py +286 -0
  10. trafficmonitor/analytics/serializers.py +238 -0
  11. trafficmonitor/analytics/tests.py +757 -0
  12. trafficmonitor/analytics/urls.py +18 -0
  13. trafficmonitor/analytics/views.py +694 -0
  14. trafficmonitor/apps.py +7 -0
  15. trafficmonitor/circuit_breaker.py +63 -0
  16. trafficmonitor/conf.py +154 -0
  17. trafficmonitor/dashboard_security.py +111 -0
  18. trafficmonitor/db_utils.py +37 -0
  19. trafficmonitor/exceptions.py +93 -0
  20. trafficmonitor/health.py +66 -0
  21. trafficmonitor/load_test.py +423 -0
  22. trafficmonitor/load_test_api.py +307 -0
  23. trafficmonitor/management/__init__.py +1 -0
  24. trafficmonitor/management/commands/__init__.py +1 -0
  25. trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
  26. trafficmonitor/middleware.py +383 -0
  27. trafficmonitor/migrations/0001_initial.py +93 -0
  28. trafficmonitor/migrations/__init__.py +0 -0
  29. trafficmonitor/models.py +206 -0
  30. trafficmonitor/monitoring.py +104 -0
  31. trafficmonitor/permissions.py +64 -0
  32. trafficmonitor/security.py +180 -0
  33. trafficmonitor/settings_production.py +105 -0
  34. trafficmonitor/static/analytics/css/dashboard.css +99 -0
  35. trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
  36. trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
  37. trafficmonitor/static/analytics/js/dashboard.js +693 -0
  38. trafficmonitor/tasks.py +137 -0
  39. trafficmonitor/templates/analytics/dashboard.html +500 -0
  40. trafficmonitor/tests.py +246 -0
  41. trafficmonitor/views.py +3 -0
  42. trafficmonitor/websocket_consumers.py +128 -0
@@ -0,0 +1,697 @@
1
+ /**
2
+ * TrafficMonitor v2.0 - Enterprise Analytics Dashboard JavaScript
3
+ * Handles data fetching, chart rendering, and interactions with new Read/Write metrics
4
+ */
5
+
6
+ // Chart instances (global)
7
+ let requestsOverTimeChart = null;
8
+ let readWriteChart = null;
9
+ let methodsChart = null;
10
+ let endpointCategoriesChart = null;
11
+ let slowestEndpointsChart = null;
12
+ let hourlyHeatmapChart = null;
13
+
14
+ // Auto-refresh interval ID (prevent duplicates)
15
+ let refreshIntervalId = null;
16
+
17
+ // Auto-refresh state
18
+ window.autoRefreshEnabled = true;
19
+
20
+ // Chart color schemes
21
+ const COLORS = {
22
+ primary: 'rgb(59, 130, 246)', // Blue
23
+ success: 'rgb(34, 197, 94)', // Green
24
+ warning: 'rgb(251, 191, 36)', // Yellow
25
+ danger: 'rgb(239, 68, 68)', // Red
26
+ info: 'rgb(139, 92, 246)', // Purple
27
+ secondary: 'rgb(107, 114, 128)', // Gray
28
+ read: 'rgb(34, 197, 94)', // Green for READ
29
+ write: 'rgb(139, 92, 246)', // Purple for WRITE
30
+ delete: 'rgb(239, 68, 68)', // Red for DELETE
31
+ };
32
+
33
+ /**
34
+ * Initialize the dashboard
35
+ */
36
+ function initializeDashboard() {
37
+ loadAnalyticsData();
38
+ }
39
+
40
+ /**
41
+ * Refresh dashboard with current filters (component-wise)
42
+ */
43
+ async function refreshDashboard() {
44
+ console.log('Starting component-wise refresh...');
45
+
46
+ try {
47
+ const params = getFilterParams();
48
+ const response = await fetch(`/api/analytics/overview/?${params}`, {
49
+ headers: {
50
+ 'X-Requested-With': 'XMLHttpRequest'
51
+ }
52
+ });
53
+
54
+ if (!response.ok) {
55
+ throw new Error(`HTTP ${response.status}: Failed to fetch analytics data`);
56
+ }
57
+
58
+ const data = await response.json();
59
+
60
+ // Refresh components independently with smooth transitions
61
+ await Promise.all([
62
+ refreshStatsCards(data),
63
+ refreshCharts(data),
64
+ refreshTables(data)
65
+ ]);
66
+
67
+ // Update last updated time
68
+ const lastUpdatedEl = document.getElementById('lastUpdated');
69
+ if (lastUpdatedEl) {
70
+ lastUpdatedEl.textContent = new Date().toLocaleString();
71
+ // Flash effect to show update
72
+ lastUpdatedEl.classList.add('text-green-600', 'font-bold');
73
+ setTimeout(() => {
74
+ lastUpdatedEl.classList.remove('text-green-600', 'font-bold');
75
+ }, 2000);
76
+ }
77
+
78
+ console.log('Component-wise refresh completed successfully');
79
+
80
+ } catch (error) {
81
+ console.error('Error refreshing dashboard:', error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Apply filters and reload dashboard
87
+ */
88
+ function applyFilters() {
89
+ loadAnalyticsData();
90
+ }
91
+
92
+ /**
93
+ * Get current filter parameters
94
+ */
95
+ function getFilterParams() {
96
+ const params = new URLSearchParams();
97
+
98
+ const range = document.getElementById('rangeSelect').value;
99
+ params.append('range', range);
100
+
101
+ if (range === 'custom') {
102
+ const startDate = document.getElementById('startDate').value;
103
+ const endDate = document.getElementById('endDate').value;
104
+ if (startDate) params.append('start_date', startDate);
105
+ if (endDate) params.append('end_date', endDate);
106
+ }
107
+
108
+ const operationType = document.getElementById('operationFilter')?.value;
109
+ if (operationType) params.append('operation_type', operationType);
110
+
111
+ const status = document.getElementById('statusFilter')?.value;
112
+ if (status) params.append('status', status);
113
+
114
+ const path = document.getElementById('pathFilter')?.value;
115
+ if (path) params.append('path', path);
116
+
117
+ return params.toString();
118
+ }
119
+
120
+ /**
121
+ * Load analytics data from API
122
+ */
123
+ async function loadAnalyticsData() {
124
+ try {
125
+ // Show loading (only on first load)
126
+ const loadingEl = document.getElementById('loadingIndicator');
127
+ const dashboardEl = document.getElementById('dashboardContent');
128
+
129
+ if (dashboardEl && dashboardEl.classList.contains('hidden')) {
130
+ loadingEl?.classList.remove('hidden');
131
+ }
132
+
133
+ const params = getFilterParams();
134
+ const response = await fetch(`/api/analytics/overview/?${params}`, {
135
+ headers: {
136
+ 'X-Requested-With': 'XMLHttpRequest'
137
+ }
138
+ });
139
+
140
+ if (!response.ok) {
141
+ throw new Error(`HTTP ${response.status}: Failed to fetch analytics data`);
142
+ }
143
+
144
+ const data = await response.json();
145
+
146
+ // Update stats cards
147
+ updateStatsCards(data);
148
+
149
+ // Update charts
150
+ updateCharts(data);
151
+
152
+ // Update tables
153
+ updateTables(data);
154
+
155
+ // Update last updated time
156
+ const lastUpdatedEl = document.getElementById('lastUpdated');
157
+ if (lastUpdatedEl) {
158
+ lastUpdatedEl.textContent = new Date().toLocaleString();
159
+ }
160
+
161
+ // Show dashboard
162
+ loadingEl?.classList.add('hidden');
163
+ dashboardEl?.classList.remove('hidden');
164
+
165
+ } catch (error) {
166
+ console.error('Error loading analytics:', error);
167
+
168
+ // Show error message in dashboard instead of alert
169
+ const dashboardEl = document.getElementById('dashboardContent');
170
+ if (dashboardEl) {
171
+ const errorDiv = document.createElement('div');
172
+ errorDiv.className = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
173
+ errorDiv.innerHTML = `
174
+ <strong>Error:</strong> Failed to load analytics data. ${error.message}
175
+ <button onclick="location.reload()" class="ml-4 px-3 py-1 bg-red-500 text-white rounded">
176
+ Reload Page
177
+ </button>
178
+ `;
179
+ dashboardEl.insertBefore(errorDiv, dashboardEl.firstChild);
180
+ }
181
+
182
+ // Hide loading
183
+ document.getElementById('loadingIndicator')?.classList.add('hidden');
184
+ dashboardEl?.classList.remove('hidden');
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Refresh stats cards with animation
190
+ */
191
+ async function refreshStatsCards(data) {
192
+ return new Promise((resolve) => {
193
+ // Add fade effect
194
+ const cards = document.querySelectorAll('.stat-card');
195
+ cards.forEach(card => card.style.opacity = '0.5');
196
+
197
+ setTimeout(() => {
198
+ updateStatsCards(data);
199
+ cards.forEach(card => {
200
+ card.style.opacity = '1';
201
+ card.style.transition = 'opacity 0.5s ease';
202
+ });
203
+ resolve();
204
+ }, 100);
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Update stats cards
210
+ */
211
+ function updateStatsCards(data) {
212
+ // Total Requests
213
+ document.getElementById('totalRequests').textContent =
214
+ (data.totals?.selected_range || 0).toLocaleString();
215
+
216
+ // Read/Write Operations
217
+ const readWriteSummary = data.read_write_summary || {};
218
+ document.getElementById('readOps').textContent =
219
+ (readWriteSummary.read_count || 0).toLocaleString();
220
+ document.getElementById('readPercent').textContent =
221
+ `${(readWriteSummary.read_percentage || 0).toFixed(1)}% of total`;
222
+
223
+ document.getElementById('writeOps').textContent =
224
+ (readWriteSummary.write_count || 0).toLocaleString();
225
+ document.getElementById('writePercent').textContent =
226
+ `${(readWriteSummary.write_percentage || 0).toFixed(1)}% of total`;
227
+
228
+ // API Health
229
+ const apiHealth = data.api_health || {};
230
+ document.getElementById('successRate').textContent =
231
+ `${(apiHealth.success_rate || 0).toFixed(1)}%`;
232
+ document.getElementById('p95ResponseTime').textContent =
233
+ `${(apiHealth.p95_response_time || 0).toFixed(0)} ms`;
234
+
235
+ // API Health Metrics Cards
236
+ document.getElementById('statusSuccess').textContent =
237
+ (data.status_summary?.success || 0).toLocaleString();
238
+ document.getElementById('statusErrors').textContent =
239
+ ((data.status_summary?.client_error || 0) + (data.status_summary?.server_error || 0)).toLocaleString();
240
+
241
+ document.getElementById('p50Response').textContent =
242
+ `${(apiHealth.p50_response_time || 0).toFixed(0)} ms`;
243
+ document.getElementById('p99Response').textContent =
244
+ `${(apiHealth.p99_response_time || 0).toFixed(0)} ms`;
245
+
246
+ // Throughput
247
+ const throughput = data.throughput || {};
248
+ document.getElementById('throughputRPS').textContent =
249
+ `${(throughput.avg_rps || 0).toFixed(1)} rps`;
250
+
251
+ // Average Queries
252
+ document.getElementById('avgQueries').textContent =
253
+ (data.performance?.avg_query_count || 0).toFixed(1);
254
+ }
255
+
256
+ /**
257
+ * Refresh all charts with staggered animation
258
+ */
259
+ async function refreshCharts(data) {
260
+ const charts = [
261
+ { fn: () => updateRequestsOverTimeChart(data.requests_over_time || []), delay: 0 },
262
+ { fn: () => updateReadWriteChart(data.read_write_over_time || []), delay: 100 },
263
+ { fn: () => updateMethodsChart(data.methods || []), delay: 200 },
264
+ { fn: () => updateEndpointCategoriesChart(data.endpoint_categories || []), delay: 300 },
265
+ { fn: () => updateSlowestEndpointsChart(data.slowest_endpoints || []), delay: 400 },
266
+ { fn: () => updateHourlyHeatmapChart(data.hourly_heatmap || []), delay: 500 }
267
+ ];
268
+
269
+ return Promise.all(
270
+ charts.map(({ fn, delay }) =>
271
+ new Promise(resolve => {
272
+ setTimeout(() => {
273
+ fn();
274
+ resolve();
275
+ }, delay);
276
+ })
277
+ )
278
+ );
279
+ }
280
+
281
+ /**
282
+ * Update all charts
283
+ */
284
+ function updateCharts(data) {
285
+ updateRequestsOverTimeChart(data.requests_over_time || []);
286
+ updateReadWriteChart(data.read_write_over_time || []);
287
+ updateMethodsChart(data.methods || []);
288
+ updateEndpointCategoriesChart(data.endpoint_categories || []);
289
+ updateSlowestEndpointsChart(data.slowest_endpoints || []);
290
+ updateHourlyHeatmapChart(data.hourly_heatmap || []);
291
+ }
292
+
293
+ /**
294
+ * Update Requests Over Time Chart
295
+ */
296
+ function updateRequestsOverTimeChart(data) {
297
+ const ctx = document.getElementById('requestsOverTimeChart');
298
+ if (!ctx) return;
299
+
300
+ // Smooth transition: update existing chart if possible
301
+ if (requestsOverTimeChart && requestsOverTimeChart.data) {
302
+ requestsOverTimeChart.data.labels = data.map(d => new Date(d.period));
303
+ requestsOverTimeChart.data.datasets[0].data = data.map(d => d.count);
304
+ requestsOverTimeChart.update('none'); // Update without animation for smoother feel
305
+ return;
306
+ }
307
+
308
+ // Create new chart if it doesn't exist
309
+ if (requestsOverTimeChart) {
310
+ requestsOverTimeChart.destroy();
311
+ }
312
+
313
+ requestsOverTimeChart = new Chart(ctx, {
314
+ type: 'line',
315
+ data: {
316
+ labels: data.map(d => new Date(d.period)),
317
+ datasets: [{
318
+ label: 'Total Requests',
319
+ data: data.map(d => d.count),
320
+ borderColor: COLORS.primary,
321
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
322
+ tension: 0.4,
323
+ fill: true
324
+ }]
325
+ },
326
+ options: {
327
+ responsive: true,
328
+ maintainAspectRatio: false,
329
+ plugins: {
330
+ legend: {
331
+ display: false
332
+ }
333
+ },
334
+ scales: {
335
+ x: {
336
+ type: 'time',
337
+ time: {
338
+ unit: 'day'
339
+ }
340
+ },
341
+ y: {
342
+ beginAtZero: true
343
+ }
344
+ }
345
+ }
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Update Read vs Write Chart
351
+ */
352
+ function updateReadWriteChart(data) {
353
+ const ctx = document.getElementById('readWriteChart');
354
+ if (!ctx) return;
355
+
356
+ // Smooth transition: update existing chart
357
+ if (readWriteChart && readWriteChart.data) {
358
+ readWriteChart.data.labels = data.map(d => new Date(d.period));
359
+ readWriteChart.data.datasets[0].data = data.map(d => d.read_count);
360
+ readWriteChart.data.datasets[1].data = data.map(d => d.write_count);
361
+ readWriteChart.data.datasets[2].data = data.map(d => d.delete_count);
362
+ readWriteChart.update('none');
363
+ return;
364
+ }
365
+
366
+ // Create new chart
367
+ if (readWriteChart) {
368
+ readWriteChart.destroy();
369
+ }
370
+
371
+ readWriteChart = new Chart(ctx, {
372
+ type: 'line',
373
+ data: {
374
+ labels: data.map(d => new Date(d.period)),
375
+ datasets: [
376
+ {
377
+ label: '📖 READ Operations',
378
+ data: data.map(d => d.read_count),
379
+ borderColor: COLORS.read,
380
+ backgroundColor: 'rgba(34, 197, 94, 0.1)',
381
+ tension: 0.4,
382
+ fill: true
383
+ },
384
+ {
385
+ label: '✏️ WRITE Operations',
386
+ data: data.map(d => d.write_count),
387
+ borderColor: COLORS.write,
388
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
389
+ tension: 0.4,
390
+ fill: true
391
+ },
392
+ {
393
+ label: '🗑️ DELETE Operations',
394
+ data: data.map(d => d.delete_count),
395
+ borderColor: COLORS.delete,
396
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
397
+ tension: 0.4,
398
+ fill: true
399
+ }
400
+ ]
401
+ },
402
+ options: {
403
+ responsive: true,
404
+ maintainAspectRatio: false,
405
+ plugins: {
406
+ legend: {
407
+ display: true,
408
+ position: 'top'
409
+ }
410
+ },
411
+ scales: {
412
+ x: {
413
+ type: 'time',
414
+ time: {
415
+ unit: 'day'
416
+ }
417
+ },
418
+ y: {
419
+ beginAtZero: true
420
+ }
421
+ }
422
+ }
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Update HTTP Methods Chart
428
+ */
429
+ function updateMethodsChart(data) {
430
+ const ctx = document.getElementById('methodsChart');
431
+ if (!ctx) return;
432
+
433
+ if (methodsChart) {
434
+ methodsChart.destroy();
435
+ }
436
+
437
+ methodsChart = new Chart(ctx, {
438
+ type: 'doughnut',
439
+ data: {
440
+ labels: data.map(d => d.method),
441
+ datasets: [{
442
+ data: data.map(d => d.count),
443
+ backgroundColor: [
444
+ COLORS.success,
445
+ COLORS.primary,
446
+ COLORS.warning,
447
+ 'rgb(249, 115, 22)',
448
+ COLORS.danger,
449
+ ],
450
+ }]
451
+ },
452
+ options: {
453
+ responsive: true,
454
+ maintainAspectRatio: false,
455
+ plugins: {
456
+ legend: {
457
+ position: 'right'
458
+ }
459
+ }
460
+ }
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Update Endpoint Categories Chart
466
+ */
467
+ function updateEndpointCategoriesChart(data) {
468
+ const ctx = document.getElementById('endpointCategoriesChart');
469
+ if (!ctx) return;
470
+
471
+ if (endpointCategoriesChart) {
472
+ endpointCategoriesChart.destroy();
473
+ }
474
+
475
+ endpointCategoriesChart = new Chart(ctx, {
476
+ type: 'bar',
477
+ data: {
478
+ labels: data.map(d => d.endpoint_category),
479
+ datasets: [{
480
+ label: 'Requests',
481
+ data: data.map(d => d.total_count),
482
+ backgroundColor: COLORS.info,
483
+ }]
484
+ },
485
+ options: {
486
+ responsive: true,
487
+ maintainAspectRatio: false,
488
+ indexAxis: 'y',
489
+ plugins: {
490
+ legend: {
491
+ display: false
492
+ }
493
+ }
494
+ }
495
+ });
496
+ }
497
+
498
+ /**
499
+ * Update Slowest Endpoints Chart
500
+ */
501
+ function updateSlowestEndpointsChart(data) {
502
+ const ctx = document.getElementById('slowestEndpointsChart');
503
+ if (!ctx) return;
504
+
505
+ if (slowestEndpointsChart) {
506
+ slowestEndpointsChart.destroy();
507
+ }
508
+
509
+ slowestEndpointsChart = new Chart(ctx, {
510
+ type: 'bar',
511
+ data: {
512
+ labels: data.map(d => d.path.length > 40 ? d.path.substring(0, 40) + '...' : d.path),
513
+ datasets: [{
514
+ label: 'Avg Response Time (ms)',
515
+ data: data.map(d => d.avg_response_time),
516
+ backgroundColor: COLORS.warning,
517
+ }]
518
+ },
519
+ options: {
520
+ responsive: true,
521
+ maintainAspectRatio: false,
522
+ indexAxis: 'y',
523
+ plugins: {
524
+ legend: {
525
+ display: false
526
+ }
527
+ }
528
+ }
529
+ });
530
+ }
531
+
532
+ /**
533
+ * Update Hourly Heatmap Chart
534
+ */
535
+ function updateHourlyHeatmapChart(data) {
536
+ const ctx = document.getElementById('hourlyHeatmapChart');
537
+ if (!ctx) return;
538
+
539
+ if (hourlyHeatmapChart) {
540
+ hourlyHeatmapChart.destroy();
541
+ }
542
+
543
+ hourlyHeatmapChart = new Chart(ctx, {
544
+ type: 'bar',
545
+ data: {
546
+ labels: data.map(d => `${d.hour}:00`),
547
+ datasets: [{
548
+ label: 'Requests',
549
+ data: data.map(d => d.count),
550
+ backgroundColor: COLORS.danger,
551
+ }]
552
+ },
553
+ options: {
554
+ responsive: true,
555
+ maintainAspectRatio: false,
556
+ plugins: {
557
+ legend: {
558
+ display: false
559
+ }
560
+ }
561
+ }
562
+ });
563
+ }
564
+
565
+ /**
566
+ * Refresh tables with fade animation
567
+ */
568
+ async function refreshTables(data) {
569
+ return new Promise((resolve) => {
570
+ const tbody = document.getElementById('topEndpointsTable');
571
+ if (tbody) {
572
+ tbody.style.opacity = '0.3';
573
+ setTimeout(() => {
574
+ updateTables(data);
575
+ tbody.style.opacity = '1';
576
+ tbody.style.transition = 'opacity 0.5s ease';
577
+ resolve();
578
+ }, 200);
579
+ } else {
580
+ resolve();
581
+ }
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Update tables
587
+ */
588
+ function updateTables(data) {
589
+ updateTopEndpointsTable(data.top_endpoints || []);
590
+ }
591
+
592
+ /**
593
+ * Update Top Endpoints Table
594
+ */
595
+ function updateTopEndpointsTable(data) {
596
+ const tbody = document.getElementById('topEndpointsTable');
597
+ if (!tbody) return;
598
+
599
+ tbody.innerHTML = '';
600
+
601
+ data.forEach((endpoint, index) => {
602
+ const row = document.createElement('tr');
603
+ row.className = 'hover:bg-gray-50';
604
+
605
+ // Health badge
606
+ let healthBadge = '';
607
+ const avgTime = endpoint.avg_response_time || 0;
608
+ if (avgTime < 100) {
609
+ healthBadge = '<span class="metric-badge badge-success">Excellent</span>';
610
+ } else if (avgTime < 500) {
611
+ healthBadge = '<span class="metric-badge badge-info">Good</span>';
612
+ } else if (avgTime < 1000) {
613
+ healthBadge = '<span class="metric-badge badge-warning">Slow</span>';
614
+ } else {
615
+ healthBadge = '<span class="metric-badge badge-danger">Critical</span>';
616
+ }
617
+
618
+ row.innerHTML = `
619
+ <td class="px-4 py-3 text-sm text-gray-900">${index + 1}</td>
620
+ <td class="px-4 py-3 text-sm text-gray-900 font-mono">${endpoint.path}</td>
621
+ <td class="px-4 py-3 text-sm text-gray-600 text-center">
622
+ <span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs font-medium">
623
+ ${endpoint.path.split('/').filter(p => p && p !== 'api')[0] || 'N/A'}
624
+ </span>
625
+ </td>
626
+ <td class="px-4 py-3 text-sm text-gray-900 text-right font-semibold">${endpoint.count.toLocaleString()}</td>
627
+ <td class="px-4 py-3 text-sm text-gray-900 text-right">${avgTime.toFixed(0)} ms</td>
628
+ <td class="px-4 py-3 text-sm text-gray-900 text-right">${(endpoint.max_response_time || 0).toFixed(0)} ms</td>
629
+ <td class="px-4 py-3 text-center">${healthBadge}</td>
630
+ `;
631
+ tbody.appendChild(row);
632
+ });
633
+ }
634
+
635
+ // Initialize on page load (ensure it runs only once)
636
+ if (document.readyState === 'loading') {
637
+ document.addEventListener('DOMContentLoaded', initializePage);
638
+ } else {
639
+ initializePage();
640
+ }
641
+
642
+ function initializePage() {
643
+ // Prevent duplicate initialization
644
+ if (window.dashboardInitialized) {
645
+ console.log('Dashboard already initialized, skipping...');
646
+ return;
647
+ }
648
+ window.dashboardInitialized = true;
649
+
650
+ console.log('Initializing dashboard...');
651
+ initializeDashboard();
652
+
653
+ // Handle custom range toggle
654
+ const rangeSelect = document.getElementById('rangeSelect');
655
+ if (rangeSelect) {
656
+ rangeSelect.addEventListener('change', function() {
657
+ const customContainer = document.getElementById('customRangeContainer');
658
+ if (customContainer) {
659
+ if (this.value === 'custom') {
660
+ customContainer.classList.remove('hidden');
661
+ } else {
662
+ customContainer.classList.add('hidden');
663
+ }
664
+ }
665
+ });
666
+ }
667
+
668
+ // Auto-refresh every 30 seconds (clear any existing interval first)
669
+ if (refreshIntervalId) {
670
+ clearInterval(refreshIntervalId);
671
+ }
672
+
673
+ if (window.autoRefreshEnabled) {
674
+ refreshIntervalId = setInterval(function() {
675
+ console.log('Auto-refreshing dashboard data...');
676
+ refreshDashboard();
677
+ }, 30000); // 30 seconds
678
+ window.refreshIntervalId = refreshIntervalId; // Make it globally accessible
679
+
680
+ // Add visual indicator for next refresh
681
+ let countdown = 30;
682
+ const countdownInterval = setInterval(() => {
683
+ countdown--;
684
+ const refreshStatus = document.getElementById('refreshStatus');
685
+ if (refreshStatus && window.autoRefreshEnabled) {
686
+ refreshStatus.textContent = `ON (${countdown}s)`;
687
+ }
688
+ if (countdown <= 0) {
689
+ countdown = 30;
690
+ }
691
+ }, 1000);
692
+
693
+ window.countdownIntervalId = countdownInterval;
694
+ }
695
+
696
+ console.log('Dashboard initialized successfully. Auto-refresh: ' + (window.autoRefreshEnabled ? '30s' : 'OFF'));
697
+ }