diff --git a/backend/Dtos/AgentStatusUpdateDto.cs b/backend/Dtos/AgentStatusUpdateDto.cs
new file mode 100644
index 0000000..b67e74d
--- /dev/null
+++ b/backend/Dtos/AgentStatusUpdateDto.cs
@@ -0,0 +1,75 @@
+namespace ControlCenter.Api.Dtos;
+
+///
+/// Data transfer object for broadcasting agent status updates
+/// to all connected SignalR clients.
+///
+public class AgentStatusUpdateDto
+{
+ ///
+ /// Agent identifier, e.g. "otto", "dex", "rex".
+ /// Not null — every update must identify the agent it refers to.
+ ///
+ public string AgentId { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable display name, e.g. "Otto", "Dex".
+ /// Not null — used by clients to render agent cards.
+ ///
+ public string DisplayName { get; set; } = string.Empty;
+
+ ///
+ /// Role description, e.g. "Orchestrator Agent", "Backend Specialist".
+ /// Not null — provides context for the agent's function.
+ ///
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// Current operational status of the agent.
+ /// Maps to values as lowercase strings:
+ /// "active", "idle", "thinking", "error".
+ ///
+ public string Status { get; set; } = string.Empty;
+
+ ///
+ /// Description of the agent's current task, if any.
+ /// Null when the agent is idle with no active task.
+ ///
+ public string? CurrentTask { get; set; }
+
+ ///
+ /// Task progress percentage (0–100).
+ /// Null when progress is not trackable for the current task.
+ ///
+ public int? TaskProgress { get; set; }
+
+ ///
+ /// Elapsed time string for the current task, e.g. "04m 12s".
+ /// Null when no task is active.
+ ///
+ public string? TaskElapsed { get; set; }
+
+ ///
+ /// Full session key, e.g. "agent:otto:telegram:direct:8787451565".
+ /// Not null — uniquely identifies the agent session.
+ ///
+ public string SessionKey { get; set; } = string.Empty;
+
+ ///
+ /// Communication channel the agent is operating on, e.g. "telegram", "discord", "slack".
+ /// Not null — every agent session operates on exactly one channel.
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// ISO 8601 timestamp of the agent's last activity.
+ /// Not null — used by clients to detect stale connections.
+ ///
+ public string LastActivity { get; set; } = string.Empty;
+
+ ///
+ /// Error message when the agent status is "error".
+ /// Null when the agent is not in an error state.
+ ///
+ public string? ErrorMessage { get; set; }
+}
\ No newline at end of file
diff --git a/backend/Hubs/AgentStatusHub.cs b/backend/Hubs/AgentStatusHub.cs
new file mode 100644
index 0000000..213804b
--- /dev/null
+++ b/backend/Hubs/AgentStatusHub.cs
@@ -0,0 +1,155 @@
+using ControlCenter.Api.Dtos;
+using Microsoft.AspNetCore.SignalR;
+
+namespace ControlCenter.Api.Hubs;
+
+///
+/// SignalR hub for broadcasting agent status updates to connected clients.
+///
+///
+/// Clients call to broadcast a status change,
+/// and the hub relays it to all connected clients via the
+/// callback.
+///
+///
+///
+/// Server-side code should use
+/// via IHubContext<AgentStatusHub, IAgentStatusClient> for background-service broadcasts.
+///
+///
+///
+/// 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.
+///
+///
+public class AgentStatusHub : Hub
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Logger for diagnostic output.
+ public AgentStatusHub(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Broadcasts an agent status update to all connected clients.
+ ///
+ ///
+ /// 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 callback.
+ ///
+ ///
+ /// The agent status update payload to broadcast.
+ public async Task SendStatusUpdate(AgentStatusUpdateDto update)
+ {
+ _logger.LogInformation(
+ "Broadcasting status update for agent {AgentId}: {Status}",
+ update.AgentId, update.Status);
+
+ await Clients.All.AgentStatusChanged(update);
+ }
+
+ ///
+ /// Adds the calling connection to the fleet group.
+ /// Once joined, the client will receive all agent status updates.
+ ///
+ public async Task JoinFleet()
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, FleetGroupName);
+ _logger.LogDebug("Connection {ConnectionId} joined fleet group", Context.ConnectionId);
+ }
+
+ ///
+ /// Removes the calling connection from the fleet group.
+ ///
+ public async Task LeaveFleet()
+ {
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, FleetGroupName);
+ _logger.LogDebug("Connection {ConnectionId} left fleet group", Context.ConnectionId);
+ }
+
+ ///
+ /// Overrides to log disconnections.
+ /// SignalR automatically removes disconnected connections from all groups.
+ ///
+ /// Exception that caused the disconnection, if any.
+ public override Task OnDisconnectedAsync(Exception? exception)
+ {
+ _logger.LogDebug("Connection {ConnectionId} disconnected", Context.ConnectionId);
+ return base.OnDisconnectedAsync(exception);
+ }
+
+ ///
+ /// The SignalR group name for the entire fleet (all agents).
+ ///
+ internal const string FleetGroupName = "fleet";
+}
+
+///
+/// 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.
+///
+public interface IAgentStatusClient
+{
+ ///
+ /// 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).
+ ///
+ /// The full status update payload.
+ /// A Task that completes when the client has processed the update.
+ Task AgentStatusChanged(AgentStatusUpdateDto update);
+}
+
+///
+/// Extension methods for pushing real-time agent updates through
+/// the of .
+///
+///
+/// These methods are intended to be called from background services
+/// or other server-side code that detects an agent state change,
+/// using the injected IHubContext<AgentStatusHub, IAgentStatusClient>.
+///
+///
+public static class AgentStatusHubExtensions
+{
+ ///
+ /// Pushes an agent status update to all connected clients.
+ ///
+ ///
+ /// 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").
+ ///
+ ///
+ /// The hub context injected via DI.
+ /// The agent status update payload.
+ /// A Task that completes when the message has been sent to all clients.
+ public static async Task PushStatusUpdateAsync(
+ this IHubContext hubContext,
+ AgentStatusUpdateDto update)
+ {
+ await hubContext.Clients.All.AgentStatusChanged(update);
+ }
+
+ ///
+ /// Pushes an agent status update to clients subscribed to the fleet group.
+ ///
+ /// The hub context injected via DI.
+ /// The agent status update payload.
+ /// A Task that completes when the message has been sent to the fleet group.
+ public static async Task PushStatusUpdateToFleetAsync(
+ this IHubContext hubContext,
+ AgentStatusUpdateDto update)
+ {
+ await hubContext.Clients.Group(AgentStatusHub.FleetGroupName)
+ .AgentStatusChanged(update);
+ }
+}
\ No newline at end of file
diff --git a/backend/Program.cs b/backend/Program.cs
index 80fb10e..c45c355 100644
--- a/backend/Program.cs
+++ b/backend/Program.cs
@@ -1,4 +1,5 @@
using ControlCenter.Api.Data;
+using ControlCenter.Api.Hubs;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@@ -6,6 +7,9 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddOpenApi();
+// Register SignalR for real-time agent status updates
+builder.Services.AddSignalR();
+
// Register DbContext with PostgreSQL
builder.Services.AddDbContext(options =>
{
@@ -28,4 +32,7 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
+// Map SignalR hubs
+app.MapHub("/hubs/agent-status");
+
app.Run();
\ No newline at end of file