a cache for slack profile pictures and emojis

feat: tidy user agents

dunkirk.sh ac0ae74d f26e9f16

verified
Changed files
+126 -18
src
+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 {