From a91f7b5df1424b2fc915fd27035d18d9346f8670 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:41:11 +0200 Subject: [PATCH] feat(web): implement expandable peer details with stats, rotation, and actions --- .../FederationPanel.tsx | 167 +++++++++++++++++- 1 file changed, 162 insertions(+), 5 deletions(-) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx index 622d1b65..181a9127 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx @@ -361,15 +361,45 @@ function PeerRow({ peer, view, expanded, onToggleExpand, onAction, defaultAutoRo 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 */} +
@@ -393,6 +423,133 @@ function PeerRow({ peer, view, expanded, onToggleExpand, onAction, defaultAutoRo {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 && ( + + )} +
+ + )} +
+
+ )}
); }