← back to Wine Finder Next
app/performance/page.tsx
296 lines
'use client';
import { useState, useEffect } from 'react';
import { Line, Bar, Pie } from 'recharts';
interface PerformanceMetrics {
timestamp: string;
metrics: {
api: Record<string, any>;
cache: {
hitRate: string;
hits: number;
misses: number;
keys: number;
memoryUsed: string;
evictedKeys: number;
};
memory: {
rss: string;
heapUsed: string;
heapTotal: string;
external: string;
};
responseTimeBuckets: {
fast: number;
medium: number;
slow: number;
};
recommendations: string[];
};
}
export default function PerformanceDashboard() {
const [metrics, setMetrics] = useState<PerformanceMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(true);
const [history, setHistory] = useState<any[]>([]);
const loadMetrics = async () => {
try {
const response = await fetch('/api/performance/metrics');
const data = await response.json();
setMetrics(data);
// Add to history (keep last 20 data points)
setHistory(prev => {
const newHistory = [...prev, {
time: new Date().toLocaleTimeString(),
cacheHitRate: parseFloat(data.metrics.cache.hitRate),
heapUsed: parseFloat(data.metrics.memory.heapUsed),
}].slice(-20);
return newHistory;
});
} catch (error) {
console.error('Failed to load metrics:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMetrics();
if (autoRefresh) {
const interval = setInterval(loadMetrics, 5000);
return () => clearInterval(interval);
}
}, [autoRefresh]);
if (loading) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-2xl">Loading performance metrics...</div>
</div>
);
}
if (!metrics) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div className="text-2xl text-red-400">Failed to load metrics</div>
</div>
);
}
const { api, cache, memory, responseTimeBuckets, recommendations } = metrics.metrics;
// Prepare data for charts
const apiMetricsData = Object.entries(api).map(([name, stats]: [string, any]) => ({
name: name.replace('api.', ''),
mean: stats?.mean?.toFixed(2) || 0,
p95: stats?.p95?.toFixed(2) || 0,
p99: stats?.p99?.toFixed(2) || 0,
}));
const responseTimeData = [
{ name: 'Fast (<100ms)', value: responseTimeBuckets.fast, color: '#10b981' },
{ name: 'Medium (100-500ms)', value: responseTimeBuckets.medium, color: '#f59e0b' },
{ name: 'Slow (>500ms)', value: responseTimeBuckets.slow, color: '#ef4444' },
];
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-4xl font-bold mb-2">Performance Dashboard</h1>
<p className="text-gray-400">Wine Finder Platform Metrics</p>
</div>
<div className="flex gap-4">
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`px-4 py-2 rounded-lg ${
autoRefresh ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-700'
}`}
>
{autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
</button>
<button
onClick={loadMetrics}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg"
>
Refresh Now
</button>
</div>
</div>
{/* Key Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<MetricCard
title="Cache Hit Rate"
value={cache.hitRate}
subtitle={`${cache.hits} hits / ${cache.misses} misses`}
color="green"
/>
<MetricCard
title="Cache Keys"
value={cache.keys.toLocaleString()}
subtitle={`Memory: ${cache.memoryUsed}`}
color="blue"
/>
<MetricCard
title="Heap Memory"
value={memory.heapUsed}
subtitle={`Total: ${memory.heapTotal}`}
color="purple"
/>
<MetricCard
title="API Endpoints"
value={apiMetricsData.length}
subtitle={`Tracked endpoints`}
color="orange"
/>
</div>
{/* Response Time Distribution */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div className="bg-gray-800 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4">Response Time Distribution</h2>
<div className="space-y-4">
{responseTimeData.map(item => (
<div key={item.name}>
<div className="flex justify-between text-sm mb-1">
<span>{item.name}</span>
<span>{item.value} endpoints</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-4">
<div
className="h-4 rounded-full"
style={{
width: `${(item.value / (responseTimeBuckets.fast + responseTimeBuckets.medium + responseTimeBuckets.slow)) * 100}%`,
backgroundColor: item.color,
}}
/>
</div>
</div>
))}
</div>
</div>
<div className="bg-gray-800 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4">Cache Performance Trend</h2>
{history.length > 0 && (
<div className="h-48">
<div className="space-y-2">
{history.slice(-5).map((point, idx) => (
<div key={idx} className="flex justify-between text-sm">
<span className="text-gray-400">{point.time}</span>
<span className={point.cacheHitRate > 80 ? 'text-green-400' : 'text-orange-400'}>
{point.cacheHitRate.toFixed(1)}%
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* API Performance Table */}
<div className="bg-gray-800 rounded-xl p-6 mb-8">
<h2 className="text-xl font-bold mb-4">API Endpoint Performance</h2>
{apiMetricsData.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-gray-700">
<th className="pb-2">Endpoint</th>
<th className="pb-2 text-right">Avg (ms)</th>
<th className="pb-2 text-right">P95 (ms)</th>
<th className="pb-2 text-right">P99 (ms)</th>
<th className="pb-2 text-right">Status</th>
</tr>
</thead>
<tbody>
{apiMetricsData.map((endpoint) => (
<tr key={endpoint.name} className="border-b border-gray-700">
<td className="py-2">{endpoint.name}</td>
<td className="py-2 text-right">{endpoint.mean}</td>
<td className="py-2 text-right">{endpoint.p95}</td>
<td className="py-2 text-right">{endpoint.p99}</td>
<td className="py-2 text-right">
<span
className={`px-2 py-1 rounded text-xs ${
parseFloat(endpoint.p95) < 100
? 'bg-green-600'
: parseFloat(endpoint.p95) < 500
? 'bg-orange-600'
: 'bg-red-600'
}`}
>
{parseFloat(endpoint.p95) < 100
? 'Fast'
: parseFloat(endpoint.p95) < 500
? 'OK'
: 'Slow'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-400">No API metrics available yet. Make some API calls to see performance data.</p>
)}
</div>
{/* Recommendations */}
<div className="bg-gray-800 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4">Performance Recommendations</h2>
<div className="space-y-2">
{recommendations.map((rec, idx) => (
<div key={idx} className="flex items-start gap-3">
<span className="text-2xl">
{rec.includes('good') ? '✅' : '⚠️'}
</span>
<p className="text-gray-300">{rec}</p>
</div>
))}
</div>
</div>
{/* Last Updated */}
<div className="text-center text-gray-500 mt-8">
Last updated: {new Date(metrics.timestamp).toLocaleString()}
</div>
</div>
</div>
);
}
function MetricCard({ title, value, subtitle, color }: {
title: string;
value: string | number;
subtitle: string;
color: string;
}) {
const colorClasses = {
green: 'bg-green-600',
blue: 'bg-blue-600',
purple: 'bg-purple-600',
orange: 'bg-orange-600',
};
return (
<div className="bg-gray-800 rounded-xl p-6">
<h3 className="text-sm text-gray-400 mb-2">{title}</h3>
<div className={`text-3xl font-bold mb-2 ${colorClasses[color as keyof typeof colorClasses] || 'text-white'}`}>
{value}
</div>
<p className="text-xs text-gray-500">{subtitle}</p>
</div>
);
}