← 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>
  );
}