a cache for slack profile pictures and emojis
1<!doctype html> 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"></script> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 } 14 15 body { 16 font-family: 17 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 18 sans-serif; 19 background: #f5f5f5; 20 color: #333; 21 } 22 23 .header { 24 background: #fff; 25 padding: 1rem 2rem; 26 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 27 margin-bottom: 2rem; 28 display: flex; 29 justify-content: space-between; 30 align-items: center; 31 } 32 33 .header h1 { 34 color: #2c3e50; 35 } 36 37 .header-links a { 38 margin-left: 1rem; 39 color: #3498db; 40 text-decoration: none; 41 } 42 43 .header-links a:hover { 44 text-decoration: underline; 45 } 46 47 .controls { 48 margin-bottom: 2rem; 49 text-align: center; 50 } 51 52 .controls select, 53 .controls button { 54 padding: 0.5rem 1rem; 55 margin: 0 0.5rem; 56 border: 1px solid #ddd; 57 border-radius: 4px; 58 background: white; 59 cursor: pointer; 60 } 61 62 .controls button { 63 background: #3498db; 64 color: white; 65 border: none; 66 } 67 68 .controls button:hover { 69 background: #2980b9; 70 } 71 72 .dashboard { 73 max-width: 1200px; 74 margin: 0 auto; 75 padding: 0 2rem; 76 } 77 78 .stats-grid { 79 display: grid; 80 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 81 gap: 1rem; 82 margin-bottom: 2rem; 83 } 84 85 .stat-card { 86 background: white; 87 padding: 1.5rem; 88 border-radius: 8px; 89 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 90 text-align: center; 91 } 92 93 .stat-number { 94 font-size: 2rem; 95 font-weight: bold; 96 color: #3498db; 97 } 98 99 .stat-label { 100 color: #666; 101 margin-top: 0.5rem; 102 } 103 104 .charts-grid { 105 display: grid; 106 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); 107 gap: 2rem; 108 margin-bottom: 2rem; 109 } 110 111 .chart-container { 112 background: white; 113 padding: 1.5rem; 114 border-radius: 8px; 115 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 116 } 117 118 .chart-title { 119 font-size: 1.2rem; 120 margin-bottom: 1rem; 121 color: #2c3e50; 122 } 123 124 .loading { 125 text-align: center; 126 padding: 2rem; 127 color: #666; 128 } 129 130 .error { 131 background: #e74c3c; 132 color: white; 133 padding: 1rem; 134 border-radius: 4px; 135 margin: 1rem 0; 136 } 137 138 .auto-refresh { 139 display: flex; 140 align-items: center; 141 gap: 0.5rem; 142 justify-content: center; 143 margin-top: 1rem; 144 } 145 146 .auto-refresh input[type="checkbox"] { 147 transform: scale(1.2); 148 } 149 150 @media (max-width: 768px) { 151 .charts-grid { 152 grid-template-columns: 1fr; 153 } 154 155 .dashboard { 156 padding: 0 1rem; 157 } 158 159 .header { 160 flex-direction: column; 161 gap: 1rem; 162 text-align: center; 163 } 164 } 165 </style> 166 </head> 167 <body> 168 <div class="header"> 169 <h1>📊 Cachet Analytics Dashboard</h1> 170 <div class="header-links"> 171 <a href="/swagger">API Docs</a> 172 <a href="/stats">Raw Stats</a> 173 </div> 174 </div> 175 176 <div class="dashboard"> 177 <div class="controls"> 178 <select id="daysSelect"> 179 <option value="1">Last 24 hours</option> 180 <option value="7" selected>Last 7 days</option> 181 <option value="30">Last 30 days</option> 182 </select> 183 <button onclick="loadData()">Refresh</button> 184 <div class="auto-refresh"> 185 <input type="checkbox" id="autoRefresh" /> 186 <label for="autoRefresh">Auto-refresh (30s)</label> 187 </div> 188 </div> 189 190 <div id="loading" class="loading">Loading analytics data...</div> 191 <div id="error" class="error" style="display: none"></div> 192 193 <div id="content" style="display: none"> 194 <div 195 class="chart-container" 196 style="margin-bottom: 2rem; height: 450px" 197 > 198 <div class="chart-title"> 199 Traffic Overview - All Routes Over Time 200 </div> 201 <canvas 202 id="trafficOverviewChart" 203 style="padding-bottom: 2rem" 204 ></canvas> 205 </div> 206 207 <div class="stats-grid"> 208 <div class="stat-card"> 209 <div class="stat-number" id="totalRequests">-</div> 210 <div class="stat-label">Total Requests</div> 211 </div> 212 <div class="stat-card"> 213 <div class="stat-number" id="avgResponseTime">-</div> 214 <div class="stat-label">Avg Response Time (ms)</div> 215 </div> 216 <div class="stat-card"> 217 <div class="stat-number" id="p95ResponseTime">-</div> 218 <div class="stat-label">P95 Response Time (ms)</div> 219 </div> 220 <div class="stat-card"> 221 <div class="stat-number" id="uniqueEndpoints">-</div> 222 <div class="stat-label">Unique Endpoints</div> 223 </div> 224 <div class="stat-card"> 225 <div class="stat-number" id="errorRate">-</div> 226 <div class="stat-label">Error Rate (%)</div> 227 </div> 228 <div class="stat-card"> 229 <div class="stat-number" id="fastRequests">-</div> 230 <div class="stat-label">Fast Requests (&lt;100ms)</div> 231 </div> 232 <div class="stat-card"> 233 <div class="stat-number" id="uptime">-</div> 234 <div class="stat-label">Uptime (%)</div> 235 </div> 236 <div class="stat-card"> 237 <div class="stat-number" id="throughput">-</div> 238 <div class="stat-label">Throughput (req/hr)</div> 239 </div> 240 <div class="stat-card"> 241 <div class="stat-number" id="apdex">-</div> 242 <div class="stat-label">APDEX Score</div> 243 </div> 244 <div class="stat-card"> 245 <div class="stat-number" id="cacheHitRate">-</div> 246 <div class="stat-label">Cache Hit Rate (%)</div> 247 </div> 248 </div> 249 250 <div class="stats-grid"> 251 <div class="stat-card"> 252 <div class="stat-number" id="peakHour">-</div> 253 <div class="stat-label">Peak Hour</div> 254 </div> 255 <div class="stat-card"> 256 <div class="stat-number" id="peakHourRequests">-</div> 257 <div class="stat-label">Peak Hour Requests</div> 258 </div> 259 <div class="stat-card"> 260 <div class="stat-number" id="peakDay">-</div> 261 <div class="stat-label">Peak Day</div> 262 </div> 263 <div class="stat-card"> 264 <div class="stat-number" id="peakDayRequests">-</div> 265 <div class="stat-label">Peak Day Requests</div> 266 </div> 267 <div class="stat-card"> 268 <div class="stat-number" id="dashboardRequests">-</div> 269 <div class="stat-label">Dashboard Requests</div> 270 </div> 271 </div> 272 273 <div class="charts-grid"> 274 <div class="chart-container"> 275 <div class="chart-title">Requests Over Time</div> 276 <canvas id="timeChart"></canvas> 277 </div> 278 279 <div class="chart-container"> 280 <div class="chart-title"> 281 Latency Over Time (Hourly) 282 </div> 283 <canvas id="latencyTimeChart"></canvas> 284 </div> 285 286 <div class="chart-container"> 287 <div class="chart-title"> 288 Response Time Distribution 289 </div> 290 <canvas id="latencyDistributionChart"></canvas> 291 </div> 292 293 <div class="chart-container"> 294 <div class="chart-title">Latency Percentiles</div> 295 <canvas id="percentilesChart"></canvas> 296 </div> 297 298 <div class="chart-container"> 299 <div class="chart-title">Top Endpoints</div> 300 <canvas id="endpointChart"></canvas> 301 </div> 302 303 <div class="chart-container"> 304 <div class="chart-title">Slowest Endpoints</div> 305 <canvas id="slowestEndpointsChart"></canvas> 306 </div> 307 308 <div class="chart-container"> 309 <div class="chart-title">Status Codes</div> 310 <canvas id="statusChart"></canvas> 311 </div> 312 313 <div class="chart-container"> 314 <div class="chart-title">Top User Agents</div> 315 <canvas id="userAgentChart"></canvas> 316 </div> 317 </div> 318 </div> 319 </div> 320 321 <script> 322 let charts = {}; 323 let autoRefreshInterval; 324 325 async function loadData() { 326 const days = document.getElementById("daysSelect").value; 327 const loading = document.getElementById("loading"); 328 const error = document.getElementById("error"); 329 const content = document.getElementById("content"); 330 331 loading.style.display = "block"; 332 error.style.display = "none"; 333 content.style.display = "none"; 334 335 try { 336 const response = await fetch(`/stats?days=${days}`); 337 if (!response.ok) 338 throw new Error(`HTTP ${response.status}`); 339 340 const data = await response.json(); 341 updateDashboard(data); 342 343 loading.style.display = "none"; 344 content.style.display = "block"; 345 } catch (err) { 346 loading.style.display = "none"; 347 error.style.display = "block"; 348 error.textContent = `Failed to load data: ${err.message}`; 349 } 350 } 351 352 function updateDashboard(data) { 353 // Main metrics 354 document.getElementById("totalRequests").textContent = 355 data.totalRequests.toLocaleString(); 356 document.getElementById("avgResponseTime").textContent = 357 data.averageResponseTime 358 ? Math.round(data.averageResponseTime) 359 : "N/A"; 360 document.getElementById("p95ResponseTime").textContent = data 361 .latencyAnalytics.percentiles.p95 362 ? Math.round(data.latencyAnalytics.percentiles.p95) 363 : "N/A"; 364 document.getElementById("uniqueEndpoints").textContent = 365 data.requestsByEndpoint.length; 366 367 const errorRequests = data.requestsByStatus 368 .filter((s) => s.status >= 400) 369 .reduce((sum, s) => sum + s.count, 0); 370 const errorRate = 371 data.totalRequests > 0 372 ? ((errorRequests / data.totalRequests) * 100).toFixed( 373 1, 374 ) 375 : "0.0"; 376 document.getElementById("errorRate").textContent = errorRate; 377 378 // Calculate fast requests percentage 379 const fastRequestsData = data.latencyAnalytics.distribution 380 .filter( 381 (d) => d.range === "0-50ms" || d.range === "50-100ms", 382 ) 383 .reduce((sum, d) => sum + d.percentage, 0); 384 document.getElementById("fastRequests").textContent = 385 fastRequestsData.toFixed(1) + "%"; 386 387 // Performance metrics 388 document.getElementById("uptime").textContent = 389 data.performanceMetrics.uptime.toFixed(1); 390 document.getElementById("throughput").textContent = Math.round( 391 data.performanceMetrics.throughput, 392 ); 393 document.getElementById("apdex").textContent = 394 data.performanceMetrics.apdex.toFixed(2); 395 document.getElementById("cacheHitRate").textContent = 396 data.performanceMetrics.cachehitRate.toFixed(1); 397 398 // Peak traffic 399 document.getElementById("peakHour").textContent = 400 data.peakTraffic.peakHour; 401 document.getElementById("peakHourRequests").textContent = 402 data.peakTraffic.peakRequests.toLocaleString(); 403 document.getElementById("peakDay").textContent = 404 data.peakTraffic.peakDay; 405 document.getElementById("peakDayRequests").textContent = 406 data.peakTraffic.peakDayRequests.toLocaleString(); 407 408 // Dashboard metrics 409 document.getElementById("dashboardRequests").textContent = 410 data.dashboardMetrics.statsRequests.toLocaleString(); 411 412 // Determine if we're showing hourly or daily data 413 const days = parseInt( 414 document.getElementById("daysSelect").value, 415 ); 416 const isHourly = days === 1; 417 418 updateTrafficOverviewChart(data.trafficOverview, days); 419 updateTimeChart(data.requestsByDay, isHourly); 420 updateLatencyTimeChart( 421 data.latencyAnalytics.latencyOverTime, 422 isHourly, 423 ); 424 updateLatencyDistributionChart( 425 data.latencyAnalytics.distribution, 426 ); 427 updatePercentilesChart(data.latencyAnalytics.percentiles); 428 updateEndpointChart(data.requestsByEndpoint.slice(0, 10)); 429 updateSlowestEndpointsChart( 430 data.latencyAnalytics.slowestEndpoints, 431 ); 432 updateStatusChart(data.requestsByStatus); 433 updateUserAgentChart(data.topUserAgents.slice(0, 5)); 434 } 435 436 function updateTrafficOverviewChart(data, days) { 437 const ctx = document 438 .getElementById("trafficOverviewChart") 439 .getContext("2d"); 440 441 if (charts.trafficOverview) charts.trafficOverview.destroy(); 442 443 // Update chart title based on granularity 444 const chartTitle = document 445 .querySelector("#trafficOverviewChart") 446 .parentElement.querySelector(".chart-title"); 447 let titleText = "Traffic Overview - All Routes Over Time"; 448 if (days === 1) { 449 titleText += " (Hourly)"; 450 } else if (days <= 7) { 451 titleText += " (4-Hour Intervals)"; 452 } else { 453 titleText += " (Daily)"; 454 } 455 chartTitle.textContent = titleText; 456 457 // Get all unique routes across all time periods 458 const allRoutes = new Set(); 459 data.forEach((timePoint) => { 460 Object.keys(timePoint.routes).forEach((route) => 461 allRoutes.add(route), 462 ); 463 }); 464 465 // Define colors for different route types 466 const routeColors = { 467 Dashboard: "#3498db", 468 "User Data": "#2ecc71", 469 "User Redirects": "#27ae60", 470 "Emoji Data": "#e74c3c", 471 "Emoji Redirects": "#c0392b", 472 "Emoji List": "#e67e22", 473 "Health Check": "#f39c12", 474 "API Documentation": "#9b59b6", 475 "Cache Management": "#34495e", 476 }; 477 478 // Create datasets for each route 479 const datasets = Array.from(allRoutes).map((route) => { 480 const color = routeColors[route] || "#95a5a6"; 481 return { 482 label: route, 483 data: data.map( 484 (timePoint) => timePoint.routes[route] || 0, 485 ), 486 borderColor: color, 487 backgroundColor: color + "20", // Add transparency 488 tension: 0.4, 489 fill: false, 490 pointRadius: 2, 491 pointHoverRadius: 4, 492 }; 493 }); 494 495 // Format labels based on time granularity 496 const labels = data.map((timePoint) => { 497 if (days === 1) { 498 // Show just hour for 24h view 499 return timePoint.time.split(" ")[1] || timePoint.time; 500 } else if (days <= 7) { 501 // Show day and hour for 7-day view 502 const parts = timePoint.time.split(" "); 503 const date = parts[0].split("-")[2]; // Get day 504 const hour = parts[1] || "00:00"; 505 return `${date} ${hour}`; 506 } else { 507 // Show full date for longer periods 508 return timePoint.time; 509 } 510 }); 511 512 charts.trafficOverview = new Chart(ctx, { 513 type: "line", 514 data: { 515 labels: labels, 516 datasets: datasets, 517 }, 518 options: { 519 responsive: true, 520 maintainAspectRatio: false, 521 interaction: { 522 mode: "index", 523 intersect: false, 524 }, 525 plugins: { 526 legend: { 527 position: "top", 528 labels: { 529 usePointStyle: true, 530 padding: 15, 531 font: { 532 size: 11, 533 }, 534 }, 535 }, 536 tooltip: { 537 mode: "index", 538 intersect: false, 539 callbacks: { 540 afterLabel: function (context) { 541 const timePoint = 542 data[context.dataIndex]; 543 return `Total: ${timePoint.total} requests`; 544 }, 545 }, 546 }, 547 }, 548 scales: { 549 x: { 550 display: true, 551 title: { 552 display: true, 553 text: 554 days === 1 555 ? "Hour" 556 : days <= 7 557 ? "Day & Hour" 558 : "Date", 559 }, 560 ticks: { 561 maxTicksLimit: 20, 562 }, 563 }, 564 y: { 565 display: true, 566 title: { 567 display: true, 568 text: "Requests", 569 }, 570 beginAtZero: true, 571 }, 572 }, 573 elements: { 574 line: { 575 tension: 0.4, 576 }, 577 }, 578 }, 579 }); 580 } 581 582 function updateTimeChart(data, isHourly) { 583 const ctx = document 584 .getElementById("timeChart") 585 .getContext("2d"); 586 587 if (charts.time) charts.time.destroy(); 588 589 // Update chart title 590 const chartTitle = document 591 .querySelector("#timeChart") 592 .parentElement.querySelector(".chart-title"); 593 chartTitle.textContent = isHourly 594 ? "Requests Over Time (Hourly)" 595 : "Requests Over Time (Daily)"; 596 597 charts.time = new Chart(ctx, { 598 type: "line", 599 data: { 600 labels: data.map((d) => 601 isHourly ? d.date.split(" ")[1] : d.date, 602 ), 603 datasets: [ 604 { 605 label: "Requests", 606 data: data.map((d) => d.count), 607 borderColor: "#3498db", 608 backgroundColor: "rgba(52, 152, 219, 0.1)", 609 tension: 0.4, 610 fill: true, 611 }, 612 ], 613 }, 614 options: { 615 responsive: true, 616 scales: { 617 y: { 618 beginAtZero: true, 619 }, 620 }, 621 }, 622 }); 623 } 624 625 function updateEndpointChart(data) { 626 const ctx = document 627 .getElementById("endpointChart") 628 .getContext("2d"); 629 630 if (charts.endpoint) charts.endpoint.destroy(); 631 632 charts.endpoint = new Chart(ctx, { 633 type: "bar", 634 data: { 635 labels: data.map((d) => d.endpoint), 636 datasets: [ 637 { 638 label: "Requests", 639 data: data.map((d) => d.count), 640 backgroundColor: "#2ecc71", 641 }, 642 ], 643 }, 644 options: { 645 responsive: true, 646 indexAxis: "y", 647 scales: { 648 x: { 649 beginAtZero: true, 650 }, 651 }, 652 }, 653 }); 654 } 655 656 function updateStatusChart(data) { 657 const ctx = document 658 .getElementById("statusChart") 659 .getContext("2d"); 660 661 if (charts.status) charts.status.destroy(); 662 663 const colors = data.map((d) => { 664 if (d.status >= 200 && d.status < 300) return "#2ecc71"; 665 if (d.status >= 300 && d.status < 400) return "#f39c12"; 666 if (d.status >= 400 && d.status < 500) return "#e74c3c"; 667 return "#9b59b6"; 668 }); 669 670 charts.status = new Chart(ctx, { 671 type: "doughnut", 672 data: { 673 labels: data.map((d) => `${d.status}`), 674 datasets: [ 675 { 676 data: data.map((d) => d.count), 677 backgroundColor: colors, 678 }, 679 ], 680 }, 681 options: { 682 responsive: true, 683 }, 684 }); 685 } 686 687 function updateUserAgentChart(data) { 688 const ctx = document 689 .getElementById("userAgentChart") 690 .getContext("2d"); 691 692 if (charts.userAgent) charts.userAgent.destroy(); 693 694 const labels = data.map((d) => { 695 const ua = d.userAgent; 696 if (ua.includes("Chrome")) return "Chrome"; 697 if (ua.includes("Firefox")) return "Firefox"; 698 if (ua.includes("Safari")) return "Safari"; 699 if (ua.includes("curl")) return "curl"; 700 if (ua.includes("bot")) return "Bot"; 701 return "Other"; 702 }); 703 704 charts.userAgent = new Chart(ctx, { 705 type: "pie", 706 data: { 707 labels: labels, 708 datasets: [ 709 { 710 data: data.map((d) => d.count), 711 backgroundColor: [ 712 "#3498db", 713 "#e74c3c", 714 "#2ecc71", 715 "#f39c12", 716 "#9b59b6", 717 ], 718 }, 719 ], 720 }, 721 options: { 722 responsive: true, 723 }, 724 }); 725 } 726 727 function updateLatencyTimeChart(data, isHourly) { 728 const ctx = document 729 .getElementById("latencyTimeChart") 730 .getContext("2d"); 731 732 if (charts.latencyTime) charts.latencyTime.destroy(); 733 734 // Update chart title 735 const chartTitle = document 736 .querySelector("#latencyTimeChart") 737 .parentElement.querySelector(".chart-title"); 738 chartTitle.textContent = isHourly 739 ? "Latency Over Time (Hourly)" 740 : "Latency Over Time (Daily)"; 741 742 charts.latencyTime = new Chart(ctx, { 743 type: "line", 744 data: { 745 labels: data.map((d) => 746 isHourly ? d.time.split(" ")[1] : d.time, 747 ), 748 datasets: [ 749 { 750 label: "Average Response Time", 751 data: data.map((d) => d.averageResponseTime), 752 borderColor: "#3498db", 753 backgroundColor: "rgba(52, 152, 219, 0.1)", 754 tension: 0.4, 755 yAxisID: "y", 756 }, 757 { 758 label: "P95 Response Time", 759 data: data.map((d) => d.p95), 760 borderColor: "#e74c3c", 761 backgroundColor: "rgba(231, 76, 60, 0.1)", 762 tension: 0.4, 763 yAxisID: "y", 764 }, 765 ], 766 }, 767 options: { 768 responsive: true, 769 scales: { 770 y: { 771 beginAtZero: true, 772 title: { 773 display: true, 774 text: "Response Time (ms)", 775 }, 776 }, 777 }, 778 }, 779 }); 780 } 781 782 function updateLatencyDistributionChart(data) { 783 const ctx = document 784 .getElementById("latencyDistributionChart") 785 .getContext("2d"); 786 787 if (charts.latencyDistribution) 788 charts.latencyDistribution.destroy(); 789 790 charts.latencyDistribution = new Chart(ctx, { 791 type: "bar", 792 data: { 793 labels: data.map((d) => d.range), 794 datasets: [ 795 { 796 label: "Requests", 797 data: data.map((d) => d.count), 798 backgroundColor: "#2ecc71", 799 }, 800 ], 801 }, 802 options: { 803 responsive: true, 804 scales: { 805 y: { 806 beginAtZero: true, 807 }, 808 }, 809 }, 810 }); 811 } 812 813 function updatePercentilesChart(percentiles) { 814 const ctx = document 815 .getElementById("percentilesChart") 816 .getContext("2d"); 817 818 if (charts.percentiles) charts.percentiles.destroy(); 819 820 const data = [ 821 { label: "P50", value: percentiles.p50 }, 822 { label: "P75", value: percentiles.p75 }, 823 { label: "P90", value: percentiles.p90 }, 824 { label: "P95", value: percentiles.p95 }, 825 { label: "P99", value: percentiles.p99 }, 826 ].filter((d) => d.value !== null); 827 828 charts.percentiles = new Chart(ctx, { 829 type: "bar", 830 data: { 831 labels: data.map((d) => d.label), 832 datasets: [ 833 { 834 label: "Response Time (ms)", 835 data: data.map((d) => d.value), 836 backgroundColor: [ 837 "#3498db", 838 "#2ecc71", 839 "#f39c12", 840 "#e74c3c", 841 "#9b59b6", 842 ], 843 }, 844 ], 845 }, 846 options: { 847 responsive: true, 848 scales: { 849 y: { 850 beginAtZero: true, 851 }, 852 }, 853 }, 854 }); 855 } 856 857 function updateSlowestEndpointsChart(data) { 858 const ctx = document 859 .getElementById("slowestEndpointsChart") 860 .getContext("2d"); 861 862 if (charts.slowestEndpoints) charts.slowestEndpoints.destroy(); 863 864 charts.slowestEndpoints = new Chart(ctx, { 865 type: "bar", 866 data: { 867 labels: data.map((d) => d.endpoint), 868 datasets: [ 869 { 870 label: "Avg Response Time (ms)", 871 data: data.map((d) => d.averageResponseTime), 872 backgroundColor: "#e74c3c", 873 }, 874 ], 875 }, 876 options: { 877 responsive: true, 878 indexAxis: "y", 879 scales: { 880 x: { 881 beginAtZero: true, 882 }, 883 }, 884 }, 885 }); 886 } 887 888 document 889 .getElementById("autoRefresh") 890 .addEventListener("change", function () { 891 if (this.checked) { 892 autoRefreshInterval = setInterval(loadData, 30000); 893 } else { 894 clearInterval(autoRefreshInterval); 895 } 896 }); 897 898 loadData(); 899 document 900 .getElementById("daysSelect") 901 .addEventListener("change", loadData); 902 </script> 903 </body> 904</html>