peerman 0.1.8

DN42 peer manager with WireGuard, BIRD, and cluster support
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Pencil, Trash2 } from 'lucide-react';
import { usePeer, useWireGuardConfig, useBirdConfig } from '../../hooks/usePeers';
import { peerClient } from '../../lib/grpc';
import ConfigViewer from './ConfigViewer';

export default function PeerDetail() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const { peer, loading, error } = usePeer(id);
  const [tab, setTab] = useState<'info' | 'wg' | 'bird'>('info');

  const wg = useWireGuardConfig(tab === 'wg' ? id : undefined);
  const bird = useBirdConfig(tab === 'bird' ? id : undefined);

  const handleDelete = async () => {
    if (!peer || !confirm(`Delete peer "${peer.name}"?`)) return;
    await peerClient.deletePeer({ id: peer.id });
    navigate('/');
  };

  if (loading) return <div className="p-xl text-body">Loading...</div>;
  if (error) return <div className="p-xl text-error">{error}</div>;
  if (!peer) return <div className="p-xl text-body">Peer not found</div>;

  const asnStr = peer.asn.toString();
  const localAsnStr = peer.localAsn.toString();

  return (
    <div className="space-y-lg animate-fade-in">
      {/* Header */}
      <div className="flex items-center gap-md">
        <button onClick={() => navigate('/')} className="btn-ghost">
          <ArrowLeft className="w-4 h-4" />
        </button>
        <div className="flex-1">
          <h1 className="text-display-md text-ink">{peer.name}</h1>
          {peer.description && (
            <p className="text-body-sm text-body">{peer.description}</p>
          )}
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={() => navigate(`/peers/${peer.id}/edit`)}
            className="btn-secondary-sm"
          >
            <Pencil className="w-3.5 h-3.5" />
            Edit
          </button>
          <button onClick={handleDelete} className="btn-secondary-sm text-error border-error/30 hover:bg-error-soft">
            <Trash2 className="w-3.5 h-3.5" />
            Delete
          </button>
        </div>
      </div>

      {/* Tabs */}
      <div className="flex items-center gap-1">
        {(['info', 'wg', 'bird'] as const).map((t) => (
          <button
            key={t}
            onClick={() => setTab(t)}
            className={tab === t ? 'tab-active' : 'tab-ghost'}
          >
            {t === 'info' ? 'Info' : t === 'wg' ? 'WireGuard Config' : 'BIRD Config'}
          </button>
        ))}
      </div>

      {/* Content */}
      {tab === 'info' && (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-lg">
          <Section title="Identity">
            <Field label="Name" value={peer.name} />
            <Field label="ASN" value={`AS${asnStr}`} mono />
            <Field label="Local ASN" value={`AS${localAsnStr}`} mono />
            <Field label="Enabled" value={peer.enabled ? 'Yes' : 'No'} />
          </Section>

          <Section title="WireGuard">
            <Field label="Public Key" value={peer.wgPublicKey || '—'} mono />
            <Field label="Remote Address" value={peer.wgRemoteAddress} mono />
            <Field label="Remote Port" value={String(peer.wgRemotePort)} mono />
            <Field label="Listen Port" value={String(peer.wgListenPort)} mono />
            <Field label="Interface" value={peer.wgInterfaceName} mono />
          </Section>

          <Section title="Tunnel Addressing">
            <Field label="IPv4 Local" value={peer.ipv4TunnelLocal || '—'} mono />
            <Field label="IPv4 Remote" value={peer.ipv4TunnelRemote || '—'} mono />
            <Field label="IPv6 Local" value={peer.ipv6TunnelLocal || '—'} mono />
            <Field label="IPv6 Remote" value={peer.ipv6TunnelRemote || '—'} mono />
          </Section>

          <Section title="BGP Session">
            <Field label="Multiprotocol" value={peer.multiprotocol ? 'Yes' : 'No'} />
            <Field label="Extended Nexthop" value={peer.extendedNexthop ? 'Yes' : 'No'} />
            <Field label="Sessions" value={peer.sessions === 0 ? 'IPv4' : peer.sessions === 1 ? 'IPv6' : 'Both'} />
            <Field label="Passive" value={peer.passive ? 'Yes' : 'No'} />
            <Field label="Max Prefix (import)" value={String(peer.importMaxPrefix || '—')} mono />
            <Field label="Max Prefix (export)" value={String(peer.exportMaxPrefix || '—')} mono />
          </Section>
        </div>
      )}

      {tab === 'wg' && (
        <div className="card">
          <ConfigViewer content={wg.content} loading={wg.loading} filename={`${peer.name}-wg.conf`} title="WireGuard Configuration" />
        </div>
      )}

      {tab === 'bird' && (
        <div className="card">
          <ConfigViewer content={bird.content} loading={bird.loading} filename={`${peer.name}-bird.conf`} title="BIRD2 Configuration" />
        </div>
      )}
    </div>
  );
}

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="card-soft">
      <h3 className="text-body-sm-strong text-ink mb-sm">{title}</h3>
      <dl className="space-y-xs">{children}</dl>
    </div>
  );
}

function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
  return (
    <div className="flex items-center justify-between py-xxs">
      <dt className="text-caption text-mute">{label}</dt>
      <dd className={`text-body-sm text-ink ${mono ? 'font-mono text-caption-mono' : ''}`}>
        {value}
      </dd>
    </div>
  );
}