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