a cache for slack profile pictures and emojis

feat: url params

dunkirk.sh f26e9f16 ec0a3bbd

verified
Changed files
+81 -5
src
+81 -5
src/dashboard.html
··· 358 358 <script> 359 359 // State 360 360 let currentDays = 7; 361 + let currentZoom = null; // { start, end } when zoomed 361 362 let chart = null; 363 + let dblClickHandler = null; 362 364 let currentTrafficData = []; 363 365 let allUserAgents = []; 364 366 let abortController = null; 367 + 368 + // URL state management 369 + function getStateFromURL() { 370 + const params = new URLSearchParams(window.location.search); 371 + const days = parseInt(params.get('days')); 372 + const start = parseInt(params.get('start')); 373 + const end = parseInt(params.get('end')); 374 + 375 + return { 376 + days: days && [1, 7, 30, 90, 365].includes(days) ? days : 7, 377 + zoom: start && end ? { start, end } : null 378 + }; 379 + } 380 + 381 + function updateURL() { 382 + const params = new URLSearchParams(); 383 + params.set('days', currentDays); 384 + if (currentZoom) { 385 + params.set('start', currentZoom.start); 386 + params.set('end', currentZoom.end); 387 + } 388 + const newURL = `${window.location.pathname}?${params.toString()}`; 389 + history.replaceState(null, '', newURL); 390 + } 365 391 366 392 // Utilities 367 393 function formatNumber(n) { ··· 467 493 { 468 494 side: 1, 469 495 scale: 'latency', 470 - stroke: '#f97316', 496 + stroke: 'rgba(249, 115, 22, 0.7)', 471 497 grid: { show: false }, 472 498 ticks: { stroke: '#30363d', width: 1 }, 473 499 font: '11px system-ui', ··· 509 535 510 536 chart = new uPlot(opts, [timestamps, hits, latency], container); 511 537 512 - // Double-click to reset zoom 513 - container.addEventListener('dblclick', () => { 538 + // Double-click to reset zoom (remove old handler first) 539 + if (dblClickHandler) { 540 + container.removeEventListener('dblclick', dblClickHandler); 541 + } 542 + dblClickHandler = () => { 543 + currentZoom = null; 544 + updateURL(); 514 545 loadData(); 515 - }); 546 + }; 547 + container.addEventListener('dblclick', dblClickHandler); 516 548 } 517 549 518 550 function handleZoom(minTime, maxTime) { ··· 525 557 minTime = Math.floor(center - minSpan / 2); 526 558 maxTime = Math.floor(center + minSpan / 2); 527 559 } 560 + 561 + currentZoom = { start: Math.floor(minTime), end: Math.floor(maxTime) }; 562 + updateURL(); 528 563 529 564 showLoading(true); 530 565 fetchTrafficData(minTime, maxTime).then(data => { ··· 641 676 document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active')); 642 677 btn.classList.add('active'); 643 678 currentDays = parseInt(btn.dataset.days); 679 + currentZoom = null; // Reset zoom when changing time range 680 + updateURL(); 644 681 loadData(); 645 682 }); 646 683 }); ··· 671 708 }); 672 709 673 710 // Initialize 674 - document.addEventListener('DOMContentLoaded', loadData); 711 + document.addEventListener('DOMContentLoaded', () => { 712 + const state = getStateFromURL(); 713 + currentDays = state.days; 714 + currentZoom = state.zoom; 715 + 716 + // Update active button 717 + document.querySelectorAll('.time-btn').forEach(b => { 718 + b.classList.toggle('active', parseInt(b.dataset.days) === currentDays); 719 + }); 720 + 721 + // Load with zoom if present 722 + if (currentZoom) { 723 + showLoading(true); 724 + Promise.all([ 725 + fetch(`/stats/essential?days=${currentDays}`), 726 + fetchTrafficData(currentZoom.start, currentZoom.end), 727 + fetch('/stats/useragents') 728 + ]).then(async ([statsRes, trafficData, uaRes]) => { 729 + if (trafficData === null) { 730 + showLoading(false); 731 + return; 732 + } 733 + const stats = await statsRes.json(); 734 + const userAgents = await uaRes.json(); 735 + 736 + document.getElementById('totalRequests').textContent = formatNumber(stats.totalRequests || 0); 737 + document.getElementById('avgResponseTime').textContent = formatMs(stats.averageResponseTime); 738 + document.getElementById('uptime').textContent = stats.uptime ? `${stats.uptime.toFixed(1)}%` : '-'; 739 + document.getElementById('uniqueAgents').textContent = formatNumber(userAgents.length || 0); 740 + 741 + currentTrafficData = trafficData; 742 + initChart(trafficData); 743 + allUserAgents = userAgents; 744 + renderUserAgents(userAgents); 745 + showLoading(false); 746 + }); 747 + } else { 748 + loadData(); 749 + } 750 + }); 675 751 </script> 676 752 </body> 677 753