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:
@@ -469,6 +469,13 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.run();
|
||||
}
|
||||
|
||||
// Notify admin users that a new approval request arrived
|
||||
connectionManager.sendToAdmins({
|
||||
type: 'federation_approval_request_received' as const,
|
||||
origin: sourceOrigin,
|
||||
instanceName: reqInstanceName ?? undefined,
|
||||
});
|
||||
|
||||
return reply.code(202).send({
|
||||
queued: true,
|
||||
message: 'Request queued for admin approval',
|
||||
@@ -514,6 +521,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
return reply.code(200).send({ accepted: true });
|
||||
}
|
||||
if (existing.status === 'awaiting_approval') {
|
||||
@@ -535,6 +544,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
return reply.code(200).send({ accepted: true });
|
||||
}
|
||||
// Pending — update with new secret and activate
|
||||
@@ -547,6 +558,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.where(eq(schema.federationPeers.id, existing.id))
|
||||
.run();
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
return reply.code(200).send({ accepted: true });
|
||||
}
|
||||
|
||||
@@ -561,6 +574,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
createdAt: Date.now(),
|
||||
}).run();
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
return reply.code(200).send({ accepted: true });
|
||||
},
|
||||
);
|
||||
@@ -733,6 +748,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.where(eq(schema.federationPeers.id, peer.id))
|
||||
.run();
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
// Push federation_peer_rejected WS event to affected users
|
||||
const entries = db
|
||||
.select({
|
||||
@@ -1041,6 +1058,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.where(eq(schema.peerApprovalRequests.id, id))
|
||||
.run();
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
const peer = db
|
||||
.select()
|
||||
.from(schema.federationPeers)
|
||||
@@ -1137,6 +1156,8 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.run();
|
||||
}
|
||||
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
|
||||
db.delete(schema.peerApprovalRequests)
|
||||
.where(eq(schema.peerApprovalRequests.id, id))
|
||||
.run();
|
||||
|
||||
@@ -145,6 +145,8 @@ async function performHandshake(
|
||||
.set({ status: 'awaiting_approval' })
|
||||
.where(eq(schema.federationPeers.id, peerId))
|
||||
.run();
|
||||
const { connectionManager } = await import('../ws/handler.js');
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
return { status: 'pending', error: 'Awaiting admin approval on remote instance' };
|
||||
}
|
||||
|
||||
@@ -154,6 +156,8 @@ async function performHandshake(
|
||||
.set({ status: 'active', lastSeenAt: Date.now() })
|
||||
.where(eq(schema.federationPeers.id, peerId))
|
||||
.run();
|
||||
const { connectionManager } = await import('../ws/handler.js');
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
return { status: 'active', peerId };
|
||||
}
|
||||
|
||||
@@ -174,6 +178,8 @@ async function performHandshake(
|
||||
.set({ status: 'rejected' })
|
||||
.where(eq(schema.federationPeers.id, peerId))
|
||||
.run();
|
||||
const { connectionManager } = await import('../ws/handler.js');
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
return { status: 'rejected', error: errorMessage };
|
||||
}
|
||||
|
||||
|
||||
@@ -283,6 +283,7 @@ async function processOutboxTick(): Promise<void> {
|
||||
})
|
||||
.where(eq(schema.federationPeers.id, peerId))
|
||||
.run();
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
} else {
|
||||
console.warn(
|
||||
`[federation-worker] Peer ${peerOrigin} returned HTTP ${response.status}`,
|
||||
@@ -388,6 +389,8 @@ async function resolvePendingPeers(): Promise<void> {
|
||||
switch (result.status) {
|
||||
case 'active':
|
||||
console.log(`[federation-worker] Auto-peered with ${peerOrigin} — entries will deliver next tick`);
|
||||
// Notify admins of peer state change
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
break;
|
||||
|
||||
case 'rejected': {
|
||||
@@ -417,6 +420,8 @@ async function resolvePendingPeers(): Promise<void> {
|
||||
|
||||
// Push federation_peer_rejected WS event to affected users
|
||||
pushPeerRejectedEvent(peerOrigin, contextMap);
|
||||
// Notify admins of peer state change
|
||||
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -876,6 +876,18 @@ class ConnectionManager {
|
||||
return this.connections;
|
||||
}
|
||||
|
||||
/** Send an event to all connected admin users. */
|
||||
sendToAdmins(event: ServerEvent): void {
|
||||
const db = getDb();
|
||||
for (const userId of this.connections.keys()) {
|
||||
const user = db.select({ isAdmin: schema.users.isAdmin })
|
||||
.from(schema.users).where(eq(schema.users.id, userId)).get();
|
||||
if (user?.isAdmin === 1) {
|
||||
this.sendToUser(userId, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Push a fresh ready payload to a specific user, forcing full store re-sync. */
|
||||
pushReadyPayload(userId: string): void {
|
||||
const connections = this.getUserConnections(userId);
|
||||
|
||||
Reference in New Issue
Block a user