peerman 0.2.5

DN42 peer manager with WireGuard, BIRD, and cluster support
import { useNavigate } from 'react-router-dom';
import { Pencil, Trash2, Eye, Cable } from 'lucide-react';
import { usePeers } from '../../hooks/usePeers';
import { useNodes } from '../../hooks/useNodes';
import { peerClient } from '../../lib/grpc';

export default function PeerTable() {
  const { peers, loading, error, refetch } = usePeers();
  const { nodes } = useNodes();
  const navigate = useNavigate();

  const offlineNodeIds = new Set(nodes.filter(n => !n.online).map(n => n.id));
  const nodeNameById = new Map(nodes.map(n => [n.id, n.name]));

  const nodeName = (id: string) => nodeNameById.get(id) ?? id.slice(0, 8);

  const handleToggle = async (id: string) => {
    await peerClient.togglePeer({ id });
    refetch();
  };

  const handleDelete = async (id: string, name: string) => {
    if (!confirm(`Delete peer "${name}"?`)) return;
    await peerClient.deletePeer({ id });
    refetch();
  };

  if (loading) {
    return <div className="p-xl text-body text-center">Loading peers...</div>;
  }

  if (error) {
    return <div className="p-xl text-error text-center">{error}</div>;
  }

  if (peers.length === 0) {
    return (
      <div className="card-soft text-center py-4xl px-xl">
        <Cable className="w-12 h-12 mx-auto mb-md text-mute" />
        <h2 className="text-display-sm text-ink mb-sm">No peers yet</h2>
        <p className="text-body-sm text-body mb-lg">
          Add your first DN42 peer to start managing WireGuard tunnels and BGP sessions.
        </p>
        <button className="btn-primary" onClick={() => navigate('/peers/new')}>
          Add Peer
        </button>
      </div>
    );
  }

  return (
    <div className="card overflow-hidden">
      <div className="flex items-center justify-between px-lg py-md border-b border-hairline">
        <h2 className="text-display-sm text-ink">Peers</h2>
        <button className="btn-primary-sm" onClick={() => navigate('/peers/new')}>
          Add Peer
        </button>
      </div>
      <table className="data-table">
        <thead>
          <tr>
            <th>Name</th>
            <th>ASN</th>
            <th>Endpoint</th>
            <th>Tunnel</th>
            <th>Sessions</th>
            <th>Origin Node</th>
            <th>Status</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {peers.map((peer) => {
            const isStale = peer.originNodeId && offlineNodeIds.has(peer.originNodeId);
            return (
            <tr
              key={peer.id}
              className={isStale ? 'opacity-50' : ''}
              title={isStale ? 'Node offline; data from cache' : undefined}
            >
              <td>
                <button
                  className="text-link hover:text-link-deep font-medium"
                  onClick={() => navigate(`/peers/${peer.id}`)}
                >
                  {peer.name}
                </button>
              </td>
              <td className="text-caption-mono">AS{peer.asn.toString()}</td>
              <td className="text-caption-mono">
                {peer.wgRemoteAddress}:{peer.wgRemotePort || '—'}
              </td>
              <td className="text-caption-mono">
                {peer.ipv6TunnelLocal || peer.ipv4TunnelLocal || '—'}
              </td>
              <td>
                <span className="badge">
                  {peer.sessions === 0 ? 'IPv4' : peer.sessions === 1 ? 'IPv6' : 'Both'}
                  {peer.multiprotocol ? ' MP' : ''}
                </span>
              </td>
              <td className="text-caption text-mute">
                {peer.originNodeId ? nodeName(peer.originNodeId) : 'local'}
              </td>
              <td>
                <button
                  onClick={() => handleToggle(peer.id)}
                  className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
                    peer.enabled ? 'bg-primary' : 'bg-hairline-strong'
                  }`}
                >
                  <span
                    className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${
                      peer.enabled ? 'translate-x-[18px]' : 'translate-x-[3px]'
                    }`}
                  />
                </button>
              </td>
              <td>
                <div className="flex items-center gap-1">
                  <button
                    onClick={() => navigate(`/peers/${peer.id}`)}
                    className="p-1 rounded-xs hover:bg-canvas-soft text-body hover:text-ink"
                    title="View"
                  >
                    <Eye className="w-3.5 h-3.5" />
                  </button>
                  <button
                    onClick={() => navigate(`/peers/${peer.id}/edit`)}
                    className="p-1 rounded-xs hover:bg-canvas-soft text-body hover:text-ink"
                    title="Edit"
                  >
                    <Pencil className="w-3.5 h-3.5" />
                  </button>
                  <button
                    onClick={() => handleDelete(peer.id, peer.name)}
                    className="p-1 rounded-xs hover:bg-error-soft text-body hover:text-error"
                    title="Delete"
                  >
                    <Trash2 className="w-3.5 h-3.5" />
                  </button>
                </div>
              </td>
            </tr>
          );
          })}
        </tbody>
      </table>
    </div>
  );
}