···
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-
justify-content: space-between;
-
border-bottom: 1px solid #e2e8f0;
text-decoration: underline;
···
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
-
transform: translateY(-1px);
.controls button:disabled {
···
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
···
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-
.stat-card.loading::after {
-
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
-
animation: shimmer 1.5s infinite;
-
margin-bottom: 0.25rem;
-
word-break: break-word;
-
overflow-wrap: break-word;
-
/* Responsive font sizing using clamp() */
-
font-size: clamp(1.25rem, 4vw, 2.5rem);
-
/* Responsive font sizing for labels */
-
font-size: clamp(0.75rem, 2vw, 0.875rem);
-
/* Container query support for modern browsers */
-
@supports (container-type: inline-size) {
-
container-type: inline-size;
-
@container (max-width: 250px) {
-
@container (min-width: 300px) {
-
@container (min-width: 400px) {
-
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
-
@media (max-width: 480px) {
grid-template-columns: 1fr;
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
-
font-size: clamp(0.9rem, 4vw, 1.5rem) !important;
-
font-size: clamp(0.65rem, 2.5vw, 0.75rem) !important;
···
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-
border: 1px solid #e2e8f0;
-
flex-direction: column;
-
.chart-container.loading {
-
justify-content: center;
-
.chart-container.loading::before {
-
content: 'Loading chart...';
-
overflow-wrap: break-word;
-
.chart-title-with-indicator {
-
justify-content: space-between;
-
.chart-title-with-indicator .chart-title {
-
border: 3px solid #e2e8f0;
-
border-top-color: #3b82f6;
animation: spin 1s ease-in-out infinite;
···
.auto-refresh input[type="checkbox"] {
-
.performance-indicator {
-
.performance-indicator.good { color: #059669; }
-
.performance-indicator.warning { color: #d97706; }
-
.performance-indicator.error { color: #dc2626; }
-
justify-content: center;
-
background: transparent;
-
@media (max-width: 768px) {
-
grid-template-columns: 1fr;
-
flex-direction: column;
-
flex-direction: column;
-
font-size: clamp(1rem, 5vw, 1.75rem) !important;
-
font-size: clamp(0.7rem, 3vw, 0.8rem) !important;
-
flex-direction: column;
-
align-items: flex-start;
-
transform: translateX(100%);
-
transition: transform 0.3s ease;
-
transform: translateX(0);
···
<div id="error" class="error" style="display: none"></div>
<div id="content" style="display: none">
-
<!-- Key Metrics Overview -->
-
<div class="chart-container" style="margin-bottom: 2rem; height: 450px">
-
<div class="chart-title-with-indicator">
-
<div class="chart-title">Traffic Overview - All Routes Over Time</div>
-
<span class="performance-indicator" id="trafficPerformance"></span>
-
id="trafficOverviewChart"
-
style="padding-bottom: 2rem"
-
<div class="stat-card" id="totalRequestsCard">
<div class="stat-number" id="totalRequests">-</div>
<div class="stat-label">Total Requests</div>
-
<div class="stat-card" id="avgResponseTimeCard">
-
<div class="stat-number" id="avgResponseTime">-</div>
-
<div class="stat-label">Avg Response Time (ms)</div>
-
<div class="stat-card" id="p95ResponseTimeCard">
-
<div class="stat-number" id="p95ResponseTime">-</div>
-
<div class="stat-label">P95 Response Time (ms)</div>
-
<div class="stat-card" id="uniqueEndpointsCard">
-
<div class="stat-number" id="uniqueEndpoints">-</div>
-
<div class="stat-label">Unique Endpoints</div>
-
<div class="stat-card" id="errorRateCard">
-
<div class="stat-number" id="errorRate">-</div>
-
<div class="stat-label">Error Rate (%)</div>
-
<div class="stat-card" id="fastRequestsCard">
-
<div class="stat-number" id="fastRequests">-</div>
-
<div class="stat-label">Fast Requests (<100ms)</div>
-
<div class="stat-card" id="uptimeCard">
<div class="stat-number" id="uptime">-</div>
-
<div class="stat-label">Uptime (%)</div>
-
<div class="stat-card" id="throughputCard">
-
<div class="stat-number" id="throughput">-</div>
-
<div class="stat-label">Throughput (req/hr)</div>
-
<div class="stat-card" id="apdexCard">
-
<div class="stat-number" id="apdex">-</div>
-
<div class="stat-label">APDEX Score</div>
-
<div class="stat-card" id="cacheHitRateCard">
-
<div class="stat-number" id="cacheHitRate">-</div>
-
<div class="stat-label">Cache Hit Rate (%)</div>
-
<!-- Peak Traffic Stats -->
-
<div class="stats-grid">
-
<div class="stat-card" id="peakHourCard">
-
<div class="stat-number" id="peakHour">-</div>
-
<div class="stat-label">Peak Hour</div>
-
<div class="stat-card" id="peakHourRequestsCard">
-
<div class="stat-number" id="peakHourRequests">-</div>
-
<div class="stat-label">Peak Hour Requests</div>
-
<div class="stat-card" id="peakDayCard">
-
<div class="stat-number" id="peakDay">-</div>
-
<div class="stat-label">Peak Day</div>
-
<div class="stat-card" id="peakDayRequestsCard">
-
<div class="stat-number" id="peakDayRequests">-</div>
-
<div class="stat-label">Peak Day Requests</div>
-
<div class="stat-card" id="dashboardRequestsCard">
-
<div class="stat-number" id="dashboardRequests">-</div>
-
<div class="stat-label">Dashboard Requests</div>
-
<!-- Charts Grid with Lazy Loading -->
<div class="charts-grid">
-
<div class="chart-container lazy-chart" data-chart="timeChart">
-
<div class="chart-title">Requests Over Time</div>
-
<canvas id="timeChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="latencyTimeChart">
-
<div class="chart-title">Latency Over Time (Hourly)</div>
-
<canvas id="latencyTimeChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="latencyDistributionChart">
-
<div class="chart-title">Response Time Distribution</div>
-
<canvas id="latencyDistributionChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="percentilesChart">
-
<div class="chart-title">Latency Percentiles</div>
-
<canvas id="percentilesChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="endpointChart">
-
<div class="chart-title">Top Endpoints</div>
-
<canvas id="endpointChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="slowestEndpointsChart">
-
<div class="chart-title">Slowest Endpoints</div>
-
<canvas id="slowestEndpointsChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="statusChart">
-
<div class="chart-title">Status Codes</div>
-
<canvas id="statusChart"></canvas>
-
<div class="chart-container lazy-chart" data-chart="userAgentChart">
-
<div class="chart-title">Top User Agents</div>
-
<canvas id="userAgentChart"></canvas>
···
-
let visibleCharts = new Set();
-
let intersectionObserver;
-
// Performance monitoring
-
// Initialize intersection observer for lazy loading
-
function initLazyLoading() {
-
intersectionObserver = new IntersectionObserver((entries) => {
-
entries.forEach(entry => {
-
if (entry.isIntersecting) {
-
const chartContainer = entry.target;
-
const chartType = chartContainer.dataset.chart;
-
if (!visibleCharts.has(chartType) && currentData) {
-
visibleCharts.add(chartType);
-
chartContainer.classList.add('visible');
-
loadChart(chartType, currentData);
-
// Observe all lazy chart containers
-
document.querySelectorAll('.lazy-chart').forEach(container => {
-
intersectionObserver.observe(container);
-
// Show toast notification
-
function showToast(message, type = 'info') {
-
const toast = document.createElement('div');
-
toast.className = `toast ${type}`;
-
toast.textContent = message;
-
document.body.appendChild(toast);
-
setTimeout(() => toast.classList.add('show'), 100);
-
toast.classList.remove('show');
-
setTimeout(() => document.body.removeChild(toast), 300);
-
// Update loading states for stat cards
-
function setStatCardLoading(cardId, loading) {
-
const card = document.getElementById(cardId);
-
card.classList.add('loading');
-
card.classList.remove('loading');
// Debounced resize handler for charts
···
window.addEventListener('resize', handleResize);
async function loadData() {
-
performance.startTime = Date.now();
const days = document.getElementById("daysSelect").value;
const loading = document.getElementById("loading");
const error = document.getElementById("error");
const content = document.getElementById("content");
const refreshBtn = document.getElementById("refreshBtn");
loading.style.display = "block";
···
refreshBtn.disabled = true;
refreshBtn.textContent = "Loading...";
-
// Set all stat cards to loading state
-
'totalRequestsCard', 'avgResponseTimeCard', 'p95ResponseTimeCard',
-
'uniqueEndpointsCard', 'errorRateCard', 'fastRequestsCard',
-
'uptimeCard', 'throughputCard', 'apdexCard', 'cacheHitRateCard',
-
'peakHourCard', 'peakHourRequestsCard', 'peakDayCard',
-
'peakDayRequestsCard', 'dashboardRequestsCard'
-
statCards.forEach(cardId => setStatCardLoading(cardId, true));
-
const response = await fetch(`/stats?days=${days}`);
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
const data = await response.json();
-
performance.endTime = Date.now();
-
performance.loadTime = performance.endTime - performance.startTime;
loading.style.display = "none";
content.style.display = "block";
-
showToast(`Dashboard updated in ${performance.loadTime}ms`, 'success');
-
loading.style.display = "none";
-
error.style.display = "block";
-
error.textContent = `Failed to load data: ${err.message}`;
-
showToast(`Error: ${err.message}`, 'error');
-
refreshBtn.disabled = false;
-
refreshBtn.textContent = "Refresh";
-
// Remove loading state from stat cards
-
statCards.forEach(cardId => setStatCardLoading(cardId, false));
-
function updateDashboard(data) {
-
// Update main metrics with animation
-
updateStatWithAnimation("totalRequests", data.totalRequests.toLocaleString());
-
updateStatWithAnimation("avgResponseTime",
-
data.averageResponseTime ? Math.round(data.averageResponseTime) : "N/A");
-
updateStatWithAnimation("p95ResponseTime",
-
data.latencyAnalytics.percentiles.p95 ? Math.round(data.latencyAnalytics.percentiles.p95) : "N/A");
-
updateStatWithAnimation("uniqueEndpoints", data.requestsByEndpoint.length);
-
const errorRequests = data.requestsByStatus
-
.filter((s) => s.status >= 400)
-
.reduce((sum, s) => sum + s.count, 0);
-
const errorRate = data.totalRequests > 0
-
? ((errorRequests / data.totalRequests) * 100).toFixed(1)
-
updateStatWithAnimation("errorRate", errorRate);
-
// Calculate fast requests percentage
-
const fastRequestsData = data.latencyAnalytics.distribution
-
.filter((d) => d.range === "0-50ms" || d.range === "50-100ms")
-
.reduce((sum, d) => sum + d.percentage, 0);
-
updateStatWithAnimation("fastRequests", fastRequestsData.toFixed(1) + "%");
-
updateStatWithAnimation("uptime", data.performanceMetrics.uptime.toFixed(1));
-
updateStatWithAnimation("throughput", Math.round(data.performanceMetrics.throughput));
-
updateStatWithAnimation("apdex", data.performanceMetrics.apdex.toFixed(2));
-
updateStatWithAnimation("cacheHitRate", data.performanceMetrics.cachehitRate.toFixed(1));
-
updateStatWithAnimation("peakHour", data.peakTraffic.peakHour);
-
updateStatWithAnimation("peakHourRequests", data.peakTraffic.peakRequests.toLocaleString());
-
updateStatWithAnimation("peakDay", data.peakTraffic.peakDay);
-
updateStatWithAnimation("peakDayRequests", data.peakTraffic.peakDayRequests.toLocaleString());
-
updateStatWithAnimation("dashboardRequests", data.dashboardMetrics.statsRequests.toLocaleString());
-
// Update performance indicator
-
updatePerformanceIndicator(data);
-
// Load main traffic overview chart immediately
-
const days = parseInt(document.getElementById("daysSelect").value);
-
updateTrafficOverviewChart(data.trafficOverview, days);
-
// Other charts will be loaded lazily when they come into view
-
function updateStatWithAnimation(elementId, value) {
-
const element = document.getElementById(elementId);
-
if (element && element.textContent !== value.toString()) {
-
element.style.transform = 'scale(1.1)';
-
element.style.transition = 'transform 0.2s ease';
-
element.textContent = value;
-
element.style.transform = 'scale(1)';
-
function updatePerformanceIndicator(data) {
-
const indicator = document.getElementById('trafficPerformance');
-
const avgResponseTime = data.averageResponseTime || 0;
-
const errorRate = data.requestsByStatus
-
.filter((s) => s.status >= 400)
-
.reduce((sum, s) => sum + s.count, 0) / data.totalRequests * 100;
-
if (avgResponseTime < 100 && errorRate < 1) {
-
} else if (avgResponseTime < 300 && errorRate < 5) {
-
text = '🔴 Needs Attention';
-
indicator.className = `performance-indicator ${status}`;
-
indicator.textContent = text;
-
// Load individual charts (called by intersection observer)
-
function loadChart(chartType, data) {
-
const days = parseInt(document.getElementById("daysSelect").value);
-
const isHourly = days === 1;
-
updateTimeChart(data.requestsByDay, isHourly);
-
case 'latencyTimeChart':
-
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
-
case 'latencyDistributionChart':
-
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
-
case 'percentilesChart':
-
updatePercentilesChart(data.latencyAnalytics.percentiles);
-
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
-
case 'slowestEndpointsChart':
-
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
-
updateStatusChart(data.requestsByStatus);
-
updateUserAgentChart(data.topUserAgents.slice(0, 5));
-
console.error(`Error loading chart ${chartType}:`, error);
-
const container = document.querySelector(`[data-chart="${chartType}"]`);
-
container.innerHTML = `<div class="chart-error">Error loading chart: ${error.message}</div>`;
-
function updateTrafficOverviewChart(data, days) {
-
const canvas = document.getElementById("trafficOverviewChart");
-
console.warn('trafficOverviewChart canvas not found');
-
const ctx = canvas.getContext("2d");
-
console.warn('Could not get 2d context for trafficOverviewChart');
-
if (charts.trafficOverview) {
-
charts.trafficOverview.destroy();
-
// Update chart title based on granularity
-
const chartTitleElement = document.querySelector(".chart-title-with-indicator .chart-title");
-
let titleText = "Traffic Overview - All Routes Over Time";
-
titleText += " (Hourly)";
-
} else if (days <= 7) {
-
titleText += " (4-Hour Intervals)";
-
titleText += " (Daily)";
-
if (chartTitleElement) {
-
chartTitleElement.textContent = titleText;
-
// Get all unique routes across all time periods
-
const allRoutes = new Set();
-
data.forEach((timePoint) => {
-
Object.keys(timePoint.routes).forEach((route) =>
-
// Define colors for different route types
-
"User Data": "#10b981",
-
"User Redirects": "#059669",
-
"Emoji Data": "#ef4444",
-
"Emoji Redirects": "#dc2626",
-
"Emoji List": "#f97316",
-
"Health Check": "#f59e0b",
-
"API Documentation": "#8b5cf6",
-
"Cache Management": "#6b7280",
-
// Create datasets for each route
-
const datasets = Array.from(allRoutes).map((route) => {
-
const color = routeColors[route] || "#9ca3af";
-
data: data.map((timePoint) => timePoint.routes[route] || 0),
-
backgroundColor: color + "20",
-
// Format labels based on time granularity
-
const labels = data.map((timePoint) => {
-
return timePoint.time.split(" ")[1] || timePoint.time;
-
const parts = timePoint.time.split(" ");
-
const date = parts[0].split("-")[2];
const hour = parts[1] || "00:00";
return `${date} ${hour}`;
-
charts.trafficOverview = new Chart(ctx, {
maintainAspectRatio: false,
-
easing: 'easeInOutQuart'
-
generateLabels: function(chart) {
-
const original = Chart.defaults.plugins.legend.labels.generateLabels;
-
const labels = original.call(this, chart);
-
// Add total request count to legend labels
-
labels.forEach((label, index) => {
-
const dataset = chart.data.datasets[index];
-
const total = dataset.data.reduce((sum, val) => sum + val, 0);
-
label.text += ` (${total.toLocaleString()})`;
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
title: function(context) {
-
return `Time: ${context[0].label}`;
-
afterBody: function(context) {
-
const timePoint = data[context[0].dataIndex];
-
`Total Requests: ${timePoint.total.toLocaleString()}`,
-
`Peak Route: ${Object.entries(timePoint.routes).sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A'}`
-
text: days === 1 ? "Hour" : days <= 7 ? "Day & Hour" : "Date",
-
maxTicksLimit: window.innerWidth < 768 ? 8 : 20,
-
maxRotation: 0, // Don't rotate labels
-
callback: function(value, index, values) {
-
const label = this.getLabelForValue(value);
-
// Truncate long labels
-
if (label.length > 8) {
-
return label; // Hours are usually short
-
// For longer periods, abbreviate
-
return label.substring(0, 6) + '...';
-
color: 'rgba(0, 0, 0, 0.05)',
-
color: 'rgba(0, 0, 0, 0.05)',
-
callback: function(value) {
-
return value.toLocaleString();
-
function updateTimeChart(data, isHourly) {
-
const canvas = document.getElementById("timeChart");
-
console.warn('timeChart canvas not found');
-
const ctx = canvas.getContext("2d");
-
console.warn('Could not get 2d context for timeChart');
-
if (charts.time) charts.time.destroy();
-
const chartTitle = document
-
.querySelector("#timeChart")
-
.parentElement.querySelector(".chart-title");
-
chartTitle.textContent = isHourly
-
? "Requests Over Time (Hourly)"
-
: "Requests Over Time (Daily)";
-
charts.time = new Chart(ctx, {
-
labels: data.map((d) => (isHourly ? d.date.split(" ")[1] : d.date)),
-
data: data.map((d) => d.count),
-
borderColor: "#3b82f6",
-
backgroundColor: "rgba(59, 130, 246, 0.1)",
-
pointBackgroundColor: "#3b82f6",
-
pointBorderColor: "#ffffff",
maintainAspectRatio: false,
-
bottom: 50 // Extra space for rotated labels
-
easing: 'easeInOutQuart'
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
title: function(context) {
-
const point = data[context[0].dataIndex];
-
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
label: function(context) {
const point = data[context.dataIndex];
-
`Requests: ${context.parsed.y.toLocaleString()}`,
-
`Avg Response Time: ${Math.round(point.averageResponseTime || 0)}ms`
-
generateLabels: function(chart) {
-
const total = data.reduce((sum, d) => sum + d.count, 0);
-
const avg = Math.round(data.reduce((sum, d) => sum + (d.averageResponseTime || 0), 0) / data.length);
-
text: `Requests (Total: ${total.toLocaleString()}, Avg RT: ${avg}ms)`,
-
strokeStyle: '#3b82f6',
-
text: isHourly ? 'Hour of Day' : 'Date',
-
font: { weight: 'bold' }
-
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
-
maxRotation: 0, // Don't rotate labels
-
callback: function(value, index, values) {
-
const label = this.getLabelForValue(value);
-
// Truncate long labels for better fit
-
return label; // Hours are short
-
// For dates, show abbreviated format
-
const date = new Date(label);
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
-
color: 'rgba(0, 0, 0, 0.05)',
-
text: 'Number of Requests',
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
-
callback: function(value) {
-
return value.toLocaleString();
-
function updateEndpointChart(data) {
-
const canvas = document.getElementById("endpointChart");
-
console.warn('endpointChart canvas not found');
-
const ctx = canvas.getContext("2d");
-
console.warn('Could not get 2d context for endpointChart');
-
if (charts.endpoint) charts.endpoint.destroy();
-
charts.endpoint = new Chart(ctx, {
-
labels: data.map((d) => d.endpoint),
-
data: data.map((d) => d.count),
-
backgroundColor: "#10b981",
-
maintainAspectRatio: false,
-
bottom: 60 // Extra space for labels
-
easing: 'easeInOutQuart'
-
font: { weight: 'bold' }
-
maxRotation: 0, // Don't rotate
-
callback: function(value, index, values) {
-
const label = this.getLabelForValue(value);
-
// Truncate long labels but show full in tooltip
-
return label.length > 12 ? label.substring(0, 9) + '...' : label;
-
color: 'rgba(0, 0, 0, 0.05)',
-
text: 'Number of Requests',
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
callback: function(value) {
-
return value.toLocaleString();
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
-
title: function(context) {
-
return context[0].label; // Show full label in tooltip
-
label: function(context) {
-
return `Requests: ${context.parsed.y.toLocaleString()}`;
-
generateLabels: function(chart) {
-
const total = data.reduce((sum, d) => sum + d.count, 0);
-
text: `Total Requests: ${total.toLocaleString()}`,
-
strokeStyle: '#10b981',
-
function updateStatusChart(data) {
-
const ctx = document.getElementById("statusChart").getContext("2d");
-
if (charts.status) charts.status.destroy();
-
const colors = data.map((d) => {
-
if (d.status >= 200 && d.status < 300) return "#10b981";
-
if (d.status >= 300 && d.status < 400) return "#f59e0b";
-
if (d.status >= 400 && d.status < 500) return "#ef4444";
-
charts.status = new Chart(ctx, {
-
labels: data.map((d) => `${d.status}`),
-
data: data.map((d) => d.count),
-
backgroundColor: colors,
-
easing: 'easeInOutQuart'
-
function updateUserAgentChart(data) {
-
const ctx = document.getElementById("userAgentChart").getContext("2d");
-
if (charts.userAgent) charts.userAgent.destroy();
-
charts.userAgent = new Chart(ctx, {
-
labels: data.map((d) => d.userAgent),
-
data: data.map((d) => d.count),
-
easing: 'easeInOutQuart'
-
function updateLatencyTimeChart(data, isHourly) {
-
const ctx = document.getElementById("latencyTimeChart").getContext("2d");
-
if (charts.latencyTime) charts.latencyTime.destroy();
-
const chartTitle = document
-
.querySelector("#latencyTimeChart")
-
.parentElement.querySelector(".chart-title");
-
chartTitle.textContent = isHourly
-
? "Latency Over Time (Hourly)"
-
: "Latency Over Time (Daily)";
-
charts.latencyTime = new Chart(ctx, {
-
labels: data.map((d) => (isHourly ? d.time.split(" ")[1] : d.time)),
-
label: "Average Response Time",
-
data: data.map((d) => d.averageResponseTime),
-
borderColor: "#3b82f6",
-
backgroundColor: "rgba(59, 130, 246, 0.1)",
-
pointBackgroundColor: "#3b82f6",
-
pointBorderColor: "#ffffff",
-
label: "P95 Response Time",
-
data: data.map((d) => d.p95),
-
borderColor: "#ef4444",
-
backgroundColor: "rgba(239, 68, 68, 0.1)",
-
pointBackgroundColor: "#ef4444",
-
pointBorderColor: "#ffffff",
-
maintainAspectRatio: false,
-
easing: 'easeInOutQuart'
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
-
title: function(context) {
-
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
-
afterBody: function(context) {
-
const point = data[context[0].dataIndex];
-
`Request Count: ${point.count.toLocaleString()}`,
-
`Performance: ${point.averageResponseTime < 100 ? 'Excellent' : point.averageResponseTime < 300 ? 'Good' : 'Needs Attention'}`
-
generateLabels: function(chart) {
-
const avgAvg = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
-
const avgP95 = Math.round(data.reduce((sum, d) => sum + (d.p95 || 0), 0) / data.length);
-
text: `Average Response Time (Overall: ${avgAvg}ms)`,
-
strokeStyle: '#3b82f6',
-
text: `P95 Response Time (Overall: ${avgP95}ms)`,
-
strokeStyle: '#ef4444',
-
text: isHourly ? 'Hour of Day' : 'Date',
-
font: { weight: 'bold' }
-
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
-
callback: function(value, index, values) {
-
const label = this.getLabelForValue(value);
-
const date = new Date(label);
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
-
color: 'rgba(0, 0, 0, 0.05)',
-
text: "Response Time (ms)",
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
-
callback: function(value) {
-
return Math.round(value) + 'ms';
-
function updateLatencyDistributionChart(data) {
-
const ctx = document.getElementById("latencyDistributionChart").getContext("2d");
-
if (charts.latencyDistribution) charts.latencyDistribution.destroy();
-
charts.latencyDistribution = new Chart(ctx, {
-
labels: data.map((d) => d.range),
-
data: data.map((d) => d.count),
-
backgroundColor: "#10b981",
-
easing: 'easeInOutQuart'
-
function updatePercentilesChart(percentiles) {
-
const ctx = document.getElementById("percentilesChart").getContext("2d");
-
if (charts.percentiles) charts.percentiles.destroy();
-
{ label: "P50 (Median)", value: percentiles.p50 },
-
{ label: "P75", value: percentiles.p75 },
-
{ label: "P90", value: percentiles.p90 },
-
{ label: "P95", value: percentiles.p95 },
-
{ label: "P99", value: percentiles.p99 },
-
].filter((d) => d.value !== null);
-
charts.percentiles = new Chart(ctx, {
-
labels: data.map((d) => d.label),
-
label: "Response Time (ms)",
-
data: data.map((d) => d.value),
-
"#10b981", // P50 - Green (good)
-
"#3b82f6", // P75 - Blue
-
"#f59e0b", // P90 - Yellow (warning)
-
"#ef4444", // P95 - Red (concerning)
-
"#8b5cf6", // P99 - Purple (critical)
-
borderColor: '#ffffff',
-
maintainAspectRatio: false,
-
easing: 'easeInOutQuart'
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
-
label: function(context) {
-
const percentile = context.label;
-
const value = Math.round(context.parsed.y);
-
let interpretation = '';
-
if (percentile.includes('P50')) {
-
interpretation = '50% of requests are faster than this';
-
} else if (percentile.includes('P95')) {
-
interpretation = '95% of requests are faster than this';
-
} else if (percentile.includes('P99')) {
-
interpretation = '99% of requests are faster than this';
-
`${percentile}: ${value}ms`,
-
display: false // Hide legend since colors are self-explanatory
-
text: 'Response Time Percentiles',
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
-
text: 'Response Time (ms)',
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
-
callback: function(value) {
-
return Math.round(value) + 'ms';
-
function updateSlowestEndpointsChart(data) {
-
const ctx = document.getElementById("slowestEndpointsChart").getContext("2d");
-
if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
-
charts.slowestEndpoints = new Chart(ctx, {
-
labels: data.map((d) => d.endpoint),
-
label: "Avg Response Time (ms)",
-
data: data.map((d) => d.averageResponseTime),
-
backgroundColor: "#ef4444",
-
maintainAspectRatio: false,
-
easing: 'easeInOutQuart'
-
indexAxis: "x", // Changed from "y" to "x" to put labels on top
-
font: { weight: 'bold' }
-
callback: function(value, index, values) {
-
const label = this.getLabelForValue(value);
-
return label.length > 15 ? label.substring(0, 12) + '...' : label;
-
color: 'rgba(0, 0, 0, 0.05)',
-
text: 'Response Time (ms)',
-
font: { weight: 'bold' }
-
color: 'rgba(0, 0, 0, 0.05)',
-
callback: function(value) {
-
return Math.round(value) + 'ms';
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
-
borderColor: 'rgba(255, 255, 255, 0.1)',
-
title: function(context) {
-
return context[0].label; // Show full label in tooltip
-
label: function(context) {
-
const point = data[context.dataIndex];
-
`Avg Response Time: ${Math.round(context.parsed.y)}ms`,
-
`Request Count: ${point.count.toLocaleString()}`
-
generateLabels: function(chart) {
-
const avgTime = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
-
text: `Average Response Time: ${avgTime}ms`,
-
strokeStyle: '#ef4444',
document.getElementById("autoRefresh").addEventListener("change", function () {
autoRefreshInterval = setInterval(loadData, 30000);
-
showToast('Auto-refresh enabled', 'success');
clearInterval(autoRefreshInterval);
-
showToast('Auto-refresh disabled', 'info');
-
// Days selector change handler
-
document.getElementById("daysSelect").addEventListener("change", function() {
-
// Reset visible charts when changing time period
-
document.addEventListener('DOMContentLoaded', function() {
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
-
if (intersectionObserver) {
-
intersectionObserver.disconnect();
clearInterval(autoRefreshInterval);
-
// Destroy all charts to prevent memory leaks
Object.values(charts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {