peerman 0.2.2

DN42 peer manager with WireGuard, BIRD, and cluster support
import { useState, useEffect, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import { create } from '@bufbuild/protobuf';
import { SettingsSchema } from '../../lib/peerman_pb';
import { useSettings } from '../../hooks/useSettings';

const DEFAULT_FORM = {
  localAsn: '4242420000',
  birdTemplateName: 'dnpeers',
  birdRouterId: '172.20.0.1',
  wgDefaultListenPort: '42420',
  dn42Ipv4Prefix: '172.20.0.0/14',
  dn42Ipv6Prefix: 'fd00::/8',
  wgTable: 'off',
  wgMtu: '1420',
  wgFwmark: '0',
  wgPostUp: '',
  wgPostDown: '',
  roaMode: 'none',
  roaStaticV4Url: '',
  roaStaticV6Url: '',
  roaRtrAddress: '',
  roaRtrPort: '323',
  birdImportLimit: '9000',
  birdExportFilter: '',
  birdImportFilter: '',
  enableCommunityFilters: false,
  enableBfd: false,
  bfdIntervalMs: '300',
  bfdMultiplier: '3',
};

export default function SettingsPage() {
  const navigate = useNavigate();
  const { settings, loading, saveSettings } = useSettings();
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(false);
  const [form, setForm] = useState(DEFAULT_FORM);

  useEffect(() => {
    if (settings) {
      setForm({
        localAsn: settings.localAsn.toString(),
        birdTemplateName: settings.birdTemplateName,
        birdRouterId: settings.birdRouterId,
        wgDefaultListenPort: String(settings.wgDefaultListenPort),
        dn42Ipv4Prefix: settings.dn42Ipv4Prefix,
        dn42Ipv6Prefix: settings.dn42Ipv6Prefix,
        wgTable: settings.wgTable,
        wgMtu: String(settings.wgMtu),
        wgFwmark: String(settings.wgFwmark),
        wgPostUp: settings.wgPostUp,
        wgPostDown: settings.wgPostDown,
        roaMode: settings.roaMode || 'none',
        roaStaticV4Url: settings.roaStaticV4Url,
        roaStaticV6Url: settings.roaStaticV6Url,
        roaRtrAddress: settings.roaRtrAddress,
        roaRtrPort: String(settings.roaRtrPort || '323'),
        birdImportLimit: String(settings.birdImportLimit || '9000'),
        birdExportFilter: settings.birdExportFilter,
        birdImportFilter: settings.birdImportFilter,
        enableCommunityFilters: settings.enableCommunityFilters,
        enableBfd: settings.enableBfd,
        bfdIntervalMs: String(settings.bfdIntervalMs || '300'),
        bfdMultiplier: String(settings.bfdMultiplier || '3'),
      });
    }
  }, [settings]);

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setSaving(true);
    setError('');
    setSaved(false);

    const s = create(SettingsSchema, {
      localAsn: BigInt(form.localAsn || '0'),
      birdTemplateName: form.birdTemplateName,
      birdRouterId: form.birdRouterId,
      wgDefaultListenPort: Number(form.wgDefaultListenPort || '0'),
      dn42Ipv4Prefix: form.dn42Ipv4Prefix,
      dn42Ipv6Prefix: form.dn42Ipv6Prefix,
      wgTable: form.wgTable,
      wgMtu: Number(form.wgMtu || '0'),
      wgFwmark: Number(form.wgFwmark || '0'),
      wgPostUp: form.wgPostUp,
      wgPostDown: form.wgPostDown,
      roaMode: form.roaMode,
      roaStaticV4Url: form.roaStaticV4Url,
      roaStaticV6Url: form.roaStaticV6Url,
      roaRtrAddress: form.roaRtrAddress,
      roaRtrPort: Number(form.roaRtrPort || '0'),
      birdImportLimit: Number(form.birdImportLimit || '0'),
      birdExportFilter: form.birdExportFilter,
      birdImportFilter: form.birdImportFilter,
      enableCommunityFilters: form.enableCommunityFilters,
      enableBfd: form.enableBfd,
      bfdIntervalMs: Number(form.bfdIntervalMs || '300'),
      bfdMultiplier: Number(form.bfdMultiplier || '3'),
    });

    try {
      await saveSettings(s);
      setSaved(true);
      setTimeout(() => setSaved(false), 2000);
    } catch (e) {
      setError(String(e));
    } finally {
      setSaving(false);
    }
  };

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

  const f = <K extends keyof typeof form>(k: K) => form[k];

  return (
    <div className="max-w-2xl mx-auto animate-fade-in">
      <div className="flex items-center gap-md mb-lg">
        <button onClick={() => navigate(-1)} className="btn-ghost">
          <ArrowLeft className="w-4 h-4" />
        </button>
        <h1 className="text-display-md text-ink">Settings</h1>
      </div>

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

      <form onSubmit={handleSubmit} className="space-y-lg">
        {/* Global Configuration */}
        <div className="card space-y-md">
          <h2 className="text-body-sm-strong text-ink">Global Configuration</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
            <Input label="Local ASN" value={f('localAsn')} onChange={(v) => setForm((p) => ({ ...p, localAsn: v }))} />
            <Input label="BIRD Template Name" value={f('birdTemplateName')} onChange={(v) => setForm((p) => ({ ...p, birdTemplateName: v }))} />
            <Input label="BIRD Router ID" value={f('birdRouterId')} onChange={(v) => setForm((p) => ({ ...p, birdRouterId: v }))} />
            <Input label="Default WG Listen Port" value={f('wgDefaultListenPort')} onChange={(v) => setForm((p) => ({ ...p, wgDefaultListenPort: v }))} type="number" />
            <Input label="DN42 IPv4 Prefix" value={f('dn42Ipv4Prefix')} onChange={(v) => setForm((p) => ({ ...p, dn42Ipv4Prefix: v }))} />
            <Input label="DN42 IPv6 Prefix" value={f('dn42Ipv6Prefix')} onChange={(v) => setForm((p) => ({ ...p, dn42Ipv6Prefix: v }))} />
            <Input label="WG Table" value={f('wgTable')} onChange={(v) => setForm((p) => ({ ...p, wgTable: v }))} />
          </div>
        </div>

        {/* WireGuard Advanced */}
        <div className="card space-y-md">
          <h2 className="text-body-sm-strong text-ink">WireGuard Advanced</h2>
          <p className="text-body-sm text-mute">Tunnel-level settings applied to each peer config.</p>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
            <Input label="MTU" value={f('wgMtu')} onChange={(v) => setForm((p) => ({ ...p, wgMtu: v }))} type="number" />
            <Input label="FwMark (0 = disabled)" value={f('wgFwmark')} onChange={(v) => setForm((p) => ({ ...p, wgFwmark: v }))} type="number" />
          </div>
          <div className="grid grid-cols-1 gap-sm">
            <Textarea label="PostUp Script" value={f('wgPostUp')} onChange={(v) => setForm((p) => ({ ...p, wgPostUp: v }))} placeholder="Additional commands after interface up" />
            <Textarea label="PostDown Script" value={f('wgPostDown')} onChange={(v) => setForm((p) => ({ ...p, wgPostDown: v }))} placeholder="Additional commands before interface down" />
          </div>
        </div>

        {/* ROA/RPKI */}
        <div className="card space-y-md">
          <h2 className="text-body-sm-strong text-ink">ROA / RPKI Filtering</h2>
          <p className="text-body-sm text-mute">Reject routes with invalid or unknown ROA status.</p>
          <div className="flex items-center gap-sm">
            {(['none', 'static_file', 'rtr'] as const).map((mode) => (
              <button
                key={mode}
                type="button"
                onClick={() => setForm((p) => ({ ...p, roaMode: mode }))}
                className={f('roaMode') === mode ? 'tab-active' : 'tab-ghost'}
              >
                {mode === 'none' ? 'None' : mode === 'static_file' ? 'Static File' : 'RTR'}
              </button>
            ))}
          </div>
          {f('roaMode') === 'static_file' && (
            <div className="grid grid-cols-1 gap-sm">
              <Input label="ROA v4 URL" value={f('roaStaticV4Url')} onChange={(v) => setForm((p) => ({ ...p, roaStaticV4Url: v }))} placeholder="https://dn42.burble.com/roa/dn42_roa_bird2_4.conf" />
              <Input label="ROA v6 URL" value={f('roaStaticV6Url')} onChange={(v) => setForm((p) => ({ ...p, roaStaticV6Url: v }))} placeholder="https://dn42.burble.com/roa/dn42_roa_bird2_6.conf" />
            </div>
          )}
          {f('roaMode') === 'rtr' && (
            <div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
              <Input label="RTR Address" value={f('roaRtrAddress')} onChange={(v) => setForm((p) => ({ ...p, roaRtrAddress: v }))} placeholder="rpki.akae.re" />
              <Input label="RTR Port" value={f('roaRtrPort')} onChange={(v) => setForm((p) => ({ ...p, roaRtrPort: v }))} type="number" />
            </div>
          )}
        </div>

        {/* BIRD Filter */}
        <div className="card space-y-md">
          <h2 className="text-body-sm-strong text-ink">BIRD Filter Templates</h2>
          <p className="text-body-sm text-mute">Custom filter bodies override auto-generated defaults. Leave empty for DN42 best-practice defaults.</p>
          <div className="grid grid-cols-1 gap-sm">
            <Input label="Import Prefix Limit" value={f('birdImportLimit')} onChange={(v) => setForm((p) => ({ ...p, birdImportLimit: v }))} type="number" />
          </div>
          <Toggle
            label="Enable Community Filters"
            description="Include DN42 standard AS 64511 community functions (latency, bandwidth, crypto) in BIRD config."
            checked={form.enableCommunityFilters}
            onChange={(v) => setForm((p) => ({ ...p, enableCommunityFilters: v }))}
          />
          <div className="grid grid-cols-1 gap-sm">
            <Textarea label="Import Filter Body" value={f('birdImportFilter')} onChange={(v) => setForm((p) => ({ ...p, birdImportFilter: v }))} placeholder="if is_valid_network() && !is_self_net() then { ... }" code />
            <Textarea label="Export Filter Body" value={f('birdExportFilter')} onChange={(v) => setForm((p) => ({ ...p, birdExportFilter: v }))} placeholder="if is_valid_network() && source ~ [RTS_STATIC, RTS_BGP] then accept; else reject;" code />
          </div>
        </div>

        {/* BFD */}
        <div className="card space-y-md">
          <h2 className="text-body-sm-strong text-ink">BFD (Bidirectional Forwarding Detection)</h2>
          <p className="text-body-sm text-mute">Enable fast failure detection for BGP sessions over WireGuard tunnels.</p>
          <Toggle
            label="Enable BFD"
            description="Add a protocol bfd block monitoring all WireGuard interfaces for rapid link failure detection."
            checked={form.enableBfd}
            onChange={(v) => setForm((p) => ({ ...p, enableBfd: v }))}
          />
          {form.enableBfd && (
            <div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
              <Input label="BFD Interval (ms)" value={f('bfdIntervalMs')} onChange={(v) => setForm((p) => ({ ...p, bfdIntervalMs: v }))} type="number" placeholder="300" />
              <Input label="BFD Multiplier" value={f('bfdMultiplier')} onChange={(v) => setForm((p) => ({ ...p, bfdMultiplier: v }))} type="number" placeholder="3" />
            </div>
          )}
        </div>

        <button type="submit" disabled={saving} className="btn-primary">
          {saving ? 'Saving...' : 'Save Settings'}
        </button>
      </form>
    </div>
  );
}

