feat: real-time Federation panel updates via WS events

Added federation_peers_changed (no-payload signal) broadcast from every
peer state mutation, and federation_approval_request_received when a new
approval request is queued. Client subscribes via onFederationPeersChanged
callback registry. FederationPanel and PendingApprovals debounce-refetch
on any event. sendToAdmins helper broadcasts only to admin users.
This commit is contained in:
Jannis Braun
2026-04-20 18:28:10 +02:00
parent 6afad97bd1
commit 3d8709d20a
7 changed files with 95 additions and 0 deletions
@@ -4,6 +4,7 @@ import { useUIStore } from '../../../stores/uiStore';
import { Toggle } from '../../ui/Toggle';
import { ConfirmDialog } from '../../ui/ConfirmDialog';
import { api } from '../../../api/client';
import { onFederationPeersChanged } from '../../../hooks/useWebSocket';
import type { InstanceAdminSettings } from '@backspace/shared';
import type { FederationPeer, ApprovalRequest } from '../../../api/client';
@@ -611,6 +612,18 @@ function PendingApprovals({ onCountChange }: { onCountChange?: (count: number) =
fetchRequests();
}, [fetchRequests]);
// Real-time updates: re-fetch approval requests on federation changes
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
const unsub = onFederationPeersChanged(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fetchRequests();
}, 500);
});
return () => { unsub(); clearTimeout(timeout); };
}, [fetchRequests]);
const handleConfirm = async () => {
if (!confirmAction) return;
const { type, request: req } = confirmAction;
@@ -765,6 +778,18 @@ export function FederationPanel({ onApprovalCountChange }: { onApprovalCountChan
fetchPeers();
}, [fetchPeers]);
// Real-time updates: re-fetch peers and approval requests on any federation change
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
const unsub = onFederationPeersChanged(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fetchPeers();
}, 500);
});
return () => { unsub(); clearTimeout(timeout); };
}, [fetchPeers]);
// Derived peer lists
const activePeers = peers.filter((p) => p.status !== 'revoked');
const revokedPeers = peers.filter((p) => p.status === 'revoked');
+24
View File
@@ -36,6 +36,18 @@ export function getActivePeerOrigins(): Set<string> {
return activePeerOrigins;
}
// ─── Federation change listeners (for real-time panel updates) ───────────────
const federationChangeListeners = new Set<() => void>();
export function onFederationPeersChanged(cb: () => void): () => void {
federationChangeListeners.add(cb);
return () => { federationChangeListeners.delete(cb); };
}
function notifyFederationChangeListeners(): void {
for (const cb of federationChangeListeners) cb();
}
// ─── Connection state ─────────────────────────────────────────────────────────
interface ConnectionState {
@@ -717,6 +729,7 @@ function handleEvent(origin: string, event: ServerEvent): void {
'warning',
10000,
);
notifyFederationChangeListeners();
break;
}
@@ -724,6 +737,17 @@ function handleEvent(origin: string, event: ServerEvent): void {
rejectedPeerOrigins.delete(event.peerOrigin);
awaitingApprovalPeerOrigins.delete(event.peerOrigin);
activePeerOrigins.add(event.peerOrigin);
notifyFederationChangeListeners();
break;
}
case 'federation_peers_changed': {
notifyFederationChangeListeners();
break;
}
case 'federation_approval_request_received': {
notifyFederationChangeListeners();
break;
}