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 (<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 charts.userAgent = new Chart(ctx, {
695 type: "pie",
696 data: {
697 labels: data.map((d) => d.userAgent),
698 datasets: [
699 {
700 data: data.map((d) => d.count),
701 backgroundColor: [
702 "#3498db",
703 "#e74c3c",
704 "#2ecc71",
705 "#f39c12",
706 "#9b59b6",
707 "#34495e",
708 "#16a085",
709 "#8e44ad",
710 "#d35400",
711 "#7f8c8d",
712 ],
713 },
714 ],
715 },
716 options: {
717 responsive: true,
718 },
719 });
720 }
721
722 function updateLatencyTimeChart(data, isHourly) {
723 const ctx = document
724 .getElementById("latencyTimeChart")
725 .getContext("2d");
726
727 if (charts.latencyTime) charts.latencyTime.destroy();
728
729 // Update chart title
730 const chartTitle = document
731 .querySelector("#latencyTimeChart")
732 .parentElement.querySelector(".chart-title");
733 chartTitle.textContent = isHourly
734 ? "Latency Over Time (Hourly)"
735 : "Latency Over Time (Daily)";
736
737 charts.latencyTime = new Chart(ctx, {
738 type: "line",
739 data: {
740 labels: data.map((d) =>
741 isHourly ? d.time.split(" ")[1] : d.time,
742 ),
743 datasets: [
744 {
745 label: "Average Response Time",
746 data: data.map((d) => d.averageResponseTime),
747 borderColor: "#3498db",
748 backgroundColor: "rgba(52, 152, 219, 0.1)",
749 tension: 0.4,
750 yAxisID: "y",
751 },
752 {
753 label: "P95 Response Time",
754 data: data.map((d) => d.p95),
755 borderColor: "#e74c3c",
756 backgroundColor: "rgba(231, 76, 60, 0.1)",
757 tension: 0.4,
758 yAxisID: "y",
759 },
760 ],
761 },
762 options: {
763 responsive: true,
764 scales: {
765 y: {
766 beginAtZero: true,
767 title: {
768 display: true,
769 text: "Response Time (ms)",
770 },
771 },
772 },
773 },
774 });
775 }
776
777 function updateLatencyDistributionChart(data) {
778 const ctx = document
779 .getElementById("latencyDistributionChart")
780 .getContext("2d");
781
782 if (charts.latencyDistribution)
783 charts.latencyDistribution.destroy();
784
785 charts.latencyDistribution = new Chart(ctx, {
786 type: "bar",
787 data: {
788 labels: data.map((d) => d.range),
789 datasets: [
790 {
791 label: "Requests",
792 data: data.map((d) => d.count),
793 backgroundColor: "#2ecc71",
794 },
795 ],
796 },
797 options: {
798 responsive: true,
799 scales: {
800 y: {
801 beginAtZero: true,
802 },
803 },
804 },
805 });
806 }
807
808 function updatePercentilesChart(percentiles) {
809 const ctx = document
810 .getElementById("percentilesChart")
811 .getContext("2d");
812
813 if (charts.percentiles) charts.percentiles.destroy();
814
815 const data = [
816 { label: "P50", value: percentiles.p50 },
817 { label: "P75", value: percentiles.p75 },
818 { label: "P90", value: percentiles.p90 },
819 { label: "P95", value: percentiles.p95 },
820 { label: "P99", value: percentiles.p99 },
821 ].filter((d) => d.value !== null);
822
823 charts.percentiles = new Chart(ctx, {
824 type: "bar",
825 data: {
826 labels: data.map((d) => d.label),
827 datasets: [
828 {
829 label: "Response Time (ms)",
830 data: data.map((d) => d.value),
831 backgroundColor: [
832 "#3498db",
833 "#2ecc71",
834 "#f39c12",
835 "#e74c3c",
836 "#9b59b6",
837 ],
838 },
839 ],
840 },
841 options: {
842 responsive: true,
843 scales: {
844 y: {
845 beginAtZero: true,
846 },
847 },
848 },
849 });
850 }
851
852 function updateSlowestEndpointsChart(data) {
853 const ctx = document
854 .getElementById("slowestEndpointsChart")
855 .getContext("2d");
856
857 if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
858
859 charts.slowestEndpoints = new Chart(ctx, {
860 type: "bar",
861 data: {
862 labels: data.map((d) => d.endpoint),
863 datasets: [
864 {
865 label: "Avg Response Time (ms)",
866 data: data.map((d) => d.averageResponseTime),
867 backgroundColor: "#e74c3c",
868 },
869 ],
870 },
871 options: {
872 responsive: true,
873 indexAxis: "y",
874 scales: {
875 x: {
876 beginAtZero: true,
877 },
878 },
879 },
880 });
881 }
882
883 document
884 .getElementById("autoRefresh")
885 .addEventListener("change", function () {
886 if (this.checked) {
887 autoRefreshInterval = setInterval(loadData, 30000);
888 } else {
889 clearInterval(autoRefreshInterval);
890 }
891 });
892
893 loadData();
894 document
895 .getElementById("daysSelect")
896 .addEventListener("change", loadData);
897 </script>
898 </body>
899</html>