···
4
-
<meta charset="UTF-8" />
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
-
<title>Cachet Analytics Dashboard</title>
7
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
8
-
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
13
-
box-sizing: border-box;
17
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
19
-
background: #f9fafb;
5
+
<meta charset="UTF-8" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
<title>Cachet Analytics Dashboard</title>
8
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
10
+
src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
15
+
box-sizing: border-box;
26
-
padding: 1.5rem 2rem;
27
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
28
-
margin-bottom: 2rem;
29
-
border-bottom: 1px solid #e5e7eb;
19
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
21
+
background: #f9fafb;
34
-
font-size: 1.875rem;
36
-
margin-bottom: 0.5rem;
28
+
padding: 1.5rem 2rem;
29
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
30
+
margin-bottom: 2rem;
31
+
border-bottom: 1px solid #e5e7eb;
36
+
font-size: 1.875rem;
38
+
margin-bottom: 0.5rem;
46
-
text-decoration: none;
50
-
.header-links a:hover {
52
-
text-decoration: underline;
48
+
text-decoration: none;
56
-
margin-bottom: 2rem;
58
-
justify-content: center;
59
-
align-items: center;
52
+
.header-links a:hover {
54
+
text-decoration: underline;
66
-
padding: 0.75rem 1.25rem;
67
-
border: 1px solid #d1d5db;
71
-
font-size: 0.875rem;
73
-
transition: all 0.2s ease;
58
+
margin-bottom: 2rem;
60
+
justify-content: center;
61
+
align-items: center;
76
-
.controls select:hover,
77
-
.controls select:focus {
78
-
border-color: #6366f1;
80
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
68
+
padding: 0.75rem 1.25rem;
69
+
border: 1px solid #d1d5db;
73
+
font-size: 0.875rem;
75
+
transition: all 0.2s ease;
84
-
background: #6366f1;
78
+
.controls select:hover,
79
+
.controls select:focus {
80
+
border-color: #6366f1;
82
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
89
-
.controls button:hover {
90
-
background: #4f46e5;
86
+
background: #6366f1;
93
-
.controls button:disabled {
94
-
background: #9ca3af;
95
-
cursor: not-allowed;
91
+
.controls button:hover {
92
+
background: #4f46e5;
95
+
.controls button:disabled {
96
+
background: #9ca3af;
97
+
cursor: not-allowed;
106
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
108
-
margin-bottom: 3rem;
114
-
border-radius: 12px;
115
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
116
-
text-align: center;
117
-
border: 1px solid #e5e7eb;
118
-
transition: all 0.2s ease;
121
-
flex-direction: column;
122
-
justify-content: center;
108
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
110
+
margin-bottom: 3rem;
116
+
border-radius: 12px;
117
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
118
+
text-align: center;
119
+
border: 1px solid #e5e7eb;
120
+
transition: all 0.2s ease;
123
+
flex-direction: column;
124
+
justify-content: center;
128
+
transform: translateY(-2px);
129
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
135
+
margin-bottom: 0.5rem;
126
-
transform: translateY(-2px);
127
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
143
+
font-size: 0.875rem;
144
+
text-transform: uppercase;
145
+
letter-spacing: 0.05em;
133
-
margin-bottom: 0.5rem;
150
+
grid-template-columns: 1fr;
152
+
margin-bottom: 3rem;
141
-
font-size: 0.875rem;
142
-
text-transform: uppercase;
143
-
letter-spacing: 0.05em;
157
+
grid-template-columns: 1fr 1fr;
161
+
@media (max-width: 768px) {
grid-template-columns: 1fr;
150
-
margin-bottom: 3rem;
155
-
grid-template-columns: 1fr 1fr;
167
+
grid-template-columns: 1fr;
159
-
@media (max-width: 768px) {
161
-
grid-template-columns: 1fr;
165
-
grid-template-columns: 1fr;
182
+
border-radius: 12px;
183
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
184
+
border: 1px solid #e5e7eb;
186
+
padding-bottom: 5rem;
190
+
font-size: 1.25rem;
191
+
margin-bottom: 1.5rem;
180
-
border-radius: 12px;
181
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
182
-
border: 1px solid #e5e7eb;
184
-
padding-bottom: 5rem;
196
+
.user-agents-table {
199
+
border-radius: 12px;
200
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
201
+
border: 1px solid #e5e7eb;
188
-
font-size: 1.25rem;
189
-
margin-bottom: 1.5rem;
204
+
.search-container {
205
+
margin-bottom: 1.5rem;
206
+
position: relative;
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;
211
+
padding: 0.75rem 1rem;
212
+
border: 1px solid #d1d5db;
213
+
border-radius: 8px;
214
+
font-size: 0.875rem;
215
+
background: #f9fafb;
216
+
transition: border-color 0.2s ease;
202
-
.search-container {
203
-
margin-bottom: 1.5rem;
204
-
position: relative;
219
+
.search-input:focus {
221
+
border-color: #6366f1;
223
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
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;
228
+
border-collapse: collapse;
229
+
font-size: 0.875rem;
217
-
.search-input:focus {
219
-
border-color: #6366f1;
221
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
234
+
padding: 0.75rem 1rem;
235
+
background: #f9fafb;
236
+
border-bottom: 2px solid #e5e7eb;
226
-
border-collapse: collapse;
227
-
font-size: 0.875rem;
244
+
padding: 0.75rem 1rem;
245
+
border-bottom: 1px solid #f3f4f6;
246
+
vertical-align: top;
232
-
padding: 0.75rem 1rem;
233
-
background: #f9fafb;
234
-
border-bottom: 2px solid #e5e7eb;
249
+
.ua-table tbody tr:hover {
250
+
background: #f9fafb;
242
-
padding: 0.75rem 1rem;
243
-
border-bottom: 1px solid #f3f4f6;
244
-
vertical-align: top;
258
+
word-break: break-word;
247
-
.ua-table tbody tr:hover {
248
-
background: #f9fafb;
262
+
font-family: monospace;
263
+
font-size: 0.75rem;
265
+
margin-top: 0.25rem;
267
+
word-break: break-all;
256
-
word-break: break-word;
275
+
white-space: nowrap;
260
-
font-family: monospace;
261
-
font-size: 0.75rem;
263
-
margin-top: 0.25rem;
265
-
word-break: break-all;
281
+
font-size: 0.75rem;
273
-
white-space: nowrap;
285
+
text-align: center;
288
+
font-style: italic;
279
-
font-size: 0.75rem;
292
+
text-align: center;
283
-
text-align: center;
286
-
font-style: italic;
298
+
display: inline-block;
301
+
border: 3px solid #e5e7eb;
302
+
border-radius: 50%;
303
+
border-top-color: #6366f1;
304
+
animation: spin 1s ease-in-out infinite;
305
+
margin-bottom: 1rem;
290
-
text-align: center;
310
+
transform: rotate(360deg);
296
-
display: inline-block;
299
-
border: 3px solid #e5e7eb;
300
-
border-radius: 50%;
301
-
border-top-color: #6366f1;
302
-
animation: spin 1s ease-in-out infinite;
303
-
margin-bottom: 1rem;
315
+
background: #fef2f2;
318
+
border-radius: 8px;
320
+
border: 1px solid #fecaca;
307
-
to { transform: rotate(360deg); }
325
+
align-items: center;
327
+
font-size: 0.875rem;
311
-
background: #fef2f2;
314
-
border-radius: 8px;
316
-
border: 1px solid #fecaca;
331
+
.auto-refresh input[type="checkbox"] {
332
+
transform: scale(1.1);
333
+
accent-color: #6366f1;
321
-
align-items: center;
323
-
font-size: 0.875rem;
339
+
<div class="header">
340
+
<h1>📊 Cachet Analytics Dashboard</h1>
341
+
<div class="header-links">
342
+
<a href="https://github.com/taciturnaxolotl/cachet">Github</a>
343
+
<a href="/swagger">API Docs</a>
344
+
<a href="/stats">Raw Stats</a>
345
+
<a href="https://status.dunkirk.sh/status/cachet">Status</a>
327
-
.auto-refresh input[type="checkbox"] {
328
-
transform: scale(1.1);
329
-
accent-color: #6366f1;
334
-
<div class="header">
335
-
<h1>📊 Cachet Analytics Dashboard</h1>
336
-
<div class="header-links">
337
-
<a href="https://github.com/taciturnaxolotl/cachet">Github</a>
338
-
<a href="/swagger">API Docs</a>
339
-
<a href="/stats">Raw Stats</a>
349
+
<div class="dashboard">
350
+
<div class="controls">
351
+
<select id="daysSelect">
352
+
<option value="1">Last 24 hours</option>
353
+
<option value="7" selected>Last 7 days</option>
354
+
<option value="30">Last 30 days</option>
356
+
<button id="refreshBtn" onclick="loadData()">Refresh</button>
357
+
<div class="auto-refresh">
358
+
<input type="checkbox" id="autoRefresh" />
359
+
<label for="autoRefresh">Auto-refresh (30s)</label>
343
-
<div class="dashboard">
344
-
<div class="controls">
345
-
<select id="daysSelect">
346
-
<option value="1">Last 24 hours</option>
347
-
<option value="7" selected>Last 7 days</option>
348
-
<option value="30">Last 30 days</option>
350
-
<button id="refreshBtn" onclick="loadData()">Refresh</button>
351
-
<div class="auto-refresh">
352
-
<input type="checkbox" id="autoRefresh" />
353
-
<label for="autoRefresh">Auto-refresh (30s)</label>
363
+
<div id="loading" class="loading">
364
+
<div class="loading-spinner"></div>
365
+
Loading analytics data...
367
+
<div id="error" class="error" style="display: none"></div>
369
+
<div id="content" style="display: none">
370
+
<!-- Key Metrics -->
371
+
<div class="stats-grid">
372
+
<div class="stat-card">
373
+
<div class="stat-number" id="totalRequests">-</div>
374
+
<div class="stat-label">Total Requests</div>
376
+
<div class="stat-card">
377
+
<div class="stat-number" id="uptime">-</div>
378
+
<div class="stat-label">Uptime</div>
357
-
<div id="loading" class="loading">
358
-
<div class="loading-spinner"></div>
359
-
Loading analytics data...
380
+
<div class="stat-card">
381
+
<div class="stat-number" id="avgResponseTime">-</div>
382
+
<div class="stat-label">Avg Response Time</div>
361
-
<div id="error" class="error" style="display: none"></div>
363
-
<div id="content" style="display: none">
364
-
<!-- Key Metrics -->
365
-
<div class="stats-grid">
366
-
<div class="stat-card">
367
-
<div class="stat-number" id="totalRequests">-</div>
368
-
<div class="stat-label">Total Requests</div>
386
+
<!-- Main Charts -->
387
+
<div class="charts-grid">
388
+
<div class="charts-row">
389
+
<div class="chart-container">
390
+
<div class="chart-title">Requests Over Time</div>
391
+
<canvas id="requestsChart"></canvas>
370
-
<div class="stat-card">
371
-
<div class="stat-number" id="uptime">-</div>
372
-
<div class="stat-label">Uptime</div>
374
-
<div class="stat-card">
375
-
<div class="stat-number" id="avgResponseTime">-</div>
376
-
<div class="stat-label">Avg Response Time</div>
393
+
<div class="chart-container">
394
+
<div class="chart-title">Latency Over Time</div>
395
+
<canvas id="latencyChart"></canvas>
380
-
<!-- Main Charts -->
381
-
<div class="charts-grid">
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>
400
+
<!-- User Agents Table -->
401
+
<div class="user-agents-table">
402
+
<div class="chart-title">User Agents</div>
403
+
<div class="search-container">
404
+
<input type="text" id="userAgentSearch" class="search-input" placeholder="Search user agents...">
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...">
400
-
<div id="userAgentsTable">
401
-
<div class="loading">Loading user agents...</div>
406
+
<div id="userAgentsTable">
407
+
<div class="loading">Loading user agents...</div>
409
-
let autoRefreshInterval;
410
-
const _currentData = null;
411
-
let _isLoading = false;
412
-
let currentRequestId = 0;
413
-
let abortController = null;
415
+
let autoRefreshInterval;
416
+
const _currentData = null;
417
+
let _isLoading = false;
418
+
let currentRequestId = 0;
419
+
let abortController = null;
415
-
// Debounced resize handler for charts
417
-
function handleResize() {
418
-
clearTimeout(resizeTimeout);
419
-
resizeTimeout = setTimeout(() => {
420
-
Object.values(charts).forEach(chart => {
421
-
if (chart && typeof chart.resize === 'function') {
421
+
// Debounced resize handler for charts
423
+
function handleResize() {
424
+
clearTimeout(resizeTimeout);
425
+
resizeTimeout = setTimeout(() => {
426
+
Object.values(charts).forEach(chart => {
427
+
if (chart && typeof chart.resize === 'function') {
428
-
window.addEventListener('resize', handleResize);
434
+
window.addEventListener('resize', handleResize);
430
-
async function loadData() {
431
-
// Cancel any existing requests
432
-
if (abortController) {
433
-
abortController.abort();
436
+
async function loadData() {
437
+
// Cancel any existing requests
438
+
if (abortController) {
439
+
abortController.abort();
436
-
// Create new abort controller for this request
437
-
abortController = new AbortController();
438
-
const requestId = ++currentRequestId;
439
-
const signal = abortController.signal;
442
+
// Create new abort controller for this request
443
+
abortController = new AbortController();
444
+
const requestId = ++currentRequestId;
445
+
const signal = abortController.signal;
442
-
const startTime = Date.now();
448
+
const startTime = Date.now();
444
-
// Capture the days value at the start to ensure consistency
445
-
const days = document.getElementById("daysSelect").value;
446
-
const loading = document.getElementById("loading");
447
-
const error = document.getElementById("error");
448
-
const content = document.getElementById("content");
449
-
const refreshBtn = document.getElementById("refreshBtn");
450
+
// Capture the days value at the start to ensure consistency
451
+
const days = document.getElementById("daysSelect").value;
452
+
const loading = document.getElementById("loading");
453
+
const error = document.getElementById("error");
454
+
const content = document.getElementById("content");
455
+
const refreshBtn = document.getElementById("refreshBtn");
451
-
console.log(`Starting request ${requestId} for ${days} days`);
457
+
console.log(`Starting request ${requestId} for ${days} days`);
454
-
loading.style.display = "block";
455
-
error.style.display = "none";
456
-
content.style.display = "none";
457
-
refreshBtn.disabled = true;
458
-
refreshBtn.textContent = "Loading...";
460
+
loading.style.display = "block";
461
+
error.style.display = "none";
462
+
content.style.display = "none";
463
+
refreshBtn.disabled = true;
464
+
refreshBtn.textContent = "Loading...";
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 });
467
+
// Step 1: Load essential stats first (fastest)
468
+
console.log(`[${requestId}] Loading essential stats...`);
469
+
const essentialResponse = await fetch(`/api/stats/essential?days=${days}`, {signal});
465
-
// Check if this request is still current
466
-
if (requestId !== currentRequestId) {
467
-
console.log(`[${requestId}] Request cancelled (essential stats)`);
471
+
// Check if this request is still current
472
+
if (requestId !== currentRequestId) {
473
+
console.log(`[${requestId}] Request cancelled (essential stats)`);
471
-
if (!essentialResponse.ok) throw new Error(`HTTP ${essentialResponse.status}`);
477
+
if (!essentialResponse.ok) throw new Error(`HTTP ${essentialResponse.status}`);
473
-
const essentialData = await essentialResponse.json();
479
+
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
+
// Double-check we're still the current request
482
+
if (requestId !== currentRequestId) {
483
+
console.log(`[${requestId}] Request cancelled (essential stats after response)`);
481
-
updateEssentialStats(essentialData);
487
+
updateEssentialStats(essentialData);
483
-
// Show content immediately with essential stats
484
-
loading.style.display = "none";
485
-
content.style.display = "block";
486
-
refreshBtn.textContent = "Loading Charts...";
489
+
// Show content immediately with essential stats
490
+
loading.style.display = "none";
491
+
content.style.display = "block";
492
+
refreshBtn.textContent = "Loading Charts...";
488
-
console.log(`[${requestId}] Essential stats loaded in ${Date.now() - startTime}ms`);
494
+
console.log(`[${requestId}] Essential stats loaded in ${Date.now() - startTime}ms`);
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 });
496
+
// Step 2: Load chart data (medium speed)
497
+
console.log(`[${requestId}] Loading chart data...`);
498
+
const chartResponse = await fetch(`/api/stats/charts?days=${days}`, {signal});
494
-
if (requestId !== currentRequestId) {
495
-
console.log(`[${requestId}] Request cancelled (chart data)`);
500
+
if (requestId !== currentRequestId) {
501
+
console.log(`[${requestId}] Request cancelled (chart data)`);
499
-
if (!chartResponse.ok) throw new Error(`HTTP ${chartResponse.status}`);
505
+
if (!chartResponse.ok) throw new Error(`HTTP ${chartResponse.status}`);
501
-
const chartData = await chartResponse.json();
507
+
const chartData = await chartResponse.json();
503
-
if (requestId !== currentRequestId) {
504
-
console.log(`[${requestId}] Request cancelled (chart data after response)`);
509
+
if (requestId !== currentRequestId) {
510
+
console.log(`[${requestId}] Request cancelled (chart data after response)`);
508
-
updateCharts(chartData, parseInt(days, 10));
509
-
refreshBtn.textContent = "Loading User Agents...";
514
+
updateCharts(chartData, parseInt(days, 10));
515
+
refreshBtn.textContent = "Loading User Agents...";
511
-
console.log(`[${requestId}] Charts loaded in ${Date.now() - startTime}ms`);
517
+
console.log(`[${requestId}] Charts loaded in ${Date.now() - startTime}ms`);
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 });
519
+
// Step 3: Load user agents last (slowest)
520
+
console.log(`[${requestId}] Loading user agents...`);
521
+
const userAgentsResponse = await fetch(`/api/stats/useragents?days=${days}`, {signal});
517
-
if (requestId !== currentRequestId) {
518
-
console.log(`[${requestId}] Request cancelled (user agents)`);
523
+
if (requestId !== currentRequestId) {
524
+
console.log(`[${requestId}] Request cancelled (user agents)`);
522
-
if (!userAgentsResponse.ok) throw new Error(`HTTP ${userAgentsResponse.status}`);
528
+
if (!userAgentsResponse.ok) throw new Error(`HTTP ${userAgentsResponse.status}`);
524
-
const userAgentsData = await userAgentsResponse.json();
530
+
const userAgentsData = await userAgentsResponse.json();
526
-
if (requestId !== currentRequestId) {
527
-
console.log(`[${requestId}] Request cancelled (user agents after response)`);
532
+
if (requestId !== currentRequestId) {
533
+
console.log(`[${requestId}] Request cancelled (user agents after response)`);
531
-
updateUserAgentsTable(userAgentsData);
537
+
updateUserAgentsTable(userAgentsData);
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}`);
548
-
// Only update UI if this is still the current request
549
-
if (requestId === currentRequestId) {
550
-
_isLoading = false;
551
-
refreshBtn.disabled = false;
552
-
refreshBtn.textContent = "Refresh";
553
-
abortController = null;
539
+
const totalTime = Date.now() - startTime;
540
+
console.log(`[${requestId}] All data loaded in ${totalTime}ms`);
542
+
// Only show error if this is still the current request
543
+
if (requestId === currentRequestId) {
544
+
if (err.name === 'AbortError') {
545
+
console.log(`[${requestId}] Request aborted`);
547
+
loading.style.display = "none";
548
+
error.style.display = "block";
549
+
error.textContent = `Failed to load data: ${err.message}`;
550
+
console.error(`[${requestId}] Error: ${err.message}`);
554
+
// Only update UI if this is still the current request
555
+
if (requestId === currentRequestId) {
556
+
_isLoading = false;
557
+
refreshBtn.disabled = false;
558
+
refreshBtn.textContent = "Refresh";
559
+
abortController = null;
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";
564
+
// Update just the essential stats (fast)
565
+
function updateEssentialStats(data) {
566
+
document.getElementById("totalRequests").textContent = data.totalRequests.toLocaleString();
567
+
document.getElementById("uptime").textContent = `${data.uptime.toFixed(1)}%`;
568
+
document.getElementById("avgResponseTime").textContent =
569
+
data.averageResponseTime ? `${Math.round(data.averageResponseTime)}ms` : "N/A";
566
-
// Update charts (medium speed)
567
-
function updateCharts(data, days) {
568
-
updateRequestsChart(data.requestsByDay, days === 1);
569
-
updateLatencyChart(data.latencyOverTime, days === 1);
572
+
// Update charts (medium speed)
573
+
function updateCharts(data, days) {
574
+
updateRequestsChart(data.requestsByDay, days === 1);
575
+
updateLatencyChart(data.latencyOverTime, days === 1);
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, 10);
579
+
// Requests Over Time Chart
580
+
function updateRequestsChart(data, _isHourly) {
581
+
const ctx = document.getElementById("requestsChart").getContext("2d");
582
+
const days = parseInt(document.getElementById("daysSelect").value, 10);
578
-
if (charts.requests) charts.requests.destroy();
584
+
if (charts.requests) charts.requests.destroy();
580
-
// Format labels based on granularity
581
-
const labels = data.map((d) => {
583
-
// 15-minute intervals: show just time
584
-
return d.date.split(" ")[1] || d.date;
585
-
} else if (days <= 7) {
586
-
// Hourly: show date + hour
587
-
const parts = d.date.split(" ");
588
-
const date = parts[0].split("-")[2]; // Get day
589
-
const hour = parts[1] || "00:00";
590
-
return `${date} ${hour}`;
592
-
// 4-hour intervals: show abbreviated
593
-
return d.date.split(" ")[0];
586
+
// Format labels based on granularity
587
+
const labels = data.map((d) => {
589
+
// 15-minute intervals: show just time
590
+
return d.date.split(" ")[1] || d.date;
591
+
} else if (days <= 7) {
592
+
// Hourly: show date + hour
593
+
const parts = d.date.split(" ");
594
+
const date = parts[0].split("-")[2]; // Get day
595
+
const hour = parts[1] || "00:00";
596
+
return `${date} ${hour}`;
598
+
// 4-hour intervals: show abbreviated
599
+
return d.date.split(" ")[0];
597
-
charts.requests = new Chart(ctx, {
603
-
data: data.map((d) => d.count),
604
-
borderColor: "#6366f1",
605
-
backgroundColor: "rgba(99, 102, 241, 0.1)",
610
-
pointBackgroundColor: "#6366f1",
615
-
maintainAspectRatio: false,
617
-
legend: { display: false },
620
-
title: (context) => {
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}`;
626
-
label: (context) => `Requests: ${context.parsed.y.toLocaleString()}`
634
-
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
603
+
charts.requests = new Chart(ctx, {
609
+
data: data.map((d) => d.count),
610
+
borderColor: "#6366f1",
611
+
backgroundColor: "rgba(99, 102, 241, 0.1)",
616
+
pointBackgroundColor: "#6366f1",
621
+
maintainAspectRatio: false,
623
+
legend: {display: false},
626
+
title: (context) => {
627
+
const original = data[context[0].dataIndex];
628
+
if (days === 1) return `Time: ${original.date}`;
629
+
if (days <= 7) return `DateTime: ${original.date}`;
630
+
return `Interval: ${original.date}`;
636
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
638
-
maxTicksLimit: days === 1 ? 12 : 20,
632
+
label: (context) => `Requests: ${context.parsed.y.toLocaleString()}`
640
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
644
-
title: { display: true, text: 'Requests' },
646
-
grid: { color: 'rgba(0, 0, 0, 0.05)' }
642
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
644
+
maxTicksLimit: days === 1 ? 12 : 20,
650
+
title: {display: true, text: 'Requests'},
652
+
grid: {color: 'rgba(0, 0, 0, 0.05)'}
653
-
// Latency Over Time Chart
654
-
function updateLatencyChart(data, _isHourly) {
655
-
const ctx = document.getElementById("latencyChart").getContext("2d");
656
-
const days = parseInt(document.getElementById("daysSelect").value, 10);
659
+
// Latency Over Time Chart
660
+
function updateLatencyChart(data, _isHourly) {
661
+
const ctx = document.getElementById("latencyChart").getContext("2d");
662
+
const days = parseInt(document.getElementById("daysSelect").value, 10);
658
-
if (charts.latency) charts.latency.destroy();
664
+
if (charts.latency) charts.latency.destroy();
660
-
// Format labels based on granularity
661
-
const labels = data.map((d) => {
663
-
// 15-minute intervals: show just time
664
-
return d.time.split(" ")[1] || d.time;
665
-
} else if (days <= 7) {
666
-
// Hourly: show date + hour
667
-
const parts = d.time.split(" ");
668
-
const date = parts[0].split("-")[2]; // Get day
669
-
const hour = parts[1] || "00:00";
670
-
return `${date} ${hour}`;
672
-
// 4-hour intervals: show abbreviated
673
-
return d.time.split(" ")[0];
666
+
// Format labels based on granularity
667
+
const labels = data.map((d) => {
669
+
// 15-minute intervals: show just time
670
+
return d.time.split(" ")[1] || d.time;
671
+
} else if (days <= 7) {
672
+
// Hourly: show date + hour
673
+
const parts = d.time.split(" ");
674
+
const date = parts[0].split("-")[2]; // Get day
675
+
const hour = parts[1] || "00:00";
676
+
return `${date} ${hour}`;
678
+
// 4-hour intervals: show abbreviated
679
+
return d.time.split(" ")[0];
677
-
// Calculate dynamic max for logarithmic scale
678
-
const responseTimes = data.map((d) => d.averageResponseTime);
679
-
const maxResponseTime = Math.max(...responseTimes);
683
+
// Calculate dynamic max for logarithmic scale
684
+
const responseTimes = data.map((d) => d.averageResponseTime);
685
+
const maxResponseTime = Math.max(...responseTimes);
681
-
// Calculate appropriate max for log scale (next power of 10)
682
-
const logMax = 10 ** Math.ceil(Math.log10(maxResponseTime));
687
+
// Calculate appropriate max for log scale (next power of 10)
688
+
const logMax = 10 ** Math.ceil(Math.log10(maxResponseTime));
684
-
// Generate dynamic tick values based on the data range
685
-
const generateLogTicks = (_min, max) => {
688
-
while (current <= max) {
689
-
ticks.push(current);
690
+
// Generate dynamic tick values based on the data range
691
+
const generateLogTicks = (_min, max) => {
694
+
while (current <= max) {
695
+
ticks.push(current);
695
-
const dynamicTicks = generateLogTicks(1, logMax);
701
+
const dynamicTicks = generateLogTicks(1, logMax);
697
-
charts.latency = new Chart(ctx, {
702
-
label: "Average Response Time",
703
-
data: responseTimes,
704
-
borderColor: "#10b981",
705
-
backgroundColor: "rgba(16, 185, 129, 0.1)",
710
-
pointBackgroundColor: "#10b981",
715
-
maintainAspectRatio: false,
717
-
legend: { display: false },
720
-
title: (context) => {
721
-
const original = data[context[0].dataIndex];
722
-
if (days === 1) return `Time: ${original.time}`;
723
-
if (days <= 7) return `DateTime: ${original.time}`;
724
-
return `Interval: ${original.time}`;
726
-
label: (context) => {
727
-
const point = data[context.dataIndex];
729
-
`Response Time: ${Math.round(context.parsed.y)}ms`,
730
-
`Request Count: ${point.count.toLocaleString()}`
740
-
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
703
+
charts.latency = new Chart(ctx, {
708
+
label: "Average Response Time",
709
+
data: responseTimes,
710
+
borderColor: "#10b981",
711
+
backgroundColor: "rgba(16, 185, 129, 0.1)",
716
+
pointBackgroundColor: "#10b981",
721
+
maintainAspectRatio: false,
723
+
legend: {display: false},
726
+
title: (context) => {
727
+
const original = data[context[0].dataIndex];
728
+
if (days === 1) return `Time: ${original.time}`;
729
+
if (days <= 7) return `DateTime: ${original.time}`;
730
+
return `Interval: ${original.time}`;
742
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
744
-
maxTicksLimit: days === 1 ? 12 : 20,
732
+
label: (context) => {
733
+
const point = data[context.dataIndex];
735
+
`Response Time: ${Math.round(context.parsed.y)}ms`,
736
+
`Request Count: ${point.count.toLocaleString()}`
746
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
750
-
type: 'logarithmic',
751
-
title: { display: true, text: 'Response Time (ms, log scale)' },
754
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
756
-
callback: (value) => {
757
-
// Show clean numbers based on dynamic range
758
-
if (dynamicTicks.includes(value)) {
759
-
return `${value}ms`;
748
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
750
+
maxTicksLimit: days === 1 ? 12 : 20,
756
+
type: 'logarithmic',
757
+
title: {display: true, text: 'Response Time (ms, log scale)'},
760
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
762
+
callback: (value) => {
763
+
// Show clean numbers based on dynamic range
764
+
if (dynamicTicks.includes(value)) {
765
+
return `${value}ms`;
776
+
// User Agents Table
777
+
let allUserAgents = [];
770
-
// User Agents Table
771
-
let allUserAgents = [];
779
+
function updateUserAgentsTable(userAgents) {
780
+
allUserAgents = userAgents;
781
+
renderUserAgentsTable(userAgents);
782
+
setupUserAgentSearch();
773
-
function updateUserAgentsTable(userAgents) {
774
-
allUserAgents = userAgents;
775
-
renderUserAgentsTable(userAgents);
776
-
setupUserAgentSearch();
785
+
function parseUserAgent(ua) {
786
+
// Keep strange/unique ones as-is
787
+
if (ua.length < 50 ||
788
+
!ua.includes('Mozilla/') ||
789
+
ua.includes('bot') ||
790
+
ua.includes('crawler') ||
791
+
ua.includes('spider') ||
792
+
!ua.includes('AppleWebKit') ||
793
+
ua.includes('Shiba-Arcade') ||
795
+
ua.includes('curl') ||
796
+
ua.includes('python') ||
797
+
ua.includes('PostmanRuntime')) {
779
-
function parseUserAgent(ua) {
780
-
// Keep strange/unique ones as-is
781
-
if (ua.length < 50 ||
782
-
!ua.includes('Mozilla/') ||
783
-
ua.includes('bot') ||
784
-
ua.includes('crawler') ||
785
-
ua.includes('spider') ||
786
-
!ua.includes('AppleWebKit') ||
787
-
ua.includes('Shiba-Arcade') ||
789
-
ua.includes('curl') ||
790
-
ua.includes('python') ||
791
-
ua.includes('PostmanRuntime')) {
801
+
// Parse common browsers
802
+
const os = ua.includes('Macintosh') ? 'macOS' :
803
+
ua.includes('Windows NT 10.0') ? 'Windows 10' :
804
+
ua.includes('Windows NT') ? 'Windows' :
805
+
ua.includes('X11; Linux') ? 'Linux' :
806
+
ua.includes('iPhone') ? 'iOS' :
807
+
ua.includes('Android') ? 'Android' : 'Unknown OS';
809
+
// Detect browser and version
810
+
let browser = 'Unknown Browser';
795
-
// Parse common browsers
796
-
const os = ua.includes('Macintosh') ? 'macOS' :
797
-
ua.includes('Windows NT 10.0') ? 'Windows 10' :
798
-
ua.includes('Windows NT') ? 'Windows' :
799
-
ua.includes('X11; Linux') ? 'Linux' :
800
-
ua.includes('iPhone') ? 'iOS' :
801
-
ua.includes('Android') ? 'Android' : 'Unknown OS';
812
+
if (ua.includes('Edg/')) {
813
+
const match = ua.match(/Edg\/(\d+\.\d+)/);
814
+
const version = match ? match[1] : '';
815
+
browser = `Edge ${version}`;
816
+
} else if (ua.includes('Chrome/')) {
817
+
const match = ua.match(/Chrome\/(\d+\.\d+)/);
818
+
const version = match ? match[1] : '';
819
+
browser = `Chrome ${version}`;
820
+
} else if (ua.includes('Firefox/')) {
821
+
const match = ua.match(/Firefox\/(\d+\.\d+)/);
822
+
const version = match ? match[1] : '';
823
+
browser = `Firefox ${version}`;
824
+
} else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
825
+
browser = 'Safari';
803
-
// Detect browser and version
804
-
let browser = 'Unknown Browser';
828
+
return `${browser} (${os})`;
806
-
if (ua.includes('Edg/')) {
807
-
const match = ua.match(/Edg\/(\d+\.\d+)/);
808
-
const version = match ? match[1] : '';
809
-
browser = `Edge ${version}`;
810
-
} else if (ua.includes('Chrome/')) {
811
-
const match = ua.match(/Chrome\/(\d+\.\d+)/);
812
-
const version = match ? match[1] : '';
813
-
browser = `Chrome ${version}`;
814
-
} else if (ua.includes('Firefox/')) {
815
-
const match = ua.match(/Firefox\/(\d+\.\d+)/);
816
-
const version = match ? match[1] : '';
817
-
browser = `Firefox ${version}`;
818
-
} else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
819
-
browser = 'Safari';
831
+
function renderUserAgentsTable(userAgents) {
832
+
const container = document.getElementById("userAgentsTable");
822
-
return `${browser} (${os})`;
834
+
if (userAgents.length === 0) {
835
+
container.innerHTML = '<div class="no-results">No user agents found</div>';
825
-
function renderUserAgentsTable(userAgents) {
826
-
const container = document.getElementById("userAgentsTable");
839
+
const totalRequests = userAgents.reduce((sum, ua) => sum + ua.count, 0);
828
-
if (userAgents.length === 0) {
829
-
container.innerHTML = '<div class="no-results">No user agents found</div>';
833
-
const totalRequests = userAgents.reduce((sum, ua) => sum + ua.count, 0);
835
-
const tableHTML = `
841
+
const tableHTML = `
···
846
-
const displayName = parseUserAgent(ua.userAgent);
847
-
const percentage = ((ua.count / totalRequests) * 100).toFixed(1);
852
+
const displayName = parseUserAgent(ua.userAgent);
853
+
const percentage = ((ua.count / totalRequests) * 100).toFixed(1);
<div class="ua-name">${displayName}</div>
···
<td class="ua-percentage">${percentage}%</td>
864
-
container.innerHTML = tableHTML;
870
+
container.innerHTML = tableHTML;
867
-
function setupUserAgentSearch() {
868
-
const searchInput = document.getElementById('userAgentSearch');
870
-
searchInput.addEventListener('input', function() {
871
-
const searchTerm = this.value.toLowerCase().trim();
873
+
function setupUserAgentSearch() {
874
+
const searchInput = document.getElementById('userAgentSearch');
873
-
if (searchTerm === '') {
874
-
renderUserAgentsTable(allUserAgents);
876
+
searchInput.addEventListener('input', function () {
877
+
const searchTerm = this.value.toLowerCase().trim();
878
-
const filtered = allUserAgents.filter(ua => {
879
-
const displayName = parseUserAgent(ua.userAgent).toLowerCase();
880
-
const rawUA = ua.userAgent.toLowerCase();
881
-
return displayName.includes(searchTerm) || rawUA.includes(searchTerm);
879
+
if (searchTerm === '') {
880
+
renderUserAgentsTable(allUserAgents);
884
-
renderUserAgentsTable(filtered);
884
+
const filtered = allUserAgents.filter(ua => {
885
+
const displayName = parseUserAgent(ua.userAgent).toLowerCase();
886
+
const rawUA = ua.userAgent.toLowerCase();
887
+
return displayName.includes(searchTerm) || rawUA.includes(searchTerm);
889
-
document.getElementById("autoRefresh").addEventListener("change", function () {
890
-
if (this.checked) {
891
-
autoRefreshInterval = setInterval(loadData, 30000);
893
-
clearInterval(autoRefreshInterval);
890
+
renderUserAgentsTable(filtered);
897
-
document.getElementById("daysSelect").addEventListener("change", loadData);
895
+
document.getElementById("autoRefresh").addEventListener("change", function () {
896
+
if (this.checked) {
897
+
autoRefreshInterval = setInterval(loadData, 30000);
899
+
clearInterval(autoRefreshInterval);
899
-
// Initialize dashboard
900
-
document.addEventListener('DOMContentLoaded', loadData);
903
+
document.getElementById("daysSelect").addEventListener("change", loadData);
902
-
// Cleanup on page unload
903
-
window.addEventListener('beforeunload', () => {
904
-
clearInterval(autoRefreshInterval);
905
-
Object.values(charts).forEach(chart => {
906
-
if (chart && typeof chart.destroy === 'function') {
905
+
// Initialize dashboard
906
+
document.addEventListener('DOMContentLoaded', loadData);
908
+
// Cleanup on page unload
909
+
window.addEventListener('beforeunload', () => {
910
+
clearInterval(autoRefreshInterval);
911
+
Object.values(charts).forEach(chart => {
912
+
if (chart && typeof chart.destroy === 'function') {