feat(CUB-55): add SignalR broadcast state method with AgentStatusHub and DTO
This commit is contained in:
155
backend/Hubs/AgentStatusHub.cs
Normal file
155
backend/Hubs/AgentStatusHub.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using ControlCenter.Api.Dtos;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ControlCenter.Api.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub for broadcasting agent status updates to connected clients.
|
||||
///
|
||||
/// <para>
|
||||
/// Clients call <see cref="SendStatusUpdate"/> to broadcast a status change,
|
||||
/// and the hub relays it to all connected clients via the
|
||||
/// <see cref="IAgentStatusClient.AgentStatusChanged"/> callback.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Server-side code should use <see cref="AgentStatusHubExtensions.PushStatusUpdateAsync"/>
|
||||
/// via <c>IHubContext<AgentStatusHub, IAgentStatusClient></c> for background-service broadcasts.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Architecture note: This hub bridges OpenClaw Gateway events to SignalR clients.
|
||||
/// A background service subscribes to Gateway events and pushes them through
|
||||
/// this hub's extension methods.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class AgentStatusHub : Hub<IAgentStatusClient>
|
||||
{
|
||||
private readonly ILogger<AgentStatusHub> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentStatusHub"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger for diagnostic output.</param>
|
||||
public AgentStatusHub(ILogger<AgentStatusHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts an agent status update to all connected clients.
|
||||
///
|
||||
/// <para>
|
||||
/// Any connected client (or server-side caller) can invoke this method
|
||||
/// to push a status update to every subscriber. The update is relayed
|
||||
/// through the <see cref="IAgentStatusClient.AgentStatusChanged"/> callback.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="update">The agent status update payload to broadcast.</param>
|
||||
public async Task SendStatusUpdate(AgentStatusUpdateDto update)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Broadcasting status update for agent {AgentId}: {Status}",
|
||||
update.AgentId, update.Status);
|
||||
|
||||
await Clients.All.AgentStatusChanged(update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the calling connection to the fleet group.
|
||||
/// Once joined, the client will receive all agent status updates.
|
||||
/// </summary>
|
||||
public async Task JoinFleet()
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, FleetGroupName);
|
||||
_logger.LogDebug("Connection {ConnectionId} joined fleet group", Context.ConnectionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the calling connection from the fleet group.
|
||||
/// </summary>
|
||||
public async Task LeaveFleet()
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, FleetGroupName);
|
||||
_logger.LogDebug("Connection {ConnectionId} left fleet group", Context.ConnectionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides <see cref="Hub{T}.OnDisconnectedAsync"/> to log disconnections.
|
||||
/// SignalR automatically removes disconnected connections from all groups.
|
||||
/// </summary>
|
||||
/// <param name="exception">Exception that caused the disconnection, if any.</param>
|
||||
public override Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
_logger.LogDebug("Connection {ConnectionId} disconnected", Context.ConnectionId);
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The SignalR group name for the entire fleet (all agents).
|
||||
/// </summary>
|
||||
internal const string FleetGroupName = "fleet";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strongly-typed client interface for the AgentStatus SignalR hub.
|
||||
/// Defines the methods the server can invoke on connected clients
|
||||
/// to push real-time agent status updates.
|
||||
/// </summary>
|
||||
public interface IAgentStatusClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes an agent status change to all subscribed clients.
|
||||
/// Fired whenever an agent's operational status changes
|
||||
/// (e.g., idle → active, active → thinking, active → error).
|
||||
/// </summary>
|
||||
/// <param name="update">The full status update payload.</param>
|
||||
/// <returns>A Task that completes when the client has processed the update.</returns>
|
||||
Task AgentStatusChanged(AgentStatusUpdateDto update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for pushing real-time agent updates through
|
||||
/// the <see cref="IHubContext{T}"/> of <see cref="AgentStatusHub"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// These methods are intended to be called from background services
|
||||
/// or other server-side code that detects an agent state change,
|
||||
/// using the injected <c>IHubContext<AgentStatusHub, IAgentStatusClient></c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class AgentStatusHubExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes an agent status update to all connected clients.
|
||||
///
|
||||
/// <para>
|
||||
/// Call this from any background service when an agent's
|
||||
/// operational status changes (e.g., the Gateway reports a
|
||||
/// session transition from "running" to "done").
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="hubContext">The hub context injected via DI.</param>
|
||||
/// <param name="update">The agent status update payload.</param>
|
||||
/// <returns>A Task that completes when the message has been sent to all clients.</returns>
|
||||
public static async Task PushStatusUpdateAsync(
|
||||
this IHubContext<AgentStatusHub, IAgentStatusClient> hubContext,
|
||||
AgentStatusUpdateDto update)
|
||||
{
|
||||
await hubContext.Clients.All.AgentStatusChanged(update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes an agent status update to clients subscribed to the fleet group.
|
||||
/// </summary>
|
||||
/// <param name="hubContext">The hub context injected via DI.</param>
|
||||
/// <param name="update">The agent status update payload.</param>
|
||||
/// <returns>A Task that completes when the message has been sent to the fleet group.</returns>
|
||||
public static async Task PushStatusUpdateToFleetAsync(
|
||||
this IHubContext<AgentStatusHub, IAgentStatusClient> hubContext,
|
||||
AgentStatusUpdateDto update)
|
||||
{
|
||||
await hubContext.Clients.Group(AgentStatusHub.FleetGroupName)
|
||||
.AgentStatusChanged(update);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user