peerman 0.2.1

DN42 peer manager with WireGuard, BIRD, and cluster support
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { create } from '@bufbuild/protobuf';
import { RegisterNodeRequestSchema, UpdateNodeRequestSchema } from '../../lib/peerman_pb';
import { useNode, useNodes } from '../../hooks/useNodes';
import { clusterClient } from '../../lib/grpc';

export default function NodeForm() {
  const { id } = useParams<{ id: string }>();
  const { node } = useNode(id);
  const { refetch } = useNodes();
  const navigate = useNavigate();
  const isEdit = Boolean(id && node);

  const [name, setName] = useState(node?.name ?? '');
  const [listenAddr, setListenAddr] = useState(node?.listenAddr ?? '');
  const [localAsn, setLocalAsn] = useState(String(node?.localAsn ?? ''));
  const [description, setDescription] = useState(node?.description ?? '');
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setSaving(true);
    try {
      if (isEdit) {
        await clusterClient.updateNode(create(UpdateNodeRequestSchema, {
          id: id!,
          name,
          listenAddr,
          localAsn: BigInt(localAsn || '0'),
          description,
        }));
      } else {
        await clusterClient.registerNode(create(RegisterNodeRequestSchema, {
          name,
          listenAddr,
          localAsn: BigInt(localAsn || '0'),
          description,
        }));
      }
      refetch();
      navigate('/nodes');
    } catch (e) {
      setError(String(e));
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="max-w-2xl space-y-lg animate-fade-in">
      <h1 className="text-display-md text-ink">{isEdit ? 'Edit Node' : 'Register Node'}</h1>

      {error && (
        <div className="bg-error-soft text-error-deep text-body-sm rounded-sm px-md py-sm">{error}</div>
      )}

      <form onSubmit={handleSubmit} className="card space-y-lg">
        <fieldset className="space-y-md">
          <legend className="text-body-md-strong text-ink mb-sm">Identity</legend>
          <div>
            <label className="block text-body-sm text-body mb-xxs">Name</label>
            <input className="form-input w-full" value={name} onChange={e => setName(e.target.value)}
              placeholder="alpha-dn42" required />
          </div>
          <div>
            <label className="block text-body-sm text-body mb-xxs">Listen Address</label>
            <input className="form-input w-full" value={listenAddr} onChange={e => setListenAddr(e.target.value)}
              placeholder="172.20.1.1:3000" required />
          </div>
          <div>
            <label className="block text-body-sm text-body mb-xxs">Local ASN</label>
            <input className="form-input w-full" value={localAsn} onChange={e => setLocalAsn(e.target.value)}
              placeholder="4242420001" />
          </div>
          <div>
            <label className="block text-body-sm text-body mb-xxs">Description</label>
            <input className="form-input w-full" value={description} onChange={e => setDescription(e.target.value)}
              placeholder="Optional description" />
          </div>
        </fieldset>

        <div className="flex items-center gap-sm">
          <button type="submit" disabled={saving} className="btn-primary">
            {saving ? 'Saving...' : isEdit ? 'Update Node' : 'Register Node'}
          </button>
          <button type="button" onClick={() => navigate('/nodes')} className="btn-secondary">
            Cancel
          </button>
        </div>
      </form>
    </div>
  );
}