a cache for slack profile pictures and emojis

feat: implement cleaner and cache charts

dunkirk.sh 9e2761e3 1d4774ed

verified
Changed files
+1238 -201
src
+47 -2
src/cache.ts
···
private db: Database;
private defaultExpiration: number; // in hours
private onEmojiExpired?: () => void;
+
private analyticsCache: Map<string, { data: any; timestamp: number }> = new Map();
+
private analyticsCacheTTL = 60000; // 1 minute cache for analytics
/**
* Creates a new Cache instance
···
CREATE INDEX IF NOT EXISTS idx_request_analytics_endpoint
ON request_analytics(endpoint)
`);
+
+
this.db.run(`
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_status_timestamp
+
ON request_analytics(status_code, timestamp)
+
`);
+
+
this.db.run(`
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_response_time
+
ON request_analytics(response_time) WHERE response_time IS NOT NULL
+
`);
+
+
this.db.run(`
+
CREATE INDEX IF NOT EXISTS idx_request_analytics_composite
+
ON request_analytics(timestamp, endpoint, status_code)
+
`);
+
+
// Enable WAL mode for better concurrent performance
+
this.db.run('PRAGMA journal_mode = WAL');
+
this.db.run('PRAGMA synchronous = NORMAL');
+
this.db.run('PRAGMA cache_size = 10000');
+
this.db.run('PRAGMA temp_store = memory');
// check if there are any emojis in the db
if (this.onEmojiExpired) {
···
}
/**
-
* Gets request analytics statistics
+
* Gets request analytics statistics with performance optimizations
* @param days Number of days to look back (default: 7)
* @returns Analytics data
*/
···
total: number;
}>;
}> {
+
// Check cache first
+
const cacheKey = `analytics_${days}`;
+
const cached = this.analyticsCache.get(cacheKey);
+
const now = Date.now();
+
+
if (cached && (now - cached.timestamp) < this.analyticsCacheTTL) {
+
return cached.data;
+
}
const cutoffTime = Date.now() - days * 24 * 60 * 60 * 1000;
// Total requests (excluding stats endpoint)
···
.sort((a, b) => a.time.localeCompare(b.time));
-
return {
+
const result = {
totalRequests: totalResult.count,
requestsByEndpoint: requestsByEndpoint,
requestsByStatus: statusResults,
···
},
trafficOverview,
};
+
+
// Cache the result
+
this.analyticsCache.set(cacheKey, {
+
data: result,
+
timestamp: now
+
});
+
+
// Clean up old cache entries (keep only last 5)
+
if (this.analyticsCache.size > 5) {
+
const oldestKey = Array.from(this.analyticsCache.keys())[0];
+
this.analyticsCache.delete(oldestKey);
+
}
+
+
return result;
+1191 -199
src/dashboard.html
···
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cachet Analytics Dashboard</title>
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<style>
* {
margin: 0;
···
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
-
background: #f5f5f5;
-
color: #333;
+
background: #f8fafc;
+
color: #1e293b;
+
line-height: 1.6;
}
.header {
background: #fff;
padding: 1rem 2rem;
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
+
border-bottom: 1px solid #e2e8f0;
}
.header h1 {
···
.controls {
margin-bottom: 2rem;
-
text-align: center;
+
display: flex;
+
justify-content: center;
+
align-items: center;
+
gap: 1rem;
+
flex-wrap: wrap;
}
.controls select,
.controls button {
-
padding: 0.5rem 1rem;
-
margin: 0 0.5rem;
-
border: 1px solid #ddd;
-
border-radius: 4px;
+
padding: 0.75rem 1.25rem;
+
border: 1px solid #d1d5db;
+
border-radius: 8px;
background: white;
cursor: pointer;
+
font-size: 0.875rem;
+
font-weight: 500;
+
transition: all 0.2s ease;
+
}
+
+
.controls select:hover,
+
.controls select:focus {
+
border-color: #3b82f6;
+
outline: none;
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.controls button {
-
background: #3498db;
+
background: #3b82f6;
color: white;
border: none;
}
.controls button:hover {
-
background: #2980b9;
+
background: #2563eb;
+
transform: translateY(-1px);
+
}
+
+
.controls button:disabled {
+
background: #9ca3af;
+
cursor: not-allowed;
+
transform: none;
}
.dashboard {
···
.stat-card {
background: white;
padding: 1.5rem;
-
border-radius: 8px;
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
border-radius: 12px;
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
text-align: center;
+
border: 1px solid #e2e8f0;
+
transition: all 0.2s ease;
+
position: relative;
+
overflow: hidden;
+
display: flex;
+
flex-direction: column;
+
justify-content: center;
+
min-height: 120px;
+
}
+
+
.stat-card:hover {
+
transform: translateY(-2px);
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+
}
+
+
.stat-card.loading {
+
opacity: 0.6;
+
}
+
+
.stat-card.loading::after {
+
content: '';
+
position: absolute;
+
top: 0;
+
left: -100%;
+
width: 100%;
+
height: 100%;
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
+
animation: shimmer 1.5s infinite;
+
}
+
+
@keyframes shimmer {
+
0% { left: -100%; }
+
100% { left: 100%; }
}
.stat-number {
-
font-size: 2rem;
-
font-weight: bold;
-
color: #3498db;
+
font-weight: 700;
+
color: #1f2937;
+
margin-bottom: 0.25rem;
+
word-break: break-word;
+
overflow-wrap: break-word;
+
line-height: 1.1;
+
+
/* Responsive font sizing using clamp() */
+
font-size: clamp(1.25rem, 4vw, 2.5rem);
}
.stat-label {
-
color: #666;
+
color: #4b5563;
margin-top: 0.5rem;
+
font-weight: 500;
+
line-height: 1.3;
+
+
/* Responsive font sizing for labels */
+
font-size: clamp(0.75rem, 2vw, 0.875rem);
+
}
+
+
/* Container query support for modern browsers */
+
@supports (container-type: inline-size) {
+
.stats-grid {
+
container-type: inline-size;
+
}
+
+
@container (max-width: 250px) {
+
.stat-number {
+
font-size: 1.25rem;
+
}
+
.stat-label {
+
font-size: 0.75rem;
+
}
+
}
+
+
@container (min-width: 300px) {
+
.stat-number {
+
font-size: 2rem;
+
}
+
.stat-label {
+
font-size: 0.875rem;
+
}
+
}
+
+
@container (min-width: 400px) {
+
.stat-number {
+
font-size: 2.5rem;
+
}
+
.stat-label {
+
font-size: 1rem;
+
}
+
}
}
.charts-grid {
···
margin-bottom: 2rem;
}
+
@media (max-width: 480px) {
+
.charts-grid {
+
grid-template-columns: 1fr;
+
}
+
+
.chart-container {
+
min-height: 250px;
+
}
+
+
.stats-grid {
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+
}
+
+
.stat-card {
+
padding: 1rem;
+
min-height: 100px;
+
}
+
+
.stat-number {
+
font-size: clamp(0.9rem, 4vw, 1.5rem) !important;
+
}
+
+
.stat-label {
+
font-size: clamp(0.65rem, 2.5vw, 0.75rem) !important;
+
}
+
}
+
.chart-container {
background: white;
padding: 1.5rem;
-
border-radius: 8px;
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
border-radius: 12px;
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
border: 1px solid #e2e8f0;
+
position: relative;
+
min-height: 350px;
+
max-height: 500px;
+
overflow: hidden;
+
display: flex;
+
flex-direction: column;
+
}
+
+
.chart-container.loading {
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
}
+
+
.chart-container.loading::before {
+
content: 'Loading chart...';
+
color: #64748b;
+
font-size: 0.875rem;
}
.chart-title {
-
font-size: 1.2rem;
+
font-size: 1.125rem;
margin-bottom: 1rem;
-
color: #2c3e50;
+
padding-top: 1rem;
+
color: #1e293b;
+
font-weight: 600;
+
display: block;
+
width: 100%;
+
text-align: left;
+
word-break: break-word;
+
overflow-wrap: break-word;
+
}
+
+
.chart-title-with-indicator {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
flex-wrap: wrap;
+
gap: 0.5rem;
+
margin-bottom: 1rem;
+
}
+
+
.chart-title-with-indicator .chart-title {
+
margin-bottom: 0;
+
flex: 1;
+
min-width: 0;
+
}
+
+
.chart-error {
+
color: #dc2626;
+
text-align: center;
+
padding: 2rem;
+
font-size: 0.875rem;
}
.loading {
text-align: center;
-
padding: 2rem;
-
color: #666;
+
padding: 3rem;
+
color: #64748b;
+
}
+
+
.loading-spinner {
+
display: inline-block;
+
width: 2rem;
+
height: 2rem;
+
border: 3px solid #e2e8f0;
+
border-radius: 50%;
+
border-top-color: #3b82f6;
+
animation: spin 1s ease-in-out infinite;
+
margin-bottom: 1rem;
+
}
+
+
@keyframes spin {
+
to { transform: rotate(360deg); }
}
.error {
-
background: #e74c3c;
-
color: white;
+
background: #fef2f2;
+
color: #dc2626;
padding: 1rem;
-
border-radius: 4px;
+
border-radius: 8px;
margin: 1rem 0;
+
border: 1px solid #fecaca;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 0.5rem;
-
justify-content: center;
-
margin-top: 1rem;
+
font-size: 0.875rem;
+
color: #64748b;
}
.auto-refresh input[type="checkbox"] {
-
transform: scale(1.2);
+
transform: scale(1.1);
+
accent-color: #3b82f6;
+
}
+
+
.performance-indicator {
+
display: inline-flex;
+
align-items: center;
+
gap: 0.25rem;
+
font-size: 0.75rem;
+
color: #64748b;
+
}
+
+
.performance-indicator.good { color: #059669; }
+
.performance-indicator.warning { color: #d97706; }
+
.performance-indicator.error { color: #dc2626; }
+
+
.lazy-chart {
+
min-height: 300px;
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
background: #f8fafc;
+
border-radius: 8px;
+
margin: 1rem 0;
+
}
+
+
.lazy-chart.visible {
+
background: transparent;
}
@media (max-width: 768px) {
···
flex-direction: column;
gap: 1rem;
text-align: center;
+
padding: 1rem;
+
}
+
+
.controls {
+
flex-direction: column;
+
align-items: stretch;
}
+
+
.controls select,
+
.controls button {
+
margin: 0.25rem 0;
+
}
+
+
.stat-number {
+
font-size: clamp(1rem, 5vw, 1.75rem) !important;
+
}
+
+
.stat-label {
+
font-size: clamp(0.7rem, 3vw, 0.8rem) !important;
+
}
+
+
.chart-container {
+
padding: 1rem;
+
min-height: 250px;
+
}
+
+
.chart-title {
+
font-size: 1rem;
+
flex-direction: column;
+
align-items: flex-start;
+
}
+
}
+
+
.toast {
+
position: fixed;
+
top: 1rem;
+
right: 1rem;
+
background: #1f2937;
+
color: white;
+
padding: 0.75rem 1rem;
+
border-radius: 8px;
+
font-size: 0.875rem;
+
z-index: 1000;
+
transform: translateX(100%);
+
transition: transform 0.3s ease;
+
}
+
+
.toast.show {
+
transform: translateX(0);
+
}
+
+
.toast.success {
+
background: #059669;
+
}
+
+
.toast.error {
+
background: #dc2626;
}
</style>
</head>
···
<option value="7" selected>Last 7 days</option>
<option value="30">Last 30 days</option>
</select>
-
<button onclick="loadData()">Refresh</button>
+
<button id="refreshBtn" onclick="loadData()">Refresh</button>
<div class="auto-refresh">
<input type="checkbox" id="autoRefresh" />
<label for="autoRefresh">Auto-refresh (30s)</label>
</div>
</div>
-
<div id="loading" class="loading">Loading analytics data...</div>
+
<div id="loading" class="loading">
+
<div class="loading-spinner"></div>
+
Loading analytics data...
+
</div>
<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">Traffic Overview - All Routes Over Time</div>
+
<div class="chart-title-with-indicator">
+
<div class="chart-title">Traffic Overview - All Routes Over Time</div>
+
<span class="performance-indicator" id="trafficPerformance"></span>
+
</div>
<canvas
id="trafficOverviewChart"
style="padding-bottom: 2rem"
></canvas>
</div>
+
<!-- Stats Grid -->
<div class="stats-grid">
-
<div class="stat-card">
+
<div class="stat-card" id="totalRequestsCard">
<div class="stat-number" id="totalRequests">-</div>
<div class="stat-label">Total Requests</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="avgResponseTimeCard">
<div class="stat-number" id="avgResponseTime">-</div>
<div class="stat-label">Avg Response Time (ms)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="p95ResponseTimeCard">
<div class="stat-number" id="p95ResponseTime">-</div>
<div class="stat-label">P95 Response Time (ms)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="uniqueEndpointsCard">
<div class="stat-number" id="uniqueEndpoints">-</div>
<div class="stat-label">Unique Endpoints</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="errorRateCard">
<div class="stat-number" id="errorRate">-</div>
<div class="stat-label">Error Rate (%)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="fastRequestsCard">
<div class="stat-number" id="fastRequests">-</div>
<div class="stat-label">Fast Requests (&lt;100ms)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="uptimeCard">
<div class="stat-number" id="uptime">-</div>
<div class="stat-label">Uptime (%)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="throughputCard">
<div class="stat-number" id="throughput">-</div>
<div class="stat-label">Throughput (req/hr)</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="apdexCard">
<div class="stat-number" id="apdex">-</div>
<div class="stat-label">APDEX Score</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="cacheHitRateCard">
<div class="stat-number" id="cacheHitRate">-</div>
<div class="stat-label">Cache Hit Rate (%)</div>
</div>
</div>
+
<!-- Peak Traffic Stats -->
<div class="stats-grid">
-
<div class="stat-card">
+
<div class="stat-card" id="peakHourCard">
<div class="stat-number" id="peakHour">-</div>
<div class="stat-label">Peak Hour</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="peakHourRequestsCard">
<div class="stat-number" id="peakHourRequests">-</div>
<div class="stat-label">Peak Hour Requests</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="peakDayCard">
<div class="stat-number" id="peakDay">-</div>
<div class="stat-label">Peak Day</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="peakDayRequestsCard">
<div class="stat-number" id="peakDayRequests">-</div>
<div class="stat-label">Peak Day Requests</div>
</div>
-
<div class="stat-card">
+
<div class="stat-card" id="dashboardRequestsCard">
<div class="stat-number" id="dashboardRequests">-</div>
<div class="stat-label">Dashboard Requests</div>
</div>
</div>
+
<!-- Charts Grid with Lazy Loading -->
<div class="charts-grid">
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="timeChart">
<div class="chart-title">Requests Over Time</div>
<canvas id="timeChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="latencyTimeChart">
<div class="chart-title">Latency Over Time (Hourly)</div>
<canvas id="latencyTimeChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="latencyDistributionChart">
<div class="chart-title">Response Time Distribution</div>
<canvas id="latencyDistributionChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="percentilesChart">
<div class="chart-title">Latency Percentiles</div>
<canvas id="percentilesChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="endpointChart">
<div class="chart-title">Top Endpoints</div>
<canvas id="endpointChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="slowestEndpointsChart">
<div class="chart-title">Slowest Endpoints</div>
<canvas id="slowestEndpointsChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="statusChart">
<div class="chart-title">Status Codes</div>
<canvas id="statusChart"></canvas>
</div>
-
<div class="chart-container">
+
<div class="chart-container lazy-chart" data-chart="userAgentChart">
<div class="chart-title">Top User Agents</div>
<canvas id="userAgentChart"></canvas>
</div>
···
<script>
let charts = {};
let autoRefreshInterval;
+
let currentData = null;
+
let isLoading = false;
+
let visibleCharts = new Set();
+
let intersectionObserver;
+
+
// Performance monitoring
+
const performance = {
+
startTime: 0,
+
endTime: 0,
+
loadTime: 0
+
};
+
+
// 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);
+
}
+
}
+
});
+
}, {
+
rootMargin: '50px',
+
threshold: 0.1
+
});
+
+
// 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);
+
setTimeout(() => {
+
toast.classList.remove('show');
+
setTimeout(() => document.body.removeChild(toast), 300);
+
}, 3000);
+
}
+
+
// Update loading states for stat cards
+
function setStatCardLoading(cardId, loading) {
+
const card = document.getElementById(cardId);
+
if (card) {
+
if (loading) {
+
card.classList.add('loading');
+
} else {
+
card.classList.remove('loading');
+
}
+
}
+
}
+
+
// Debounced resize handler for charts
+
let resizeTimeout;
+
function handleResize() {
+
clearTimeout(resizeTimeout);
+
resizeTimeout = setTimeout(() => {
+
Object.values(charts).forEach(chart => {
+
if (chart && typeof chart.resize === 'function') {
+
chart.resize();
+
}
+
});
+
}, 250);
+
}
+
+
window.addEventListener('resize', handleResize);
async function loadData() {
+
if (isLoading) return;
+
+
isLoading = true;
+
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");
+
// Update UI state
loading.style.display = "block";
error.style.display = "none";
content.style.display = "none";
+
refreshBtn.disabled = true;
+
refreshBtn.textContent = "Loading...";
+
+
// Set all stat cards to loading state
+
const statCards = [
+
'totalRequestsCard', 'avgResponseTimeCard', 'p95ResponseTimeCard',
+
'uniqueEndpointsCard', 'errorRateCard', 'fastRequestsCard',
+
'uptimeCard', 'throughputCard', 'apdexCard', 'cacheHitRateCard',
+
'peakHourCard', 'peakHourRequestsCard', 'peakDayCard',
+
'peakDayRequestsCard', 'dashboardRequestsCard'
+
];
+
statCards.forEach(cardId => setStatCardLoading(cardId, true));
try {
const response = await fetch(`/stats?days=${days}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
+
currentData = data;
+
+
performance.endTime = Date.now();
+
performance.loadTime = performance.endTime - performance.startTime;
+
updateDashboard(data);
-
+
loading.style.display = "none";
content.style.display = "block";
+
+
showToast(`Dashboard updated in ${performance.loadTime}ms`, 'success');
} catch (err) {
loading.style.display = "none";
error.style.display = "block";
error.textContent = `Failed to load data: ${err.message}`;
+
showToast(`Error: ${err.message}`, 'error');
+
} finally {
+
isLoading = false;
+
refreshBtn.disabled = false;
+
refreshBtn.textContent = "Refresh";
+
+
// Remove loading state from stat cards
+
statCards.forEach(cardId => setStatCardLoading(cardId, false));
}
}
function updateDashboard(data) {
-
// Main metrics
-
document.getElementById("totalRequests").textContent =
-
data.totalRequests.toLocaleString();
-
document.getElementById("avgResponseTime").textContent =
-
data.averageResponseTime
-
? Math.round(data.averageResponseTime)
-
: "N/A";
-
document.getElementById("p95ResponseTime").textContent = data
-
.latencyAnalytics.percentiles.p95
-
? Math.round(data.latencyAnalytics.percentiles.p95)
-
: "N/A";
-
document.getElementById("uniqueEndpoints").textContent =
-
data.requestsByEndpoint.length;
+
// 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)
-
: "0.0";
-
document.getElementById("errorRate").textContent = errorRate;
+
const errorRate = data.totalRequests > 0
+
? ((errorRequests / data.totalRequests) * 100).toFixed(1)
+
: "0.0";
+
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);
-
document.getElementById("fastRequests").textContent =
-
fastRequestsData.toFixed(1) + "%";
+
updateStatWithAnimation("fastRequests", fastRequestsData.toFixed(1) + "%");
// Performance metrics
-
document.getElementById("uptime").textContent =
-
data.performanceMetrics.uptime.toFixed(1);
-
document.getElementById("throughput").textContent = Math.round(
-
data.performanceMetrics.throughput,
-
);
-
document.getElementById("apdex").textContent =
-
data.performanceMetrics.apdex.toFixed(2);
-
document.getElementById("cacheHitRate").textContent =
-
data.performanceMetrics.cachehitRate.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));
// Peak traffic
-
document.getElementById("peakHour").textContent =
-
data.peakTraffic.peakHour;
-
document.getElementById("peakHourRequests").textContent =
-
data.peakTraffic.peakRequests.toLocaleString();
-
document.getElementById("peakDay").textContent =
-
data.peakTraffic.peakDay;
-
document.getElementById("peakDayRequests").textContent =
-
data.peakTraffic.peakDayRequests.toLocaleString();
+
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';
+
+
setTimeout(() => {
+
element.textContent = value;
+
element.style.transform = 'scale(1)';
+
}, 100);
+
}
+
}
+
+
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;
+
+
let status, text;
+
if (avgResponseTime < 100 && errorRate < 1) {
+
status = 'good';
+
text = '🟢 Excellent';
+
} else if (avgResponseTime < 300 && errorRate < 5) {
+
status = 'warning';
+
text = '🟡 Good';
+
} else {
+
status = 'error';
+
text = '🔴 Needs Attention';
+
}
-
// Dashboard metrics
-
document.getElementById("dashboardRequests").textContent =
-
data.dashboardMetrics.statsRequests.toLocaleString();
+
indicator.className = `performance-indicator ${status}`;
+
indicator.textContent = text;
+
}
-
// Determine if we're showing hourly or daily data
+
// Load individual charts (called by intersection observer)
+
function loadChart(chartType, data) {
const days = parseInt(document.getElementById("daysSelect").value);
const isHourly = days === 1;
-
updateTrafficOverviewChart(data.trafficOverview, days);
-
updateTimeChart(data.requestsByDay, isHourly);
-
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
-
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
-
updatePercentilesChart(data.latencyAnalytics.percentiles);
-
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
-
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
-
updateStatusChart(data.requestsByStatus);
-
updateUserAgentChart(data.topUserAgents.slice(0, 5));
+
try {
+
switch(chartType) {
+
case 'timeChart':
+
updateTimeChart(data.requestsByDay, isHourly);
+
break;
+
case 'latencyTimeChart':
+
updateLatencyTimeChart(data.latencyAnalytics.latencyOverTime, isHourly);
+
break;
+
case 'latencyDistributionChart':
+
updateLatencyDistributionChart(data.latencyAnalytics.distribution);
+
break;
+
case 'percentilesChart':
+
updatePercentilesChart(data.latencyAnalytics.percentiles);
+
break;
+
case 'endpointChart':
+
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
+
break;
+
case 'slowestEndpointsChart':
+
updateSlowestEndpointsChart(data.latencyAnalytics.slowestEndpoints);
+
break;
+
case 'statusChart':
+
updateStatusChart(data.requestsByStatus);
+
break;
+
case 'userAgentChart':
+
updateUserAgentChart(data.topUserAgents.slice(0, 5));
+
break;
+
}
+
} catch (error) {
+
console.error(`Error loading chart ${chartType}:`, error);
+
const container = document.querySelector(`[data-chart="${chartType}"]`);
+
if (container) {
+
container.innerHTML = `<div class="chart-error">Error loading chart: ${error.message}</div>`;
+
}
+
}
}
function updateTrafficOverviewChart(data, days) {
-
const ctx = document
-
.getElementById("trafficOverviewChart")
-
.getContext("2d");
+
const canvas = document.getElementById("trafficOverviewChart");
+
if (!canvas) {
+
console.warn('trafficOverviewChart canvas not found');
+
return;
+
}
+
+
const ctx = canvas.getContext("2d");
+
if (!ctx) {
+
console.warn('Could not get 2d context for trafficOverviewChart');
+
return;
+
}
-
if (charts.trafficOverview) charts.trafficOverview.destroy();
+
if (charts.trafficOverview) {
+
charts.trafficOverview.destroy();
+
}
// Update chart title based on granularity
-
const chartTitle = document
-
.querySelector("#trafficOverviewChart")
-
.parentElement.querySelector(".chart-title");
+
const chartTitleElement = document.querySelector(".chart-title-with-indicator .chart-title");
let titleText = "Traffic Overview - All Routes Over Time";
if (days === 1) {
titleText += " (Hourly)";
···
} else {
titleText += " (Daily)";
}
-
chartTitle.textContent = titleText;
+
if (chartTitleElement) {
+
chartTitleElement.textContent = titleText;
+
}
// Get all unique routes across all time periods
const allRoutes = new Set();
···
// Define colors for different route types
const routeColors = {
-
Dashboard: "#3498db",
-
"User Data": "#2ecc71",
-
"User Redirects": "#27ae60",
-
"Emoji Data": "#e74c3c",
-
"Emoji Redirects": "#c0392b",
-
"Emoji List": "#e67e22",
-
"Health Check": "#f39c12",
-
"API Documentation": "#9b59b6",
-
"Cache Management": "#34495e",
+
Dashboard: "#3b82f6",
+
"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] || "#95a5a6";
+
const color = routeColors[route] || "#9ca3af";
return {
label: route,
data: data.map((timePoint) => timePoint.routes[route] || 0),
borderColor: color,
-
backgroundColor: color + "20", // Add transparency
+
backgroundColor: color + "20",
tension: 0.4,
fill: false,
pointRadius: 2,
···
// Format labels based on time granularity
const labels = data.map((timePoint) => {
if (days === 1) {
-
// Show just hour for 24h view
return timePoint.time.split(" ")[1] || timePoint.time;
} else if (days <= 7) {
-
// Show day and hour for 7-day view
const parts = timePoint.time.split(" ");
-
const date = parts[0].split("-")[2]; // Get day
+
const date = parts[0].split("-")[2];
const hour = parts[1] || "00:00";
return `${date} ${hour}`;
} else {
-
// Show full date for longer periods
return timePoint.time;
}
});
···
options: {
responsive: true,
maintainAspectRatio: false,
+
layout: {
+
padding: {
+
left: 10,
+
right: 10,
+
top: 10,
+
bottom: 50
+
}
+
},
+
animation: {
+
duration: 750,
+
easing: 'easeInOutQuart'
+
},
interaction: {
mode: "index",
intersect: false,
···
font: {
size: 11,
},
+
boxWidth: 12,
+
boxHeight: 12,
+
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()})`;
+
});
+
+
return labels;
+
}
},
},
tooltip: {
mode: "index",
intersect: false,
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
callbacks: {
-
afterLabel: function (context) {
-
const timePoint = data[context.dataIndex];
-
return `Total: ${timePoint.total} requests`;
+
title: function(context) {
+
return `Time: ${context[0].label}`;
+
},
+
afterBody: function(context) {
+
const timePoint = data[context[0].dataIndex];
+
return [
+
`Total Requests: ${timePoint.total.toLocaleString()}`,
+
`Peak Route: ${Object.entries(timePoint.routes).sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A'}`
+
];
},
},
},
···
title: {
display: true,
text: days === 1 ? "Hour" : days <= 7 ? "Day & Hour" : "Date",
+
font: {
+
weight: 'bold'
+
}
},
ticks: {
-
maxTicksLimit: 20,
+
maxTicksLimit: window.innerWidth < 768 ? 8 : 20,
+
maxRotation: 0, // Don't rotate labels
+
minRotation: 0,
+
callback: function(value, index, values) {
+
const label = this.getLabelForValue(value);
+
// Truncate long labels
+
if (label.length > 8) {
+
if (days === 1) {
+
return label; // Hours are usually short
+
} else {
+
// For longer periods, abbreviate
+
return label.substring(0, 6) + '...';
+
}
+
}
+
return label;
+
}
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
},
},
y: {
···
title: {
display: true,
text: "Requests",
+
font: {
+
weight: 'bold'
+
}
},
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return value.toLocaleString();
+
}
+
}
},
},
elements: {
line: {
tension: 0.4,
},
+
point: {
+
radius: 3,
+
hoverRadius: 6,
+
hitRadius: 10,
+
},
},
},
});
}
function updateTimeChart(data, isHourly) {
-
const ctx = document.getElementById("timeChart").getContext("2d");
+
const canvas = document.getElementById("timeChart");
+
if (!canvas) {
+
console.warn('timeChart canvas not found');
+
return;
+
}
+
+
const ctx = canvas.getContext("2d");
+
if (!ctx) {
+
console.warn('Could not get 2d context for timeChart');
+
return;
+
}
if (charts.time) charts.time.destroy();
-
// Update chart title
const chartTitle = document
.querySelector("#timeChart")
.parentElement.querySelector(".chart-title");
-
chartTitle.textContent = isHourly
-
? "Requests Over Time (Hourly)"
-
: "Requests Over Time (Daily)";
+
if (chartTitle) {
+
chartTitle.textContent = isHourly
+
? "Requests Over Time (Hourly)"
+
: "Requests Over Time (Daily)";
+
}
charts.time = new Chart(ctx, {
type: "line",
···
{
label: "Requests",
data: data.map((d) => d.count),
-
borderColor: "#3498db",
-
backgroundColor: "rgba(52, 152, 219, 0.1)",
+
borderColor: "#3b82f6",
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
tension: 0.4,
fill: true,
+
pointRadius: 4,
+
pointHoverRadius: 6,
+
pointBackgroundColor: "#3b82f6",
+
pointBorderColor: "#ffffff",
+
pointBorderWidth: 2,
},
],
},
options: {
responsive: true,
+
maintainAspectRatio: false,
+
layout: {
+
padding: {
+
left: 10,
+
right: 10,
+
top: 10,
+
bottom: 50 // Extra space for rotated labels
+
}
+
},
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
+
plugins: {
+
tooltip: {
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
+
callbacks: {
+
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];
+
return [
+
`Requests: ${context.parsed.y.toLocaleString()}`,
+
`Avg Response Time: ${Math.round(point.averageResponseTime || 0)}ms`
+
];
+
}
+
}
+
},
+
legend: {
+
labels: {
+
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);
+
return [{
+
text: `Requests (Total: ${total.toLocaleString()}, Avg RT: ${avg}ms)`,
+
fillStyle: '#3b82f6',
+
strokeStyle: '#3b82f6',
+
lineWidth: 2,
+
pointStyle: 'circle'
+
}];
+
}
+
}
+
}
+
},
scales: {
+
x: {
+
title: {
+
display: true,
+
text: isHourly ? 'Hour of Day' : 'Date',
+
font: { weight: 'bold' }
+
},
+
ticks: {
+
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
+
maxRotation: 0, // Don't rotate labels
+
minRotation: 0,
+
callback: function(value, index, values) {
+
const label = this.getLabelForValue(value);
+
// Truncate long labels for better fit
+
if (isHourly) {
+
return label; // Hours are short
+
} else {
+
// For dates, show abbreviated format
+
const date = new Date(label);
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+
}
+
}
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
},
y: {
+
title: {
+
display: true,
+
text: 'Number of Requests',
+
font: { weight: 'bold' }
+
},
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return value.toLocaleString();
+
}
+
}
},
},
},
···
}
function updateEndpointChart(data) {
-
const ctx = document.getElementById("endpointChart").getContext("2d");
+
const canvas = document.getElementById("endpointChart");
+
if (!canvas) {
+
console.warn('endpointChart canvas not found');
+
return;
+
}
+
+
const ctx = canvas.getContext("2d");
+
if (!ctx) {
+
console.warn('Could not get 2d context for endpointChart');
+
return;
+
}
if (charts.endpoint) charts.endpoint.destroy();
···
{
label: "Requests",
data: data.map((d) => d.count),
-
backgroundColor: "#2ecc71",
+
backgroundColor: "#10b981",
+
borderRadius: 4,
},
],
},
options: {
responsive: true,
-
indexAxis: "y",
+
maintainAspectRatio: false,
+
layout: {
+
padding: {
+
left: 10,
+
right: 10,
+
top: 10,
+
bottom: 60 // Extra space for labels
+
}
+
},
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
+
indexAxis: "x",
scales: {
x: {
+
title: {
+
display: true,
+
text: 'Endpoints',
+
font: { weight: 'bold' }
+
},
+
ticks: {
+
maxRotation: 0, // Don't rotate
+
minRotation: 0,
+
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;
+
}
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
},
+
y: {
+
title: {
+
display: true,
+
text: 'Number of Requests',
+
font: { weight: 'bold' }
+
},
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return value.toLocaleString();
+
}
+
}
},
},
+
plugins: {
+
tooltip: {
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
+
callbacks: {
+
title: function(context) {
+
return context[0].label; // Show full label in tooltip
+
},
+
label: function(context) {
+
return `Requests: ${context.parsed.y.toLocaleString()}`;
+
}
+
}
+
},
+
legend: {
+
labels: {
+
generateLabels: function(chart) {
+
const total = data.reduce((sum, d) => sum + d.count, 0);
+
return [{
+
text: `Total Requests: ${total.toLocaleString()}`,
+
fillStyle: '#10b981',
+
strokeStyle: '#10b981',
+
lineWidth: 2,
+
pointStyle: 'rect'
+
}];
+
}
+
}
+
}
+
},
},
});
}
···
if (charts.status) charts.status.destroy();
const colors = data.map((d) => {
-
if (d.status >= 200 && d.status < 300) return "#2ecc71";
-
if (d.status >= 300 && d.status < 400) return "#f39c12";
-
if (d.status >= 400 && d.status < 500) return "#e74c3c";
-
return "#9b59b6";
+
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";
+
return "#8b5cf6";
});
charts.status = new Chart(ctx, {
···
{
data: data.map((d) => d.count),
backgroundColor: colors,
+
borderWidth: 2,
+
borderColor: '#fff'
},
],
},
options: {
responsive: true,
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
},
});
}
···
{
data: data.map((d) => d.count),
backgroundColor: [
-
"#3498db",
-
"#e74c3c",
-
"#2ecc71",
-
"#f39c12",
-
"#9b59b6",
-
"#34495e",
-
"#16a085",
-
"#8e44ad",
-
"#d35400",
-
"#7f8c8d",
+
"#3b82f6",
+
"#ef4444",
+
"#10b981",
+
"#f59e0b",
+
"#8b5cf6",
+
"#6b7280",
+
"#06b6d4",
+
"#84cc16",
+
"#f97316",
+
"#64748b",
],
+
borderWidth: 2,
+
borderColor: '#fff'
},
],
},
options: {
responsive: true,
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
},
});
}
function updateLatencyTimeChart(data, isHourly) {
-
const ctx = document
-
.getElementById("latencyTimeChart")
-
.getContext("2d");
+
const ctx = document.getElementById("latencyTimeChart").getContext("2d");
if (charts.latencyTime) charts.latencyTime.destroy();
-
// Update chart title
const chartTitle = document
.querySelector("#latencyTimeChart")
.parentElement.querySelector(".chart-title");
-
chartTitle.textContent = isHourly
-
? "Latency Over Time (Hourly)"
-
: "Latency Over Time (Daily)";
+
if (chartTitle) {
+
chartTitle.textContent = isHourly
+
? "Latency Over Time (Hourly)"
+
: "Latency Over Time (Daily)";
+
}
charts.latencyTime = new Chart(ctx, {
type: "line",
···
{
label: "Average Response Time",
data: data.map((d) => d.averageResponseTime),
-
borderColor: "#3498db",
-
backgroundColor: "rgba(52, 152, 219, 0.1)",
+
borderColor: "#3b82f6",
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
tension: 0.4,
yAxisID: "y",
+
pointRadius: 4,
+
pointHoverRadius: 6,
+
pointBackgroundColor: "#3b82f6",
+
pointBorderColor: "#ffffff",
+
pointBorderWidth: 2,
},
{
label: "P95 Response Time",
data: data.map((d) => d.p95),
-
borderColor: "#e74c3c",
-
backgroundColor: "rgba(231, 76, 60, 0.1)",
+
borderColor: "#ef4444",
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
tension: 0.4,
yAxisID: "y",
+
pointRadius: 4,
+
pointHoverRadius: 6,
+
pointBackgroundColor: "#ef4444",
+
pointBorderColor: "#ffffff",
+
pointBorderWidth: 2,
},
],
},
options: {
responsive: true,
+
maintainAspectRatio: false,
+
layout: {
+
padding: {
+
left: 10,
+
right: 10,
+
top: 10,
+
bottom: 50
+
}
+
},
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
+
plugins: {
+
tooltip: {
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
+
callbacks: {
+
title: function(context) {
+
return isHourly ? `Hour: ${context[0].label}` : `Date: ${context[0].label}`;
+
},
+
afterBody: function(context) {
+
const point = data[context[0].dataIndex];
+
return [
+
`Request Count: ${point.count.toLocaleString()}`,
+
`Performance: ${point.averageResponseTime < 100 ? 'Excellent' : point.averageResponseTime < 300 ? 'Good' : 'Needs Attention'}`
+
];
+
}
+
}
+
},
+
legend: {
+
labels: {
+
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);
+
return [
+
{
+
text: `Average Response Time (Overall: ${avgAvg}ms)`,
+
fillStyle: '#3b82f6',
+
strokeStyle: '#3b82f6',
+
lineWidth: 2,
+
pointStyle: 'circle'
+
},
+
{
+
text: `P95 Response Time (Overall: ${avgP95}ms)`,
+
fillStyle: '#ef4444',
+
strokeStyle: '#ef4444',
+
lineWidth: 2,
+
pointStyle: 'circle'
+
}
+
];
+
}
+
}
+
}
+
},
scales: {
+
x: {
+
title: {
+
display: true,
+
text: isHourly ? 'Hour of Day' : 'Date',
+
font: { weight: 'bold' }
+
},
+
ticks: {
+
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
+
maxRotation: 0,
+
minRotation: 0,
+
callback: function(value, index, values) {
+
const label = this.getLabelForValue(value);
+
if (isHourly) {
+
return label;
+
} else {
+
const date = new Date(label);
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+
}
+
}
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
},
y: {
-
beginAtZero: true,
title: {
display: true,
text: "Response Time (ms)",
+
font: { weight: 'bold' }
},
+
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return Math.round(value) + 'ms';
+
}
+
}
},
},
},
···
}
function updateLatencyDistributionChart(data) {
-
const ctx = document
-
.getElementById("latencyDistributionChart")
-
.getContext("2d");
+
const ctx = document.getElementById("latencyDistributionChart").getContext("2d");
if (charts.latencyDistribution) charts.latencyDistribution.destroy();
···
{
label: "Requests",
data: data.map((d) => d.count),
-
backgroundColor: "#2ecc71",
+
backgroundColor: "#10b981",
+
borderRadius: 4,
},
],
},
options: {
responsive: true,
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
scales: {
y: {
beginAtZero: true,
···
}
function updatePercentilesChart(percentiles) {
-
const ctx = document
-
.getElementById("percentilesChart")
-
.getContext("2d");
+
const ctx = document.getElementById("percentilesChart").getContext("2d");
if (charts.percentiles) charts.percentiles.destroy();
const data = [
-
{ label: "P50", value: percentiles.p50 },
+
{ label: "P50 (Median)", value: percentiles.p50 },
{ label: "P75", value: percentiles.p75 },
{ label: "P90", value: percentiles.p90 },
{ label: "P95", value: percentiles.p95 },
···
label: "Response Time (ms)",
data: data.map((d) => d.value),
backgroundColor: [
-
"#3498db",
-
"#2ecc71",
-
"#f39c12",
-
"#e74c3c",
-
"#9b59b6",
+
"#10b981", // P50 - Green (good)
+
"#3b82f6", // P75 - Blue
+
"#f59e0b", // P90 - Yellow (warning)
+
"#ef4444", // P95 - Red (concerning)
+
"#8b5cf6", // P99 - Purple (critical)
],
+
borderRadius: 4,
+
borderWidth: 2,
+
borderColor: '#ffffff',
},
],
},
options: {
responsive: true,
+
maintainAspectRatio: false,
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
+
plugins: {
+
tooltip: {
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
+
callbacks: {
+
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';
+
}
+
+
return [
+
`${percentile}: ${value}ms`,
+
interpretation
+
];
+
}
+
}
+
},
+
legend: {
+
display: false // Hide legend since colors are self-explanatory
+
}
+
},
scales: {
+
x: {
+
title: {
+
display: true,
+
text: 'Response Time Percentiles',
+
font: { weight: 'bold' }
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
},
y: {
+
title: {
+
display: true,
+
text: 'Response Time (ms)',
+
font: { weight: 'bold' }
+
},
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return Math.round(value) + 'ms';
+
}
+
}
},
},
},
···
}
function updateSlowestEndpointsChart(data) {
-
const ctx = document
-
.getElementById("slowestEndpointsChart")
-
.getContext("2d");
+
const ctx = document.getElementById("slowestEndpointsChart").getContext("2d");
if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
···
{
label: "Avg Response Time (ms)",
data: data.map((d) => d.averageResponseTime),
-
backgroundColor: "#e74c3c",
+
backgroundColor: "#ef4444",
+
borderRadius: 4,
},
],
},
options: {
responsive: true,
-
indexAxis: "y",
+
maintainAspectRatio: false,
+
animation: {
+
duration: 500,
+
easing: 'easeInOutQuart'
+
},
+
indexAxis: "x", // Changed from "y" to "x" to put labels on top
scales: {
x: {
+
title: {
+
display: true,
+
text: 'Endpoints',
+
font: { weight: 'bold' }
+
},
+
ticks: {
+
maxRotation: 45,
+
minRotation: 45,
+
callback: function(value, index, values) {
+
const label = this.getLabelForValue(value);
+
return label.length > 15 ? label.substring(0, 12) + '...' : label;
+
}
+
},
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
},
+
y: {
+
title: {
+
display: true,
+
text: 'Response Time (ms)',
+
font: { weight: 'bold' }
+
},
beginAtZero: true,
+
grid: {
+
color: 'rgba(0, 0, 0, 0.05)',
+
},
+
ticks: {
+
callback: function(value) {
+
return Math.round(value) + 'ms';
+
}
+
}
},
},
+
plugins: {
+
tooltip: {
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
+
titleColor: 'white',
+
bodyColor: 'white',
+
borderColor: 'rgba(255, 255, 255, 0.1)',
+
borderWidth: 1,
+
callbacks: {
+
title: function(context) {
+
return context[0].label; // Show full label in tooltip
+
},
+
label: function(context) {
+
const point = data[context.dataIndex];
+
return [
+
`Avg Response Time: ${Math.round(context.parsed.y)}ms`,
+
`Request Count: ${point.count.toLocaleString()}`
+
];
+
}
+
}
+
},
+
legend: {
+
labels: {
+
generateLabels: function(chart) {
+
const avgTime = Math.round(data.reduce((sum, d) => sum + d.averageResponseTime, 0) / data.length);
+
return [{
+
text: `Average Response Time: ${avgTime}ms`,
+
fillStyle: '#ef4444',
+
strokeStyle: '#ef4444',
+
lineWidth: 2,
+
pointStyle: 'rect'
+
}];
+
}
+
}
+
}
+
},
},
});
}
-
document
-
.getElementById("autoRefresh")
-
.addEventListener("change", function () {
-
if (this.checked) {
-
autoRefreshInterval = setInterval(loadData, 30000);
-
} else {
-
clearInterval(autoRefreshInterval);
+
document.getElementById("autoRefresh").addEventListener("change", function () {
+
if (this.checked) {
+
autoRefreshInterval = setInterval(loadData, 30000);
+
showToast('Auto-refresh enabled', 'success');
+
} else {
+
clearInterval(autoRefreshInterval);
+
showToast('Auto-refresh disabled', 'info');
+
}
+
});
+
+
// Days selector change handler
+
document.getElementById("daysSelect").addEventListener("change", function() {
+
// Reset visible charts when changing time period
+
visibleCharts.clear();
+
loadData();
+
});
+
+
// Initialize dashboard
+
document.addEventListener('DOMContentLoaded', function() {
+
initLazyLoading();
+
loadData();
+
});
+
+
// 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') {
+
chart.destroy();
}
});
-
-
loadData();
-
document
-
.getElementById("daysSelect")
-
.addEventListener("change", loadData);
+
});
</script>
</body>
</html>