peerman 0.2.2

DN42 peer manager with WireGuard, BIRD, and cluster support
import { useEffect, useRef } from 'react';
import { RotateCw, Activity, Calendar, BarChart3 } from 'lucide-react';
import { useFlapEvents, useFlapStats } from '../../hooks/useFlaps';
import { cn } from '../../lib/utils';

const SOURCE_LABELS: Record<string, string> = {
  ibgp: 'iBGP',
  socket: 'Socket',
  probe: 'Probe',
};

function relativeTime(rfc3339: string): string {
  if (!rfc3339) return '—';
  const diff = Date.now() - new Date(rfc3339).getTime();
  const seconds = Math.floor(diff / 1000);
  if (seconds < 60) return `${seconds}s ago`;
  const minutes = Math.floor(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

export default function FlapDashboard() {
  const { events: activeEvents, loading, error, refetch } = useFlapEvents(true);
  const { events: allEvents } = useFlapEvents(false);
  const { stats, refetch: refetchStats } = useFlapStats();
  const intervalRef = useRef<ReturnType<typeof setInterval>>();

  // Auto-refresh every 30s
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      refetch();
      refetchStats();
    }, 30_000);
    return () => clearInterval(intervalRef.current);
  }, [refetch, refetchStats]);

  return (
    <div className="space-y-xl animate-fade-in">
      <div>
        <h1 className="text-display-md text-ink">Route Flap Detection</h1>
        <p className="text-body-md text-body mt-1">
          Real-time BGP route flap monitoring across cluster nodes.
        </p>
      </div>

      {/* Stats cards */}
      <div className="grid grid-cols-3 gap-lg">
        <div className="card flex items-center gap-md">
          <Activity className="w-5 h-5 text-warning" />
          <div>
            <p className="text-caption-mono text-mute uppercase">Active Flaps</p>
            <p className="text-display-sm text-ink">{stats.activeCount}</p>
          </div>
        </div>
        <div className="card flex items-center gap-md">
          <Calendar className="w-5 h-5 text-body" />
          <div>
            <p className="text-caption-mono text-mute uppercase">Today's Events</p>
            <p className="text-display-sm text-ink">{stats.totalToday}</p>
          </div>
        </div>
        <div className="card flex items-center gap-md">
          <BarChart3 className="w-5 h-5 text-link" />
          <div>
            <p className="text-caption-mono text-mute uppercase">Avg Rate</p>
            <p className="text-display-sm text-ink">
              {stats.avgChangesPerHour.toFixed(1)}/h
            </p>
          </div>
        </div>
      </div>

      {/* Error */}
      {error && (
        <div className="card border border-error bg-error-soft">
          <p className="text-body-sm text-error">{error}</p>
        </div>
      )}

      {/* Loading */}
      {loading && (
        <div className="card flex items-center gap-3 text-body">
          <RotateCw className="w-4 h-4 animate-spin" />
          <span className="text-body-sm">Loading flap data...</span>
        </div>
      )}

      {/* Active Flaps */}
      {!loading && (
        <div>
          <h2 className="text-display-sm text-ink mb-md">
            Active Flaps
            <span className="text-body-md text-mute ml-2 font-normal">
              ({activeEvents.length})
            </span>
          </h2>

          {activeEvents.length === 0 ? (
            <div className="card-soft text-center py-2xl">
              <p className="text-body-md text-mute">No active flaps detected.</p>
              <p className="text-body-sm text-mute mt-1">
                BGP routes are currently stable.
              </p>
            </div>
          ) : (
            <div className="card overflow-x-auto">
              <table className="data-table">
                <thead>
                  <tr>
                    <th>Prefix</th>
                    <th>Type</th>
                    <th>Changes</th>
                    <th>Source</th>
                    <th>Detected</th>
                  </tr>
                </thead>
                <tbody>
                  {activeEvents.map((e) => (
                    <tr key={e.id}>
                      <td className="text-body-sm-strong font-mono">{e.prefix}</td>
                      <td>
                        <span className="badge">{e.prefixType}</span>
                      </td>
                      <td className="text-body-sm-strong text-warning">
                        {String(e.changeCount)}
                      </td>
                      <td>
                        <span
                          className={cn(
                            'badge',
                            e.source === 'ibgp' && 'bg-link-bg-soft text-link-deep',
                            e.source === 'socket' && 'bg-cyan-soft text-cyan-deep',
                            e.source === 'probe' && 'bg-warning-soft text-warning-deep'
                          )}
                        >
                          {SOURCE_LABELS[e.source] || e.source}
                        </span>
                      </td>
                      <td className="text-body-sm text-body">
                        {relativeTime(e.detectedAt)}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </div>
      )}

      {/* History */}
      {!loading && allEvents.length > 0 && (
        <div>
          <h2 className="text-display-sm text-ink mb-md">Recent History</h2>
          <div className="card overflow-x-auto">
            <table className="data-table">
              <thead>
                <tr>
                  <th>Prefix</th>
                  <th>Type</th>
                  <th>Changes</th>
                  <th>Source</th>
                  <th>Status</th>
                  <th>Detected</th>
                  <th>Resolved</th>
                </tr>
              </thead>
              <tbody>
                {allEvents.slice(0, 50).map((e) => (
                  <tr key={e.id}>
                    <td className="text-body-sm-strong font-mono">{e.prefix}</td>
                    <td>
                      <span className="badge">{e.prefixType}</span>
                    </td>
                    <td className="text-body-sm-strong">{String(e.changeCount)}</td>
                    <td>
                      <span
                        className={cn(
                          'badge',
                          e.source === 'ibgp' && 'bg-link-bg-soft text-link-deep',
                          e.source === 'socket' && 'bg-cyan-soft text-cyan-deep',
                          e.source === 'probe' && 'bg-warning-soft text-warning-deep'
                        )}
                      >
                        {SOURCE_LABELS[e.source] || e.source}
                      </span>
                    </td>
                    <td>
                      <span
                        className={cn(
                          'badge',
                          e.active
                            ? 'bg-warning-soft text-warning-deep'
                            : 'bg-canvas-soft-2 text-mute'
                        )}
                      >
                        {e.active ? 'Active' : 'Resolved'}
                      </span>
                    </td>
                    <td className="text-body-sm text-body">
                      {relativeTime(e.detectedAt)}
                    </td>
                    <td className="text-body-sm text-mute">
                      {e.resolvedAt ? relativeTime(e.resolvedAt) : '—'}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
    </div>
  );
}