+126
-18
src/dashboard.html
+126
-18
src/dashboard.html
···
199
199
padding: 1.5rem;
200
200
}
201
201
202
+
.section-header {
203
+
display: flex;
204
+
justify-content: space-between;
205
+
align-items: center;
206
+
margin-bottom: 1rem;
207
+
}
208
+
202
209
.section-title {
203
210
color: #f0f6fc;
204
211
font-size: 1rem;
205
212
font-weight: 600;
213
+
}
214
+
215
+
.last-updated {
216
+
color: #6e7681;
217
+
font-size: 0.75rem;
218
+
}
219
+
220
+
.ua-summary {
221
+
display: flex;
222
+
flex-wrap: wrap;
223
+
gap: 0.5rem;
206
224
margin-bottom: 1rem;
225
+
}
226
+
227
+
.ua-tag {
228
+
display: inline-flex;
229
+
align-items: center;
230
+
gap: 0.375rem;
231
+
padding: 0.25rem 0.5rem;
232
+
background: #21262d;
233
+
border-radius: 4px;
234
+
font-size: 0.75rem;
235
+
color: #c9d1d9;
236
+
}
237
+
238
+
.ua-tag-count {
239
+
color: #8b949e;
207
240
}
208
241
209
242
.search-input {
···
349
382
</div>
350
383
351
384
<div class="user-agents-section">
352
-
<div class="section-title">Top User Agents</div>
385
+
<div class="section-header">
386
+
<div class="section-title">Traffic Sources</div>
387
+
<div class="last-updated" id="lastUpdated"></div>
388
+
</div>
389
+
<div class="ua-summary" id="uaSummary"></div>
353
390
<input type="text" class="search-input" id="uaSearch" placeholder="Search user agents...">
354
391
<div class="ua-list" id="uaList"></div>
355
392
</div>
···
403
440
return `${(ms / 1000).toFixed(2)}s`;
404
441
}
405
442
406
-
function parseUserAgent(ua) {
407
-
if (!ua) return 'Unknown';
408
-
if (ua.length < 50 || !ua.includes('Mozilla/') || ua.includes('bot') || ua.includes('curl')) {
409
-
return ua.length > 60 ? ua.substring(0, 57) + '...' : ua;
443
+
function classifyUserAgent(ua) {
444
+
if (!ua) return { browser: 'Unknown', os: 'Unknown', type: 'unknown' };
445
+
446
+
// Real browsers have Mozilla/5.0 and specific browser/engine identifiers
447
+
const isBrowser = ua.includes('Mozilla/5.0') && (
448
+
ua.includes('Chrome/') ||
449
+
ua.includes('Firefox/') ||
450
+
ua.includes('Safari/') ||
451
+
ua.includes('Edg/') ||
452
+
ua.includes('OPR/') ||
453
+
ua.includes('AppleWebKit/')
454
+
);
455
+
456
+
if (!isBrowser) {
457
+
return { browser: 'Other', os: '-', type: 'other' };
410
458
}
411
459
412
460
const os = ua.includes('Macintosh') ? 'macOS' :
413
461
ua.includes('Windows') ? 'Windows' :
414
-
ua.includes('Linux') ? 'Linux' :
415
-
ua.includes('iPhone') ? 'iOS' :
416
-
ua.includes('Android') ? 'Android' : '';
462
+
ua.includes('Linux') && !ua.includes('Android') ? 'Linux' :
463
+
ua.includes('iPhone') || ua.includes('iPad') ? 'iOS' :
464
+
ua.includes('Android') ? 'Android' : 'Other';
417
465
418
-
let browser = '';
466
+
let browser = 'Other';
419
467
if (ua.includes('Edg/')) browser = 'Edge';
420
-
else if (ua.includes('Chrome/')) browser = 'Chrome';
468
+
else if (ua.includes('OPR/') || ua.includes('Opera')) browser = 'Opera';
469
+
else if (ua.includes('Chrome/') && !ua.includes('Edg/')) browser = 'Chrome';
421
470
else if (ua.includes('Firefox/')) browser = 'Firefox';
422
471
else if (ua.includes('Safari/') && !ua.includes('Chrome')) browser = 'Safari';
423
472
424
-
if (browser && os) return `${browser} (${os})`;
425
-
if (browser) return browser;
473
+
return { browser, os, type: 'browser' };
474
+
}
475
+
476
+
function parseUserAgent(ua) {
477
+
const { browser, os, type } = classifyUserAgent(ua);
478
+
if (type !== 'browser') return browser;
479
+
if (browser !== 'Other' && os !== 'Other') return `${browser} (${os})`;
480
+
if (browser !== 'Other') return browser;
481
+
if (!ua) return 'Unknown';
426
482
return ua.length > 60 ? ua.substring(0, 57) + '...' : ua;
483
+
}
484
+
485
+
function computeUASummary(agents) {
486
+
const browsers = {};
487
+
const oses = {};
488
+
const types = { browser: 0, bot: 0, cli: 0, social: 0, unknown: 0 };
489
+
490
+
for (const agent of agents) {
491
+
const hits = agent.hits || agent.count || 1;
492
+
const { browser, os, type } = classifyUserAgent(agent.userAgent);
493
+
494
+
browsers[browser] = (browsers[browser] || 0) + hits;
495
+
if (os !== '-') oses[os] = (oses[os] || 0) + hits;
496
+
types[type] = (types[type] || 0) + hits;
497
+
}
498
+
499
+
return {
500
+
browsers: Object.entries(browsers).sort((a, b) => b[1] - a[1]).slice(0, 5),
501
+
oses: Object.entries(oses).sort((a, b) => b[1] - a[1]).slice(0, 4),
502
+
types
503
+
};
504
+
}
505
+
506
+
function updateLastUpdated() {
507
+
const el = document.getElementById('lastUpdated');
508
+
const now = new Date();
509
+
el.textContent = `Updated ${now.toLocaleTimeString()}`;
427
510
}
428
511
429
512
// Chart
···
641
724
// Update user agents
642
725
allUserAgents = userAgents;
643
726
renderUserAgents(userAgents);
727
+
updateLastUpdated();
644
728
645
729
} catch (e) {
646
730
console.error('Error loading data:', e);
···
650
734
}
651
735
652
736
// User agents
653
-
function renderUserAgents(agents) {
737
+
function renderUserAgents(agents, showSummary = true) {
654
738
const list = document.getElementById('uaList');
739
+
const summaryEl = document.getElementById('uaSummary');
655
740
656
741
if (!agents || agents.length === 0) {
657
742
list.innerHTML = '<div style="color: #8b949e; padding: 1rem 0;">No user agents found</div>';
743
+
summaryEl.innerHTML = '';
658
744
return;
659
745
}
660
746
661
-
list.innerHTML = agents.slice(0, 50).map((ua, i) => {
747
+
// Render summary (only for full list, not filtered)
748
+
if (showSummary) {
749
+
const summary = computeUASummary(agents);
750
+
const tags = [
751
+
...summary.browsers.map(([name, count]) =>
752
+
`<span class="ua-tag">${name} <span class="ua-tag-count">${formatNumber(count)}</span></span>`
753
+
),
754
+
...summary.oses.map(([name, count]) =>
755
+
`<span class="ua-tag">${name} <span class="ua-tag-count">${formatNumber(count)}</span></span>`
756
+
)
757
+
];
758
+
summaryEl.innerHTML = tags.join('');
759
+
}
760
+
761
+
// Filter out standard browsers, keep bots/CLI/social/other
762
+
const nonBrowserAgents = agents.filter(ua => {
763
+
const { type } = classifyUserAgent(ua.userAgent);
764
+
return type !== 'browser';
765
+
});
766
+
767
+
list.innerHTML = nonBrowserAgents.slice(0, 50).map((ua, i) => {
662
768
const rankClass = i < 3 ? `top-${i + 1}` : '';
769
+
const display = ua.userAgent?.length > 60 ? ua.userAgent.substring(0, 57) + '...' : (ua.userAgent || 'Unknown');
663
770
return `
664
771
<div class="ua-item">
665
772
<span class="ua-rank ${rankClass}">${i + 1}</span>
666
-
<span class="ua-name" title="${ua.userAgent}">${parseUserAgent(ua.userAgent)}</span>
773
+
<span class="ua-name" title="${ua.userAgent}">${display}</span>
667
774
<span class="ua-count">${formatNumber(ua.hits || ua.count)}</span>
668
775
</div>
669
776
`;
670
-
}).join('');
777
+
}).join('') || '<div style="color: #8b949e; padding: 1rem 0;">No non-browser agents found</div>';
671
778
}
672
779
673
780
// Event listeners
···
685
792
document.getElementById('uaSearch').addEventListener('input', (e) => {
686
793
const term = e.target.value.toLowerCase();
687
794
if (!term) {
688
-
renderUserAgents(allUserAgents);
795
+
renderUserAgents(allUserAgents, true);
689
796
return;
690
797
}
691
798
const filtered = allUserAgents.filter(ua =>
692
799
ua.userAgent.toLowerCase().includes(term) ||
693
800
parseUserAgent(ua.userAgent).toLowerCase().includes(term)
694
801
);
695
-
renderUserAgents(filtered);
802
+
renderUserAgents(filtered, false);
696
803
});
697
804
698
805
// Handle resize
···
742
849
initChart(trafficData);
743
850
allUserAgents = userAgents;
744
851
renderUserAgents(userAgents);
852
+
updateLastUpdated();
745
853
showLoading(false);
746
854
});
747
855
} else {