+747
-740
src/dashboard.html
+747
-740
src/dashboard.html
···
1
1
<!doctype html>
2
2
<html lang="en">
3
-
<head>
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>
9
-
<style>
10
-
* {
11
-
margin: 0;
12
-
padding: 0;
13
-
box-sizing: border-box;
14
-
}
15
3
16
-
body {
17
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
18
-
sans-serif;
19
-
background: #f9fafb;
20
-
color: #111827;
21
-
line-height: 1.6;
22
-
}
4
+
<head>
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>
9
+
<script
10
+
src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
11
+
<style>
12
+
* {
13
+
margin: 0;
14
+
padding: 0;
15
+
box-sizing: border-box;
16
+
}
23
17
24
-
.header {
25
-
background: #fff;
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;
30
-
}
18
+
body {
19
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
20
+
sans-serif;
21
+
background: #f9fafb;
22
+
color: #111827;
23
+
line-height: 1.6;
24
+
}
31
25
32
-
.header h1 {
33
-
color: #111827;
34
-
font-size: 1.875rem;
35
-
font-weight: 700;
36
-
margin-bottom: 0.5rem;
37
-
}
26
+
.header {
27
+
background: #fff;
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;
32
+
}
38
33
39
-
.header-links {
40
-
display: flex;
41
-
gap: 1.5rem;
42
-
}
34
+
.header h1 {
35
+
color: #111827;
36
+
font-size: 1.875rem;
37
+
font-weight: 700;
38
+
margin-bottom: 0.5rem;
39
+
}
43
40
44
-
.header-links a {
45
-
color: #6366f1;
46
-
text-decoration: none;
47
-
font-weight: 500;
48
-
}
41
+
.header-links {
42
+
display: flex;
43
+
gap: 1.5rem;
44
+
}
49
45
50
-
.header-links a:hover {
51
-
color: #4f46e5;
52
-
text-decoration: underline;
53
-
}
46
+
.header-links a {
47
+
color: #6366f1;
48
+
text-decoration: none;
49
+
font-weight: 500;
50
+
}
54
51
55
-
.controls {
56
-
margin-bottom: 2rem;
57
-
display: flex;
58
-
justify-content: center;
59
-
align-items: center;
60
-
gap: 1rem;
61
-
flex-wrap: wrap;
62
-
}
52
+
.header-links a:hover {
53
+
color: #4f46e5;
54
+
text-decoration: underline;
55
+
}
63
56
64
-
.controls select,
65
-
.controls button {
66
-
padding: 0.75rem 1.25rem;
67
-
border: 1px solid #d1d5db;
68
-
border-radius: 8px;
69
-
background: white;
70
-
cursor: pointer;
71
-
font-size: 0.875rem;
72
-
font-weight: 500;
73
-
transition: all 0.2s ease;
74
-
}
57
+
.controls {
58
+
margin-bottom: 2rem;
59
+
display: flex;
60
+
justify-content: center;
61
+
align-items: center;
62
+
gap: 1rem;
63
+
flex-wrap: wrap;
64
+
}
75
65
76
-
.controls select:hover,
77
-
.controls select:focus {
78
-
border-color: #6366f1;
79
-
outline: none;
80
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
81
-
}
66
+
.controls select,
67
+
.controls button {
68
+
padding: 0.75rem 1.25rem;
69
+
border: 1px solid #d1d5db;
70
+
border-radius: 8px;
71
+
background: white;
72
+
cursor: pointer;
73
+
font-size: 0.875rem;
74
+
font-weight: 500;
75
+
transition: all 0.2s ease;
76
+
}
82
77
83
-
.controls button {
84
-
background: #6366f1;
85
-
color: white;
86
-
border: none;
87
-
}
78
+
.controls select:hover,
79
+
.controls select:focus {
80
+
border-color: #6366f1;
81
+
outline: none;
82
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
83
+
}
88
84
89
-
.controls button:hover {
90
-
background: #4f46e5;
91
-
}
85
+
.controls button {
86
+
background: #6366f1;
87
+
color: white;
88
+
border: none;
89
+
}
92
90
93
-
.controls button:disabled {
94
-
background: #9ca3af;
95
-
cursor: not-allowed;
96
-
}
91
+
.controls button:hover {
92
+
background: #4f46e5;
93
+
}
97
94
98
-
.dashboard {
99
-
max-width: 1200px;
100
-
margin: 0 auto;
101
-
padding: 0 2rem;
102
-
}
95
+
.controls button:disabled {
96
+
background: #9ca3af;
97
+
cursor: not-allowed;
98
+
}
103
99
104
-
.stats-grid {
105
-
display: grid;
106
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
107
-
gap: 1.5rem;
108
-
margin-bottom: 3rem;
109
-
}
100
+
.dashboard {
101
+
max-width: 1200px;
102
+
margin: 0 auto;
103
+
padding: 0 2rem;
104
+
}
110
105
111
-
.stat-card {
112
-
background: white;
113
-
padding: 2rem;
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;
119
-
min-height: 140px;
120
-
display: flex;
121
-
flex-direction: column;
122
-
justify-content: center;
123
-
}
106
+
.stats-grid {
107
+
display: grid;
108
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
109
+
gap: 1.5rem;
110
+
margin-bottom: 3rem;
111
+
}
112
+
113
+
.stat-card {
114
+
background: white;
115
+
padding: 2rem;
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;
121
+
min-height: 140px;
122
+
display: flex;
123
+
flex-direction: column;
124
+
justify-content: center;
125
+
}
126
+
127
+
.stat-card:hover {
128
+
transform: translateY(-2px);
129
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
130
+
}
131
+
132
+
.stat-number {
133
+
font-weight: 800;
134
+
color: #111827;
135
+
margin-bottom: 0.5rem;
136
+
font-size: 2.5rem;
137
+
line-height: 1;
138
+
}
124
139
125
-
.stat-card:hover {
126
-
transform: translateY(-2px);
127
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
128
-
}
140
+
.stat-label {
141
+
color: #6b7280;
142
+
font-weight: 600;
143
+
font-size: 0.875rem;
144
+
text-transform: uppercase;
145
+
letter-spacing: 0.05em;
146
+
}
129
147
130
-
.stat-number {
131
-
font-weight: 800;
132
-
color: #111827;
133
-
margin-bottom: 0.5rem;
134
-
font-size: 2.5rem;
135
-
line-height: 1;
136
-
}
148
+
.charts-grid {
149
+
display: grid;
150
+
grid-template-columns: 1fr;
151
+
gap: 2rem;
152
+
margin-bottom: 3rem;
153
+
}
137
154
138
-
.stat-label {
139
-
color: #6b7280;
140
-
font-weight: 600;
141
-
font-size: 0.875rem;
142
-
text-transform: uppercase;
143
-
letter-spacing: 0.05em;
144
-
}
155
+
.charts-row {
156
+
display: grid;
157
+
grid-template-columns: 1fr 1fr;
158
+
gap: 2rem;
159
+
}
145
160
146
-
.charts-grid {
147
-
display: grid;
161
+
@media (max-width: 768px) {
162
+
.charts-row {
148
163
grid-template-columns: 1fr;
149
-
gap: 2rem;
150
-
margin-bottom: 3rem;
151
164
}
152
165
153
-
.charts-row {
154
-
display: grid;
155
-
grid-template-columns: 1fr 1fr;
156
-
gap: 2rem;
166
+
.stats-grid {
167
+
grid-template-columns: 1fr;
157
168
}
158
169
159
-
@media (max-width: 768px) {
160
-
.charts-row {
161
-
grid-template-columns: 1fr;
162
-
}
170
+
.dashboard {
171
+
padding: 0 1rem;
172
+
}
163
173
164
-
.stats-grid {
165
-
grid-template-columns: 1fr;
166
-
}
174
+
.stat-number {
175
+
font-size: 2rem;
176
+
}
177
+
}
167
178
168
-
.dashboard {
169
-
padding: 0 1rem;
170
-
}
179
+
.chart-container {
180
+
background: white;
181
+
padding: 1.5rem;
182
+
border-radius: 12px;
183
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
184
+
border: 1px solid #e5e7eb;
185
+
height: 25rem;
186
+
padding-bottom: 5rem;
187
+
}
171
188
172
-
.stat-number {
173
-
font-size: 2rem;
174
-
}
175
-
}
189
+
.chart-title {
190
+
font-size: 1.25rem;
191
+
margin-bottom: 1.5rem;
192
+
color: #111827;
193
+
font-weight: 700;
194
+
}
176
195
177
-
.chart-container {
178
-
background: white;
179
-
padding: 1.5rem;
180
-
border-radius: 12px;
181
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
182
-
border: 1px solid #e5e7eb;
183
-
height: 25rem;
184
-
padding-bottom: 5rem;
185
-
}
196
+
.user-agents-table {
197
+
background: white;
198
+
padding: 2rem;
199
+
border-radius: 12px;
200
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
201
+
border: 1px solid #e5e7eb;
202
+
}
186
203
187
-
.chart-title {
188
-
font-size: 1.25rem;
189
-
margin-bottom: 1.5rem;
190
-
color: #111827;
191
-
font-weight: 700;
192
-
}
204
+
.search-container {
205
+
margin-bottom: 1.5rem;
206
+
position: relative;
207
+
}
193
208
194
-
.user-agents-table {
195
-
background: white;
196
-
padding: 2rem;
197
-
border-radius: 12px;
198
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
199
-
border: 1px solid #e5e7eb;
200
-
}
209
+
.search-input {
210
+
width: 100%;
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;
217
+
}
201
218
202
-
.search-container {
203
-
margin-bottom: 1.5rem;
204
-
position: relative;
205
-
}
219
+
.search-input:focus {
220
+
outline: none;
221
+
border-color: #6366f1;
222
+
background: white;
223
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
224
+
}
206
225
207
-
.search-input {
208
-
width: 100%;
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;
215
-
}
226
+
.ua-table {
227
+
width: 100%;
228
+
border-collapse: collapse;
229
+
font-size: 0.875rem;
230
+
}
216
231
217
-
.search-input:focus {
218
-
outline: none;
219
-
border-color: #6366f1;
220
-
background: white;
221
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
222
-
}
232
+
.ua-table th {
233
+
text-align: left;
234
+
padding: 0.75rem 1rem;
235
+
background: #f9fafb;
236
+
border-bottom: 2px solid #e5e7eb;
237
+
font-weight: 600;
238
+
color: #374151;
239
+
position: sticky;
240
+
top: 0;
241
+
}
223
242
224
-
.ua-table {
225
-
width: 100%;
226
-
border-collapse: collapse;
227
-
font-size: 0.875rem;
228
-
}
243
+
.ua-table td {
244
+
padding: 0.75rem 1rem;
245
+
border-bottom: 1px solid #f3f4f6;
246
+
vertical-align: top;
247
+
}
229
248
230
-
.ua-table th {
231
-
text-align: left;
232
-
padding: 0.75rem 1rem;
233
-
background: #f9fafb;
234
-
border-bottom: 2px solid #e5e7eb;
235
-
font-weight: 600;
236
-
color: #374151;
237
-
position: sticky;
238
-
top: 0;
239
-
}
249
+
.ua-table tbody tr:hover {
250
+
background: #f9fafb;
251
+
}
240
252
241
-
.ua-table td {
242
-
padding: 0.75rem 1rem;
243
-
border-bottom: 1px solid #f3f4f6;
244
-
vertical-align: top;
245
-
}
253
+
.ua-name {
254
+
font-weight: 500;
255
+
color: #111827;
256
+
line-height: 1.4;
257
+
max-width: 400px;
258
+
word-break: break-word;
259
+
}
246
260
247
-
.ua-table tbody tr:hover {
248
-
background: #f9fafb;
249
-
}
261
+
.ua-raw {
262
+
font-family: monospace;
263
+
font-size: 0.75rem;
264
+
color: #6b7280;
265
+
margin-top: 0.25rem;
266
+
max-width: 400px;
267
+
word-break: break-all;
268
+
line-height: 1.3;
269
+
}
250
270
251
-
.ua-name {
252
-
font-weight: 500;
253
-
color: #111827;
254
-
line-height: 1.4;
255
-
max-width: 400px;
256
-
word-break: break-word;
257
-
}
271
+
.ua-count {
272
+
font-weight: 600;
273
+
color: #111827;
274
+
text-align: right;
275
+
white-space: nowrap;
276
+
}
258
277
259
-
.ua-raw {
260
-
font-family: monospace;
261
-
font-size: 0.75rem;
262
-
color: #6b7280;
263
-
margin-top: 0.25rem;
264
-
max-width: 400px;
265
-
word-break: break-all;
266
-
line-height: 1.3;
267
-
}
278
+
.ua-percentage {
279
+
color: #6b7280;
280
+
text-align: right;
281
+
font-size: 0.75rem;
282
+
}
268
283
269
-
.ua-count {
270
-
font-weight: 600;
271
-
color: #111827;
272
-
text-align: right;
273
-
white-space: nowrap;
274
-
}
284
+
.no-results {
285
+
text-align: center;
286
+
padding: 2rem;
287
+
color: #6b7280;
288
+
font-style: italic;
289
+
}
275
290
276
-
.ua-percentage {
277
-
color: #6b7280;
278
-
text-align: right;
279
-
font-size: 0.75rem;
280
-
}
291
+
.loading {
292
+
text-align: center;
293
+
padding: 3rem;
294
+
color: #6b7280;
295
+
}
281
296
282
-
.no-results {
283
-
text-align: center;
284
-
padding: 2rem;
285
-
color: #6b7280;
286
-
font-style: italic;
287
-
}
297
+
.loading-spinner {
298
+
display: inline-block;
299
+
width: 2rem;
300
+
height: 2rem;
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;
306
+
}
288
307
289
-
.loading {
290
-
text-align: center;
291
-
padding: 3rem;
292
-
color: #6b7280;
308
+
@keyframes spin {
309
+
to {
310
+
transform: rotate(360deg);
293
311
}
312
+
}
294
313
295
-
.loading-spinner {
296
-
display: inline-block;
297
-
width: 2rem;
298
-
height: 2rem;
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;
304
-
}
314
+
.error {
315
+
background: #fef2f2;
316
+
color: #dc2626;
317
+
padding: 1rem;
318
+
border-radius: 8px;
319
+
margin: 1rem 0;
320
+
border: 1px solid #fecaca;
321
+
}
305
322
306
-
@keyframes spin {
307
-
to { transform: rotate(360deg); }
308
-
}
323
+
.auto-refresh {
324
+
display: flex;
325
+
align-items: center;
326
+
gap: 0.5rem;
327
+
font-size: 0.875rem;
328
+
color: #6b7280;
329
+
}
309
330
310
-
.error {
311
-
background: #fef2f2;
312
-
color: #dc2626;
313
-
padding: 1rem;
314
-
border-radius: 8px;
315
-
margin: 1rem 0;
316
-
border: 1px solid #fecaca;
317
-
}
331
+
.auto-refresh input[type="checkbox"] {
332
+
transform: scale(1.1);
333
+
accent-color: #6366f1;
334
+
}
335
+
</style>
336
+
</head>
318
337
319
-
.auto-refresh {
320
-
display: flex;
321
-
align-items: center;
322
-
gap: 0.5rem;
323
-
font-size: 0.875rem;
324
-
color: #6b7280;
325
-
}
338
+
<body>
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>
346
+
</div>
347
+
</div>
326
348
327
-
.auto-refresh input[type="checkbox"] {
328
-
transform: scale(1.1);
329
-
accent-color: #6366f1;
330
-
}
331
-
</style>
332
-
</head>
333
-
<body>
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>
355
+
</select>
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>
340
360
</div>
341
361
</div>
342
362
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>
349
-
</select>
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...
366
+
</div>
367
+
<div id="error" class="error" style="display: none"></div>
368
+
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>
375
+
</div>
376
+
<div class="stat-card">
377
+
<div class="stat-number" id="uptime">-</div>
378
+
<div class="stat-label">Uptime</div>
354
379
</div>
355
-
</div>
356
-
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>
383
+
</div>
360
384
</div>
361
-
<div id="error" class="error" style="display: none"></div>
362
385
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>
369
392
</div>
370
-
<div class="stat-card">
371
-
<div class="stat-number" id="uptime">-</div>
372
-
<div class="stat-label">Uptime</div>
373
-
</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>
377
396
</div>
378
397
</div>
398
+
</div>
379
399
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>
386
-
</div>
387
-
<div class="chart-container">
388
-
<div class="chart-title">Latency Over Time</div>
389
-
<canvas id="latencyChart"></canvas>
390
-
</div>
391
-
</div>
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...">
392
405
</div>
393
-
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...">
399
-
</div>
400
-
<div id="userAgentsTable">
401
-
<div class="loading">Loading user agents...</div>
402
-
</div>
406
+
<div id="userAgentsTable">
407
+
<div class="loading">Loading user agents...</div>
403
408
</div>
404
409
</div>
405
410
</div>
411
+
</div>
406
412
407
-
<script>
408
-
const charts = {};
409
-
let autoRefreshInterval;
410
-
const _currentData = null;
411
-
let _isLoading = false;
412
-
let currentRequestId = 0;
413
-
let abortController = null;
413
+
<script>
414
+
const charts = {};
415
+
let autoRefreshInterval;
416
+
const _currentData = null;
417
+
let _isLoading = false;
418
+
let currentRequestId = 0;
419
+
let abortController = null;
414
420
415
-
// Debounced resize handler for charts
416
-
let resizeTimeout;
417
-
function handleResize() {
418
-
clearTimeout(resizeTimeout);
419
-
resizeTimeout = setTimeout(() => {
420
-
Object.values(charts).forEach(chart => {
421
-
if (chart && typeof chart.resize === 'function') {
422
-
chart.resize();
423
-
}
424
-
});
425
-
}, 250);
426
-
}
421
+
// Debounced resize handler for charts
422
+
let resizeTimeout;
423
+
function handleResize() {
424
+
clearTimeout(resizeTimeout);
425
+
resizeTimeout = setTimeout(() => {
426
+
Object.values(charts).forEach(chart => {
427
+
if (chart && typeof chart.resize === 'function') {
428
+
chart.resize();
429
+
}
430
+
});
431
+
}, 250);
432
+
}
427
433
428
-
window.addEventListener('resize', handleResize);
434
+
window.addEventListener('resize', handleResize);
429
435
430
-
async function loadData() {
431
-
// Cancel any existing requests
432
-
if (abortController) {
433
-
abortController.abort();
434
-
}
436
+
async function loadData() {
437
+
// Cancel any existing requests
438
+
if (abortController) {
439
+
abortController.abort();
440
+
}
435
441
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;
440
446
441
-
_isLoading = true;
442
-
const startTime = Date.now();
447
+
_isLoading = true;
448
+
const startTime = Date.now();
443
449
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");
450
456
451
-
console.log(`Starting request ${requestId} for ${days} days`);
457
+
console.log(`Starting request ${requestId} for ${days} days`);
452
458
453
-
// Update UI state
454
-
loading.style.display = "block";
455
-
error.style.display = "none";
456
-
content.style.display = "none";
457
-
refreshBtn.disabled = true;
458
-
refreshBtn.textContent = "Loading...";
459
+
// Update UI state
460
+
loading.style.display = "block";
461
+
error.style.display = "none";
462
+
content.style.display = "none";
463
+
refreshBtn.disabled = true;
464
+
refreshBtn.textContent = "Loading...";
459
465
460
-
try {
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 });
466
+
try {
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});
464
470
465
-
// Check if this request is still current
466
-
if (requestId !== currentRequestId) {
467
-
console.log(`[${requestId}] Request cancelled (essential stats)`);
468
-
return;
469
-
}
471
+
// Check if this request is still current
472
+
if (requestId !== currentRequestId) {
473
+
console.log(`[${requestId}] Request cancelled (essential stats)`);
474
+
return;
475
+
}
470
476
471
-
if (!essentialResponse.ok) throw new Error(`HTTP ${essentialResponse.status}`);
477
+
if (!essentialResponse.ok) throw new Error(`HTTP ${essentialResponse.status}`);
472
478
473
-
const essentialData = await essentialResponse.json();
479
+
const essentialData = await essentialResponse.json();
474
480
475
-
// Double-check we're still the current request
476
-
if (requestId !== currentRequestId) {
477
-
console.log(`[${requestId}] Request cancelled (essential stats after response)`);
478
-
return;
479
-
}
481
+
// Double-check we're still the current request
482
+
if (requestId !== currentRequestId) {
483
+
console.log(`[${requestId}] Request cancelled (essential stats after response)`);
484
+
return;
485
+
}
480
486
481
-
updateEssentialStats(essentialData);
487
+
updateEssentialStats(essentialData);
482
488
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...";
487
493
488
-
console.log(`[${requestId}] Essential stats loaded in ${Date.now() - startTime}ms`);
494
+
console.log(`[${requestId}] Essential stats loaded in ${Date.now() - startTime}ms`);
489
495
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});
493
499
494
-
if (requestId !== currentRequestId) {
495
-
console.log(`[${requestId}] Request cancelled (chart data)`);
496
-
return;
497
-
}
500
+
if (requestId !== currentRequestId) {
501
+
console.log(`[${requestId}] Request cancelled (chart data)`);
502
+
return;
503
+
}
498
504
499
-
if (!chartResponse.ok) throw new Error(`HTTP ${chartResponse.status}`);
505
+
if (!chartResponse.ok) throw new Error(`HTTP ${chartResponse.status}`);
500
506
501
-
const chartData = await chartResponse.json();
507
+
const chartData = await chartResponse.json();
502
508
503
-
if (requestId !== currentRequestId) {
504
-
console.log(`[${requestId}] Request cancelled (chart data after response)`);
505
-
return;
506
-
}
509
+
if (requestId !== currentRequestId) {
510
+
console.log(`[${requestId}] Request cancelled (chart data after response)`);
511
+
return;
512
+
}
507
513
508
-
updateCharts(chartData, parseInt(days, 10));
509
-
refreshBtn.textContent = "Loading User Agents...";
514
+
updateCharts(chartData, parseInt(days, 10));
515
+
refreshBtn.textContent = "Loading User Agents...";
510
516
511
-
console.log(`[${requestId}] Charts loaded in ${Date.now() - startTime}ms`);
517
+
console.log(`[${requestId}] Charts loaded in ${Date.now() - startTime}ms`);
512
518
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});
516
522
517
-
if (requestId !== currentRequestId) {
518
-
console.log(`[${requestId}] Request cancelled (user agents)`);
519
-
return;
520
-
}
523
+
if (requestId !== currentRequestId) {
524
+
console.log(`[${requestId}] Request cancelled (user agents)`);
525
+
return;
526
+
}
521
527
522
-
if (!userAgentsResponse.ok) throw new Error(`HTTP ${userAgentsResponse.status}`);
528
+
if (!userAgentsResponse.ok) throw new Error(`HTTP ${userAgentsResponse.status}`);
523
529
524
-
const userAgentsData = await userAgentsResponse.json();
530
+
const userAgentsData = await userAgentsResponse.json();
525
531
526
-
if (requestId !== currentRequestId) {
527
-
console.log(`[${requestId}] Request cancelled (user agents after response)`);
528
-
return;
529
-
}
532
+
if (requestId !== currentRequestId) {
533
+
console.log(`[${requestId}] Request cancelled (user agents after response)`);
534
+
return;
535
+
}
530
536
531
-
updateUserAgentsTable(userAgentsData);
537
+
updateUserAgentsTable(userAgentsData);
532
538
533
-
const totalTime = Date.now() - startTime;
534
-
console.log(`[${requestId}] All data loaded in ${totalTime}ms`);
535
-
} catch (err) {
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`);
540
-
} else {
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}`);
545
-
}
546
-
}
547
-
} finally {
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`);
541
+
} catch (err) {
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`);
546
+
} else {
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
551
}
552
+
}
553
+
} finally {
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;
555
560
}
556
561
}
562
+
}
557
563
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
-
}
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";
570
+
}
565
571
566
-
// Update charts (medium speed)
567
-
function updateCharts(data, days) {
568
-
updateRequestsChart(data.requestsByDay, days === 1);
569
-
updateLatencyChart(data.latencyOverTime, days === 1);
570
-
}
572
+
// Update charts (medium speed)
573
+
function updateCharts(data, days) {
574
+
updateRequestsChart(data.requestsByDay, days === 1);
575
+
updateLatencyChart(data.latencyOverTime, days === 1);
576
+
}
571
577
572
578
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);
577
583
578
-
if (charts.requests) charts.requests.destroy();
584
+
if (charts.requests) charts.requests.destroy();
579
585
580
-
// Format labels based on granularity
581
-
const labels = data.map((d) => {
582
-
if (days === 1) {
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}`;
591
-
} else {
592
-
// 4-hour intervals: show abbreviated
593
-
return d.date.split(" ")[0];
594
-
}
595
-
});
586
+
// Format labels based on granularity
587
+
const labels = data.map((d) => {
588
+
if (days === 1) {
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}`;
597
+
} else {
598
+
// 4-hour intervals: show abbreviated
599
+
return d.date.split(" ")[0];
600
+
}
601
+
});
596
602
597
-
charts.requests = new Chart(ctx, {
598
-
type: "line",
599
-
data: {
600
-
labels: labels,
601
-
datasets: [{
602
-
label: "Requests",
603
-
data: data.map((d) => d.count),
604
-
borderColor: "#6366f1",
605
-
backgroundColor: "rgba(99, 102, 241, 0.1)",
606
-
tension: 0.4,
607
-
fill: true,
608
-
borderWidth: 1.5,
609
-
pointRadius: 1,
610
-
pointBackgroundColor: "#6366f1",
611
-
}],
612
-
},
613
-
options: {
614
-
responsive: true,
615
-
maintainAspectRatio: false,
616
-
plugins: {
617
-
legend: { display: false },
618
-
tooltip: {
619
-
callbacks: {
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}`;
625
-
},
626
-
label: (context) => `Requests: ${context.parsed.y.toLocaleString()}`
627
-
}
628
-
}
629
-
},
630
-
scales: {
631
-
x: {
632
-
title: {
633
-
display: true,
634
-
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
603
+
charts.requests = new Chart(ctx, {
604
+
type: "line",
605
+
data: {
606
+
labels: labels,
607
+
datasets: [{
608
+
label: "Requests",
609
+
data: data.map((d) => d.count),
610
+
borderColor: "#6366f1",
611
+
backgroundColor: "rgba(99, 102, 241, 0.1)",
612
+
tension: 0.4,
613
+
fill: true,
614
+
borderWidth: 1.5,
615
+
pointRadius: 1,
616
+
pointBackgroundColor: "#6366f1",
617
+
}],
618
+
},
619
+
options: {
620
+
responsive: true,
621
+
maintainAspectRatio: false,
622
+
plugins: {
623
+
legend: {display: false},
624
+
tooltip: {
625
+
callbacks: {
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}`;
635
631
},
636
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
637
-
ticks: {
638
-
maxTicksLimit: days === 1 ? 12 : 20,
639
-
maxRotation: 0,
640
-
minRotation: 0
641
-
}
632
+
label: (context) => `Requests: ${context.parsed.y.toLocaleString()}`
633
+
}
634
+
}
635
+
},
636
+
scales: {
637
+
x: {
638
+
title: {
639
+
display: true,
640
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
642
641
},
643
-
y: {
644
-
title: { display: true, text: 'Requests' },
645
-
beginAtZero: true,
646
-
grid: { color: 'rgba(0, 0, 0, 0.05)' }
642
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
643
+
ticks: {
644
+
maxTicksLimit: days === 1 ? 12 : 20,
645
+
maxRotation: 0,
646
+
minRotation: 0
647
647
}
648
+
},
649
+
y: {
650
+
title: {display: true, text: 'Requests'},
651
+
beginAtZero: true,
652
+
grid: {color: 'rgba(0, 0, 0, 0.05)'}
648
653
}
649
654
}
650
-
});
651
-
}
655
+
}
656
+
});
657
+
}
652
658
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);
657
663
658
-
if (charts.latency) charts.latency.destroy();
664
+
if (charts.latency) charts.latency.destroy();
659
665
660
-
// Format labels based on granularity
661
-
const labels = data.map((d) => {
662
-
if (days === 1) {
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}`;
671
-
} else {
672
-
// 4-hour intervals: show abbreviated
673
-
return d.time.split(" ")[0];
674
-
}
675
-
});
666
+
// Format labels based on granularity
667
+
const labels = data.map((d) => {
668
+
if (days === 1) {
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}`;
677
+
} else {
678
+
// 4-hour intervals: show abbreviated
679
+
return d.time.split(" ")[0];
680
+
}
681
+
});
676
682
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);
680
686
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));
683
689
684
-
// Generate dynamic tick values based on the data range
685
-
const generateLogTicks = (_min, max) => {
686
-
const ticks = [];
687
-
let current = 1;
688
-
while (current <= max) {
689
-
ticks.push(current);
690
-
current *= 10;
691
-
}
692
-
return ticks;
693
-
};
690
+
// Generate dynamic tick values based on the data range
691
+
const generateLogTicks = (_min, max) => {
692
+
const ticks = [];
693
+
let current = 1;
694
+
while (current <= max) {
695
+
ticks.push(current);
696
+
current *= 10;
697
+
}
698
+
return ticks;
699
+
};
694
700
695
-
const dynamicTicks = generateLogTicks(1, logMax);
701
+
const dynamicTicks = generateLogTicks(1, logMax);
696
702
697
-
charts.latency = new Chart(ctx, {
698
-
type: "line",
699
-
data: {
700
-
labels: labels,
701
-
datasets: [{
702
-
label: "Average Response Time",
703
-
data: responseTimes,
704
-
borderColor: "#10b981",
705
-
backgroundColor: "rgba(16, 185, 129, 0.1)",
706
-
tension: 0.4,
707
-
fill: true,
708
-
borderWidth: 1.5,
709
-
pointRadius: 1,
710
-
pointBackgroundColor: "#10b981",
711
-
}],
712
-
},
713
-
options: {
714
-
responsive: true,
715
-
maintainAspectRatio: false,
716
-
plugins: {
717
-
legend: { display: false },
718
-
tooltip: {
719
-
callbacks: {
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}`;
725
-
},
726
-
label: (context) => {
727
-
const point = data[context.dataIndex];
728
-
return [
729
-
`Response Time: ${Math.round(context.parsed.y)}ms`,
730
-
`Request Count: ${point.count.toLocaleString()}`
731
-
];
732
-
}
733
-
}
734
-
}
735
-
},
736
-
scales: {
737
-
x: {
738
-
title: {
739
-
display: true,
740
-
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
703
+
charts.latency = new Chart(ctx, {
704
+
type: "line",
705
+
data: {
706
+
labels: labels,
707
+
datasets: [{
708
+
label: "Average Response Time",
709
+
data: responseTimes,
710
+
borderColor: "#10b981",
711
+
backgroundColor: "rgba(16, 185, 129, 0.1)",
712
+
tension: 0.4,
713
+
fill: true,
714
+
borderWidth: 1.5,
715
+
pointRadius: 1,
716
+
pointBackgroundColor: "#10b981",
717
+
}],
718
+
},
719
+
options: {
720
+
responsive: true,
721
+
maintainAspectRatio: false,
722
+
plugins: {
723
+
legend: {display: false},
724
+
tooltip: {
725
+
callbacks: {
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}`;
741
731
},
742
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
743
-
ticks: {
744
-
maxTicksLimit: days === 1 ? 12 : 20,
745
-
maxRotation: 0,
746
-
minRotation: 0
732
+
label: (context) => {
733
+
const point = data[context.dataIndex];
734
+
return [
735
+
`Response Time: ${Math.round(context.parsed.y)}ms`,
736
+
`Request Count: ${point.count.toLocaleString()}`
737
+
];
747
738
}
739
+
}
740
+
}
741
+
},
742
+
scales: {
743
+
x: {
744
+
title: {
745
+
display: true,
746
+
text: days === 1 ? 'Time (15min intervals)' : days <= 7 ? 'Time (hourly)' : 'Time (4hr intervals)'
748
747
},
749
-
y: {
750
-
type: 'logarithmic',
751
-
title: { display: true, text: 'Response Time (ms, log scale)' },
752
-
min: 1,
753
-
max: logMax,
754
-
grid: { color: 'rgba(0, 0, 0, 0.05)' },
755
-
ticks: {
756
-
callback: (value) => {
757
-
// Show clean numbers based on dynamic range
758
-
if (dynamicTicks.includes(value)) {
759
-
return `${value}ms`;
760
-
}
761
-
return '';
748
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
749
+
ticks: {
750
+
maxTicksLimit: days === 1 ? 12 : 20,
751
+
maxRotation: 0,
752
+
minRotation: 0
753
+
}
754
+
},
755
+
y: {
756
+
type: 'logarithmic',
757
+
title: {display: true, text: 'Response Time (ms, log scale)'},
758
+
min: 1,
759
+
max: logMax,
760
+
grid: {color: 'rgba(0, 0, 0, 0.05)'},
761
+
ticks: {
762
+
callback: (value) => {
763
+
// Show clean numbers based on dynamic range
764
+
if (dynamicTicks.includes(value)) {
765
+
return `${value}ms`;
762
766
}
767
+
return '';
763
768
}
764
769
}
765
770
}
766
771
}
767
-
});
768
-
}
772
+
}
773
+
});
774
+
}
775
+
776
+
// User Agents Table
777
+
let allUserAgents = [];
769
778
770
-
// User Agents Table
771
-
let allUserAgents = [];
779
+
function updateUserAgentsTable(userAgents) {
780
+
allUserAgents = userAgents;
781
+
renderUserAgentsTable(userAgents);
782
+
setupUserAgentSearch();
783
+
}
772
784
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') ||
794
+
ua === 'node' ||
795
+
ua.includes('curl') ||
796
+
ua.includes('python') ||
797
+
ua.includes('PostmanRuntime')) {
798
+
return ua;
777
799
}
778
800
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') ||
788
-
ua === 'node' ||
789
-
ua.includes('curl') ||
790
-
ua.includes('python') ||
791
-
ua.includes('PostmanRuntime')) {
792
-
return ua;
793
-
}
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';
808
+
809
+
// Detect browser and version
810
+
let browser = 'Unknown Browser';
794
811
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';
826
+
}
802
827
803
-
// Detect browser and version
804
-
let browser = 'Unknown Browser';
828
+
return `${browser} (${os})`;
829
+
}
805
830
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';
820
-
}
831
+
function renderUserAgentsTable(userAgents) {
832
+
const container = document.getElementById("userAgentsTable");
821
833
822
-
return `${browser} (${os})`;
834
+
if (userAgents.length === 0) {
835
+
container.innerHTML = '<div class="no-results">No user agents found</div>';
836
+
return;
823
837
}
824
838
825
-
function renderUserAgentsTable(userAgents) {
826
-
const container = document.getElementById("userAgentsTable");
839
+
const totalRequests = userAgents.reduce((sum, ua) => sum + ua.count, 0);
827
840
828
-
if (userAgents.length === 0) {
829
-
container.innerHTML = '<div class="no-results">No user agents found</div>';
830
-
return;
831
-
}
832
-
833
-
const totalRequests = userAgents.reduce((sum, ua) => sum + ua.count, 0);
834
-
835
-
const tableHTML = `
841
+
const tableHTML = `
836
842
<table class="ua-table">
837
843
<thead>
838
844
<tr>
···
843
849
</thead>
844
850
<tbody>
845
851
${userAgents.map(ua => {
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);
848
854
849
-
return `
855
+
return `
850
856
<tr>
851
857
<td>
852
858
<div class="ua-name">${displayName}</div>
···
856
862
<td class="ua-percentage">${percentage}%</td>
857
863
</tr>
858
864
`;
859
-
}).join('')}
865
+
}).join('')}
860
866
</tbody>
861
867
</table>
862
868
`;
863
869
864
-
container.innerHTML = tableHTML;
865
-
}
870
+
container.innerHTML = tableHTML;
871
+
}
866
872
867
-
function setupUserAgentSearch() {
868
-
const searchInput = document.getElementById('userAgentSearch');
869
-
870
-
searchInput.addEventListener('input', function() {
871
-
const searchTerm = this.value.toLowerCase().trim();
873
+
function setupUserAgentSearch() {
874
+
const searchInput = document.getElementById('userAgentSearch');
872
875
873
-
if (searchTerm === '') {
874
-
renderUserAgentsTable(allUserAgents);
875
-
return;
876
-
}
876
+
searchInput.addEventListener('input', function () {
877
+
const searchTerm = this.value.toLowerCase().trim();
877
878
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);
882
-
});
879
+
if (searchTerm === '') {
880
+
renderUserAgentsTable(allUserAgents);
881
+
return;
882
+
}
883
883
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);
885
888
});
886
-
}
887
889
888
-
// Event Handlers
889
-
document.getElementById("autoRefresh").addEventListener("change", function () {
890
-
if (this.checked) {
891
-
autoRefreshInterval = setInterval(loadData, 30000);
892
-
} else {
893
-
clearInterval(autoRefreshInterval);
894
-
}
890
+
renderUserAgentsTable(filtered);
895
891
});
892
+
}
896
893
897
-
document.getElementById("daysSelect").addEventListener("change", loadData);
894
+
// Event Handlers
895
+
document.getElementById("autoRefresh").addEventListener("change", function () {
896
+
if (this.checked) {
897
+
autoRefreshInterval = setInterval(loadData, 30000);
898
+
} else {
899
+
clearInterval(autoRefreshInterval);
900
+
}
901
+
});
898
902
899
-
// Initialize dashboard
900
-
document.addEventListener('DOMContentLoaded', loadData);
903
+
document.getElementById("daysSelect").addEventListener("change", loadData);
901
904
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') {
907
-
chart.destroy();
908
-
}
909
-
});
905
+
// Initialize dashboard
906
+
document.addEventListener('DOMContentLoaded', loadData);
907
+
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') {
913
+
chart.destroy();
914
+
}
910
915
});
911
-
</script>
912
-
</body>
913
-
</html>
916
+
});
917
+
</script>
918
+
</body>
919
+
920
+
</html>