From e372a7a57170b8c2fb5f77e49161d45a02bf718d Mon Sep 17 00:00:00 2001
From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com>
Date: Fri, 26 Jun 2026 13:58:38 +0200
Subject: [PATCH] feat(web): Check now button to manually recover unreachable
federation peers
---
packages/web/src/api/client.ts | 3 ++
.../FederationPanel.recheck.test.tsx | 48 +++++++++++++++++++
.../FederationPanel.tsx | 37 +++++++++++++-
3 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.recheck.test.tsx
diff --git a/packages/web/src/api/client.ts b/packages/web/src/api/client.ts
index acb47ba6..8445bb75 100644
--- a/packages/web/src/api/client.ts
+++ b/packages/web/src/api/client.ts
@@ -277,6 +277,7 @@ export class BackspaceApiClient {
peers: () => Promise<{ peers: FederationPeer[] }>;
revokePeer: (id: string) => Promise<{ success: boolean }>;
resetPeer: (id: string) => Promise<{ success: boolean }>;
+ recheckPeer: (id: string) => Promise<{ recovered: boolean; status: string }>;
rotatePeerSecret: (id: string) => Promise<{ success: boolean; gracePeriodMs: number }>;
updatePeer: (id: string, data: { autoRotateIntervalDays: number }) => Promise<{ peer: FederationPeer }>;
deletePeerPermanently: (id: string) => Promise<{ success: boolean }>;
@@ -705,6 +706,8 @@ export class BackspaceApiClient {
request<{ success: boolean }>('DELETE', `/federation/peers/${id}`),
resetPeer: (id: string) =>
request<{ success: boolean }>('POST', `/federation/peers/${id}/reset`),
+ recheckPeer: (id: string) =>
+ request<{ recovered: boolean; status: string }>('POST', `/federation/peers/${id}/recheck`),
rotatePeerSecret: (id: string) =>
request<{ success: boolean; gracePeriodMs: number }>('POST', `/federation/peers/${id}/rotate`),
updatePeer: (id: string, data: { autoRotateIntervalDays: number }) =>
diff --git a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.recheck.test.tsx b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.recheck.test.tsx
new file mode 100644
index 00000000..5f2c0f46
--- /dev/null
+++ b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.recheck.test.tsx
@@ -0,0 +1,48 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+
+const { recheckPeer, addToast } = vi.hoisted(() => ({
+ recheckPeer: vi.fn(),
+ addToast: vi.fn(),
+}));
+
+vi.mock('../../../api/client', () => ({
+ api: {
+ federation: {
+ peers: vi.fn().mockResolvedValue({ peers: [{
+ id: 'p1', origin: 'https://peer.example', instanceName: 'Peer',
+ status: 'unreachable', consecutiveFailures: 10, lastSeenAt: Date.now(),
+ autoRotateIntervalDays: 90, rotationInProgress: false,
+ }] }),
+ approvalRequests: vi.fn().mockResolvedValue({ requests: [] }),
+ recheckPeer,
+ },
+ },
+}));
+
+vi.mock('../../../stores/uiStore', () => ({
+ useUIStore: (sel: (s: { addToast: typeof addToast }) => unknown) => sel({ addToast }),
+}));
+
+vi.mock('../../../hooks/useWebSocket', () => ({
+ onFederationPeersChanged: () => () => {},
+}));
+
+import { FederationPanel } from './FederationPanel';
+
+describe('FederationPanel — Check now', () => {
+ beforeEach(() => { recheckPeer.mockReset(); addToast.mockReset(); });
+
+ it('shows Check now for an unreachable peer and recovers it on click', async () => {
+ recheckPeer.mockResolvedValue({ recovered: true, status: 'active' });
+ render(