function Input({
  label, value, onChange, type = 'text', placeholder,
}: {
  label: string; value: string; onChange: (v: string) => void; type?: string; placeholder?: string;
}) {
  return (
    <div className="flex flex-col gap-1">
      <label className="text-caption text-mute">{label}</label>
      <input
        type={type}
        value={value}
        placeholder={placeholder}
        onChange={(e) => onChange(e.target.value)}
        className="form-input"
      />
    </div>
  );
}

function Textarea({
  label, value, onChange, placeholder, code,
}: {
  label: string; value: string; onChange: (v: string) => void; placeholder?: string; code?: boolean;
}) {
  return (
    <div className="flex flex-col gap-1">
      <label className="text-caption text-mute">{label}</label>
      <textarea
        value={value}
        placeholder={placeholder}
        onChange={(e) => onChange(e.target.value)}
        rows={3}
        className={code ? 'form-input font-mono' : 'form-input'}
        style={code ? { fontFamily: 'Geist Mono, ui-monospace, monospace', fontSize: '13px' } : undefined}
      />
    </div>
  );
}

function Toggle({
  label, description, checked, onChange,
}: {
  label: string; description?: string; checked: boolean; onChange: (v: boolean) => void;
}) {
  return (
    <div className="flex items-start gap-sm">
      <button
        type="button"
        role="switch"
        aria-checked={checked}
        onClick={() => onChange(!checked)}
        className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${checked ? 'bg-cyan' : 'bg-surface-3'}`}
      >
        <span
          className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out ${checked ? 'translate-x-4' : 'translate-x-0'}`}
        />
      </button>
      <div className="flex flex-col gap-0.5">
        <span className="text-body-sm-strong text-ink">{label}</span>
        {description && <span className="text-caption text-mute">{description}</span>}
      </div>
    </div>
  );
}