import { useState, useEffect, useCallback } from 'react'; import { useSettingsStore } from '../../../stores/settingsStore'; import { useUIStore } from '../../../stores/uiStore'; import { Toggle } from '../../ui/Toggle'; import { ConfirmDialog } from '../../ui/ConfirmDialog'; import { api } from '../../../api/client'; import type { InstanceAdminSettings } from '@backspace/shared'; import type { FederationPeer } from '../../../api/client'; // ─── Global Settings ───────────────────────────────────────────────────────── function FederationGlobalSettings() { const instanceSettings = useSettingsStore((s) => s.instanceSettings); const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings); const addToast = useUIStore((s) => s.addToast); const [draft, setDraft] = useState | null>(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); useEffect(() => { if (instanceSettings) { setDraft({ federationRelayEnabled: instanceSettings.federationRelayEnabled, federationRelayTtlDays: instanceSettings.federationRelayTtlDays, defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays, autoAcceptPeering: instanceSettings.autoAcceptPeering, }); } }, [instanceSettings]); if (!draft) return null; const hasChanges = instanceSettings ? draft.federationRelayEnabled !== instanceSettings.federationRelayEnabled || draft.federationRelayTtlDays !== instanceSettings.federationRelayTtlDays || draft.defaultAutoRotateIntervalDays !== instanceSettings.defaultAutoRotateIntervalDays || draft.autoAcceptPeering !== instanceSettings.autoAcceptPeering : false; const handleSave = async () => { setSaving(true); setSaveError(''); try { await updateInstanceSettings(draft); addToast('Settings saved', 'success', 2000); } catch (err) { setSaveError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; const handleReset = () => { if (instanceSettings) { setDraft({ federationRelayEnabled: instanceSettings.federationRelayEnabled, federationRelayTtlDays: instanceSettings.federationRelayTtlDays, defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays, autoAcceptPeering: instanceSettings.autoAcceptPeering, }); } setSaveError(''); }; return (
Relay Settings

Control DM relay between federated instances. When enabled, DMs with users on peer instances are relayed server-to-server.

