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.
- setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
- setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
- setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
- setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
- setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
- trafficmonitor/__init__.py +11 -0
- trafficmonitor/admin.py +217 -0
- trafficmonitor/analytics/__init__.py +0 -0
- trafficmonitor/analytics/enhanced_queries.py +286 -0
- trafficmonitor/analytics/serializers.py +238 -0
- trafficmonitor/analytics/tests.py +757 -0
- trafficmonitor/analytics/urls.py +18 -0
- trafficmonitor/analytics/views.py +694 -0
- trafficmonitor/apps.py +7 -0
- trafficmonitor/circuit_breaker.py +63 -0
- trafficmonitor/conf.py +154 -0
- trafficmonitor/dashboard_security.py +111 -0
- trafficmonitor/db_utils.py +37 -0
- trafficmonitor/exceptions.py +93 -0
- trafficmonitor/health.py +66 -0
- trafficmonitor/load_test.py +423 -0
- trafficmonitor/load_test_api.py +307 -0
- trafficmonitor/management/__init__.py +1 -0
- trafficmonitor/management/commands/__init__.py +1 -0
- trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
- trafficmonitor/middleware.py +383 -0
- trafficmonitor/migrations/0001_initial.py +93 -0
- trafficmonitor/migrations/__init__.py +0 -0
- trafficmonitor/models.py +206 -0
- trafficmonitor/monitoring.py +104 -0
- trafficmonitor/permissions.py +64 -0
- trafficmonitor/security.py +180 -0
- trafficmonitor/settings_production.py +105 -0
- trafficmonitor/static/analytics/css/dashboard.css +99 -0
- trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
- trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
- trafficmonitor/static/analytics/js/dashboard.js +693 -0
- trafficmonitor/tasks.py +137 -0
- trafficmonitor/templates/analytics/dashboard.html +500 -0
- trafficmonitor/tests.py +246 -0
- trafficmonitor/views.py +3 -0
- 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
|
+
}
|