Relay TTL (days)
How long relayed messages are retained in the outbox before cleanup
{ const val = parseInt(e.target.value, 10); if (!isNaN(val) && val >= 1 && val <= 365) { setDraft({ ...draft, federationRelayTtlDays: val }); } }} className="input-standard w-24" />
Default Secret Rotation (days)
Auto-rotation interval for new peers. Existing peers keep their current setting.
{ const val = parseInt(e.target.value, 10); if (!isNaN(val) && val >= 1 && val <= 365) { setDraft({ ...draft, defaultAutoRotateIntervalDays: val }); } }} className="input-standard w-24" />
{saveError && (
{saveError}
)} {hasChanges && (
)}
); } // ─── Helpers ───────────────────────────────────────────────────────────────── function formatRelativeTime(timestamp: number | null): string { if (!timestamp) return 'Never'; const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'Just now'; 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`; } function formatAbsoluteDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function peerStatusColor(status: string): string { switch (status) { case 'active': return 'bg-status-online/15 text-status-online'; case 'pending': return 'bg-accent-lavender/15 text-accent-lavender'; case 'unreachable': return 'bg-accent-amber/15 text-accent-amber'; case 'rejected': return 'bg-accent-rose/15 text-accent-rose'; case 'revoked': return 'bg-white/5 text-txt-tertiary'; default: return 'bg-white/5 text-txt-tertiary'; } } function peerStatusDotColor(status: string): string { switch (status) { case 'active': return 'bg-status-online'; case 'pending': return 'bg-accent-lavender'; case 'unreachable': return 'bg-accent-amber'; case 'rejected': return 'bg-accent-rose'; default: return 'bg-txt-tertiary'; } } function peerStatusLabel(status: string): string { switch (status) { case 'active': return 'Active'; case 'pending': return 'Pending'; case 'unreachable': return 'Unreachable'; case 'rejected': return 'Rejected (auto-peering denied)'; case 'revoked': return 'Revoked'; default: return status; } } type PeerView = 'active' | 'revoked'; type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures'; type StatusFilter = 'active' | 'unreachable' | 'pending'; // ─── Filter Dropdown ───────────────────────────────────────────────────────── function FilterDropdown({ view, statusFilter, setStatusFilter, sortBy, setSortBy, }: { view: PeerView; statusFilter: Set; setStatusFilter: (f: Set) => void; sortBy: SortBy; setSortBy: (s: SortBy) => void; }) { const [open, setOpen] = useState(false); const toggleStatus = (s: StatusFilter) => { const next = new Set(statusFilter); if (next.has(s)) { if (next.size > 1) next.delete(s); } else { next.add(s); } setStatusFilter(next); }; const sortOptions: Array<{ key: SortBy; label: string }> = view === 'active' ? [ { key: 'name', label: 'Name (A-Z)' }, { key: 'lastSeen', label: 'Last seen' }, { key: 'dateAdded', label: 'Date added' }, { key: 'failures', label: 'Failures' }, ] : [ { key: 'name', label: 'Name (A-Z)' }, { key: 'dateAdded', label: 'Revoked date' }, ]; return (
{open && ( <>
setOpen(false)} />
{view === 'active' && ( <>
Status
{(['active', 'unreachable', 'pending'] as StatusFilter[]).map((s) => ( ))}
)}
Sort by
{sortOptions.map((opt) => ( ))}
)}
); } // ─── Peer List Controls ────────────────────────────────────────────────────── function PeerListControls({ view, setView, activeCount, revokedCount, statusFilter, setStatusFilter, sortBy, setSortBy, }: { view: PeerView; setView: (v: PeerView) => void; activeCount: number; revokedCount: number; statusFilter: Set; setStatusFilter: (f: Set) => void; sortBy: SortBy; setSortBy: (s: SortBy) => void; }) { return (
); } // ─── Sorting ───────────────────────────────────────────────────────────────── function sortPeers(peers: FederationPeer[], sortBy: SortBy, view: PeerView): FederationPeer[] { return [...peers].sort((a, b) => { switch (sortBy) { case 'name': { const nameA = (a.instanceName || new URL(a.origin).host).toLowerCase(); const nameB = (b.instanceName || new URL(b.origin).host).toLowerCase(); return nameA.localeCompare(nameB); } case 'lastSeen': return (b.lastSeenAt ?? 0) - (a.lastSeenAt ?? 0); case 'dateAdded': return b.createdAt - a.createdAt; case 'failures': return (b.consecutiveFailures ?? 0) - (a.consecutiveFailures ?? 0); default: return 0; } }); } // ─── Peer Row ──────────────────────────────────────────────────────────────── function PeerRow({ peer, view, expanded, onToggleExpand, onAction, defaultAutoRotateIntervalDays }: { peer: FederationPeer; view: PeerView; expanded: boolean; onToggleExpand: () => void; onAction: (type: 'rotate' | 'revoke' | 'reinitiate' | 'delete') => void; defaultAutoRotateIntervalDays: number; }) { const [editingInterval, setEditingInterval] = useState(false); const [intervalDraft, setIntervalDraft] = useState(peer.autoRotateIntervalDays); const [intervalSaving, setIntervalSaving] = useState(false); const [intervalError, setIntervalError] = useState(''); const addToast = useUIStore((s) => s.addToast); const name = peer.instanceName || new URL(peer.origin).host; const isRevoked = view === 'revoked'; const isDefault = peer.autoRotateIntervalDays === defaultAutoRotateIntervalDays; const handleSaveInterval = async () => { if (intervalDraft < 1 || intervalDraft > 365) { setIntervalError('Must be 1-365'); return; } setIntervalSaving(true); setIntervalError(''); try { const result = await api.federation.updatePeer(peer.id, { autoRotateIntervalDays: intervalDraft }); // Update peer in parent state via a re-fetch would be cleanest, // but for responsiveness we update the peer object directly. // This works because React re-renders from the parent's setPeers. peer.autoRotateIntervalDays = result.peer.autoRotateIntervalDays; setEditingInterval(false); addToast('Rotation interval updated', 'success', 2000); } catch (err) { setIntervalError(err instanceof Error ? err.message : 'Failed to update'); } finally { setIntervalSaving(false); } }; return (
{/* Compact row */}
{name}
{isRevoked ? `Revoked: ${formatAbsoluteDate(peer.lastSeenAt ?? peer.createdAt)} · Peered: ${formatAbsoluteDate(peer.createdAt)}` : peer.status === 'unreachable' ? `Last seen: ${formatRelativeTime(peer.lastSeenAt)} · ${peer.consecutiveFailures ?? 0} failures` : `Last seen: ${formatRelativeTime(peer.lastSeenAt)} · Synced: ${formatRelativeTime(peer.lastSyncedAt)}` }
{peerStatusLabel(peer.status)} {expanded ? '▾' : '▸'}
{/* Expanded details */} {expanded && (
{isRevoked ? ( /* Revoked peer actions */
) : ( <> {/* Stats grid */}
Consecutive Failures
0 ? 'text-accent-amber font-medium' : 'text-txt-secondary'}`}> {peer.consecutiveFailures ?? 0}
Last Failure
{formatRelativeTime(peer.lastFailureAt)}
Peered Since
{formatAbsoluteDate(peer.createdAt)}
Secret Rotated
{formatRelativeTime(peer.secretRotatedAt)}
Auto-Rotate
{editingInterval ? (
e.stopPropagation()}> setIntervalDraft(parseInt(e.target.value, 10) || 0)} className="input-standard w-16 py-0.5 text-xs" disabled={intervalSaving} autoFocus />
) : (
Every {peer.autoRotateIntervalDays}d {isDefault && (default)}
)} {intervalError &&
{intervalError}
}
Rotation Status
{peer.rotationInProgress ? 'In progress' : 'Idle'}
{/* Actions */}
{!editingInterval && ( )}
)}
)}
); } // ─── Main Panel ────────────────────────────────────────────────────────────── export function FederationPanel() { const addToast = useUIStore((s) => s.addToast); // Peer list state const [peers, setPeers] = useState([]); const [peersLoading, setPeersLoading] = useState(false); const [peersError, setPeersError] = useState(''); const [view, setView] = useState('active'); const [statusFilter, setStatusFilter] = useState>(new Set(['active', 'unreachable', 'pending'])); const [sortBy, setSortBy] = useState('name'); const [expandedPeerId, setExpandedPeerId] = useState(null); // Confirm dialog state (used in Task 10) const [confirmAction, setConfirmAction] = useState<{ type: 'rotate' | 'revoke' | 'reinitiate' | 'delete'; peer: FederationPeer; } | null>(null); const [actionLoading, setActionLoading] = useState(false); const fetchPeers = useCallback(async () => { setPeersLoading(true); setPeersError(''); try { const result = await api.federation.peers(); setPeers(result.peers); } catch (err) { setPeersError(err instanceof Error ? err.message : 'Failed to load peers'); } finally { setPeersLoading(false); } }, []); useEffect(() => { fetchPeers(); }, [fetchPeers]); // Derived peer lists const activePeers = peers.filter((p) => p.status !== 'revoked'); const revokedPeers = peers.filter((p) => p.status === 'revoked'); const filteredPeers = view === 'active' ? activePeers.filter((p) => statusFilter.has(p.status as StatusFilter)) : revokedPeers; const sortedPeers = sortPeers(filteredPeers, sortBy, view); // Empty state message const emptyMessage = peers.length === 0 ? 'No federation peers configured. Peers are created automatically when users connect to remote instances.' : view === 'active' && filteredPeers.length === 0 ? 'No peers match the current filter.' : view === 'revoked' && revokedPeers.length === 0 ? 'No revoked peers.' : null; const handleConfirmAction = async () => { if (!confirmAction) return; const { type, peer } = confirmAction; setActionLoading(true); try { switch (type) { case 'rotate': { await api.federation.rotatePeerSecret(peer.id); setPeers((prev) => prev.map((p) => p.id === peer.id ? { ...p, rotationInProgress: true } : p )); addToast('Secret rotation initiated — 15 minute grace period', 'success', 3000); break; } case 'revoke': { await api.federation.revokePeer(peer.id); setPeers((prev) => prev.map((p) => p.id === peer.id ? { ...p, status: 'revoked' } : p )); addToast('Peer revoked', 'success', 2000); break; } case 'reinitiate': { const origin = peer.origin; await api.federation.deletePeerPermanently(peer.id); setPeers((prev) => prev.filter((p) => p.id !== peer.id)); try { const result = await api.federation.initiatePeering({ remoteOrigin: origin }); setPeers((prev) => [...prev, result.peer]); addToast('Peering re-initiated', 'success', 2000); } catch (err) { addToast( `Peer record deleted but handshake failed: ${(err as Error).message}. Re-peer manually with ${origin}`, 'warning', 5000, ); } break; } case 'delete': { await api.federation.deletePeerPermanently(peer.id); setPeers((prev) => prev.filter((p) => p.id !== peer.id)); addToast('Peer permanently deleted', 'success', 2000); break; } } } catch (err) { addToast(err instanceof Error ? err.message : 'Action failed', 'warning', 3000); } finally { setActionLoading(false); setConfirmAction(null); } }; const confirmDialogProps = confirmAction ? (() => { const name = confirmAction.peer.instanceName || new URL(confirmAction.peer.origin).host; switch (confirmAction.type) { case 'rotate': return { title: 'Rotate HMAC Secret', description: `This will generate a new HMAC secret for ${name}. Both instances will accept old and new secrets during a 15-minute grace period.`, confirmLabel: 'Rotate', variant: 'warning' as const, }; case 'revoke': return { title: 'Revoke Peer', description: `This will stop all federation relay traffic with ${name}. Pending outbox entries will be purged. You can re-initiate peering later.`, confirmLabel: 'Revoke', variant: 'danger' as const, }; case 'reinitiate': return { title: 'Re-initiate Peering', description: `This will delete the revoked record and start a fresh handshake with ${confirmAction.peer.origin}. The remote instance must be reachable.`, confirmLabel: 'Re-initiate', variant: 'warning' as const, }; case 'delete': return { title: 'Delete Peer Record', description: `This will permanently delete the peer record for ${name}. This cannot be undone.`, confirmLabel: 'Delete', variant: 'danger' as const, }; } })() : null; return (
e.preventDefault()}>

Federation

Configure federation relay, secret rotation, and manage peered instances.
{/* Peered Instances */}
Peered Instances
{peers.length > 0 && ( )} {peersError && (
{peersError}
)} {sortedPeers.length > 0 && (
{sortedPeers.map((peer) => ( setExpandedPeerId(expandedPeerId === peer.id ? null : peer.id)} onAction={(type) => setConfirmAction({ type, peer })} defaultAutoRotateIntervalDays={useSettingsStore.getState().instanceSettings?.defaultAutoRotateIntervalDays ?? 90} /> ))}
)} {emptyMessage && !peersError && !peersLoading && (
{emptyMessage}
)}
{confirmAction && confirmDialogProps && ( { if (!actionLoading) setConfirmAction(null); }} onConfirm={handleConfirmAction} title={confirmDialogProps.title} description={confirmDialogProps.description} confirmLabel={confirmDialogProps.confirmLabel} variant={confirmDialogProps.variant} loading={actionLoading} /> )} ); }