diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..c2c0c5e
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,21 @@
+## .NET
+bin/
+obj/
+*.user
+*.suo
+*.cache
+*.dll
+*.pdb
+
+## IDE
+.vs/
+.idea/
+*.swp
+*~
+
+## OS
+.DS_Store
+Thumbs.db
+
+## Environment
+.env
\ No newline at end of file
diff --git a/backend/Configurations/AgentConfiguration.cs b/backend/Configurations/AgentConfiguration.cs
new file mode 100644
index 0000000..0f9d304
--- /dev/null
+++ b/backend/Configurations/AgentConfiguration.cs
@@ -0,0 +1,88 @@
+using ControlCenter.Api.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ControlCenter.Api.Configurations;
+
+///
+/// EF Core entity type configuration for the agents table.
+/// Enforces snake_case naming, required fields, and index design.
+///
+public class AgentConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ // Table name — snake_case
+ builder.ToTable("agents");
+
+ // Primary key
+ builder.HasKey(a => a.Id);
+ builder.Property(a => a.Id)
+ .HasColumnName("id")
+ .ValueGeneratedOnAdd();
+
+ // Status — stored as PostgreSQL enum via Npgsql
+ builder.Property(a => a.Status)
+ .HasColumnName("status")
+ .HasColumnType("agent_status")
+ .IsRequired();
+
+ // Task — nullable text
+ builder.Property(a => a.Task)
+ .HasColumnName("task")
+ .HasColumnType("text");
+
+ // Progress — nullable integer (0–100)
+ builder.Property(a => a.Progress)
+ .HasColumnName("progress");
+
+ // Session key — required, not null
+ builder.Property(a => a.SessionKey)
+ .HasColumnName("session_key")
+ .HasColumnType("text")
+ .IsRequired();
+
+ // Channel — required, not null
+ builder.Property(a => a.Channel)
+ .HasColumnName("channel")
+ .HasColumnType("text")
+ .IsRequired();
+
+ // Last activity — required, defaults to now()
+ builder.Property(a => a.LastActivity)
+ .HasColumnName("last_activity")
+ .HasColumnType("timestamptz")
+ .IsRequired();
+
+ // Created at — auto-set on insert
+ builder.Property(a => a.CreatedAt)
+ .HasColumnName("created_at")
+ .HasColumnType("timestamptz")
+ .IsRequired()
+ .HasDefaultValueSql("now()");
+
+ // Updated at — auto-set on insert and update
+ builder.Property(a => a.UpdatedAt)
+ .HasColumnName("updated_at")
+ .HasColumnType("timestamptz")
+ .IsRequired()
+ .HasDefaultValueSql("now()");
+
+ // Indexes
+ // Sessions are looked up by session_key frequently
+ builder.HasIndex(a => a.SessionKey)
+ .HasDatabaseName("ix_agents_session_key")
+ .IsUnique();
+
+ // Agents are filtered by channel for channel-specific queries
+ builder.HasIndex(a => a.Channel)
+ .HasDatabaseName("ix_agents_channel");
+
+ // Agents are filtered by status for fleet health monitoring
+ builder.HasIndex(a => a.Status)
+ .HasDatabaseName("ix_agents_status");
+
+ // Check constraint: progress must be 0–100 if present
+ builder.ToTable(t => t.HasCheckConstraint("ck_agents_progress_range", "progress IS NULL OR (progress >= 0 AND progress <= 100)"));
+ }
+}
\ No newline at end of file
diff --git a/backend/ControlCenter.Api.csproj b/backend/ControlCenter.Api.csproj
new file mode 100644
index 0000000..d2896da
--- /dev/null
+++ b/backend/ControlCenter.Api.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/backend/ControlCenter.Api.http b/backend/ControlCenter.Api.http
new file mode 100644
index 0000000..f621eb8
--- /dev/null
+++ b/backend/ControlCenter.Api.http
@@ -0,0 +1,6 @@
+@ControlCenter.Api_HostAddress = http://localhost:5178
+
+GET {{ControlCenter.Api_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/backend/Data/AppDbContext.cs b/backend/Data/AppDbContext.cs
new file mode 100644
index 0000000..3277f00
--- /dev/null
+++ b/backend/Data/AppDbContext.cs
@@ -0,0 +1,29 @@
+using ControlCenter.Api.Configurations;
+using ControlCenter.Api.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ControlCenter.Api.Data;
+
+///
+/// EF Core DbContext for the Control Center database.
+/// All table and column names use snake_case via explicit HasColumnName configuration.
+///
+public class AppDbContext : DbContext
+{
+ public AppDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet Agents => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // Apply all entity type configurations from the Configurations namespace
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(AgentConfiguration).Assembly);
+
+ // Map the AgentStatus enum to a PostgreSQL enum type named "agent_status"
+ // This must be called after ApplyConfigurations to ensure the model is built
+ // before the enum mapping is applied.
+ modelBuilder.HasPostgresEnum();
+
+ base.OnModelCreating(modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/backend/Data/AppDbContextFactory.cs b/backend/Data/AppDbContextFactory.cs
new file mode 100644
index 0000000..7b82843
--- /dev/null
+++ b/backend/Data/AppDbContextFactory.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore;
+using ControlCenter.Api.Entities;
+
+namespace ControlCenter.Api.Data;
+
+///
+/// Design-time factory for AppDbContext, used by EF Core tools (dotnet ef)
+/// to create migrations without requiring a running application.
+///
+public class AppDbContextFactory : Microsoft.EntityFrameworkCore.Design.IDesignTimeDbContextFactory
+{
+ public AppDbContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+
+ // Connection string for design-time operations (migrations).
+ // In production, this comes from appsettings / environment variables.
+ var connectionString = "Host=localhost;Database=control_center;Username=postgres;Password=postgres";
+
+ optionsBuilder.UseNpgsql(connectionString, npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
+ });
+
+ return new AppDbContext(optionsBuilder.Options);
+ }
+}
\ No newline at end of file
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/Entities/Agent.cs b/backend/Entities/Agent.cs
new file mode 100644
index 0000000..67be593
--- /dev/null
+++ b/backend/Entities/Agent.cs
@@ -0,0 +1,59 @@
+namespace ControlCenter.Api.Entities;
+
+///
+/// Represents an agent's current state in the Control Center.
+/// Each row tracks one agent session's status, task, and activity.
+///
+public class Agent
+{
+ ///
+ /// Primary key — UUID generated on insert.
+ ///
+ public Guid Id { get; set; }
+
+ ///
+ /// Current operational status of the agent.
+ /// Stored as an enum in PostgreSQL via Npgsql.
+ ///
+ public AgentStatus Status { get; set; } = AgentStatus.Idle;
+
+ ///
+ /// Description of the agent's current task, if any.
+ /// Nullable — not all agents have an active task.
+ ///
+ public string? Task { get; set; }
+
+ ///
+ /// Task progress percentage (0–100).
+ /// Nullable — progress is only meaningful when an agent has a trackable task.
+ ///
+ public int? Progress { get; set; }
+
+ ///
+ /// Full session key, e.g. "agent:otto:telegram:direct:8787451565".
+ /// Not null — every agent row must be associated with a 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;
+
+ ///
+ /// Timestamp of the agent's last activity.
+ /// Default: current UTC timestamp on insert.
+ ///
+ public DateTime LastActivity { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Row creation timestamp. Set automatically on insert.
+ ///
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Row last-update timestamp. Updated automatically on any modification.
+ ///
+ public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/backend/Entities/AgentStatus.cs b/backend/Entities/AgentStatus.cs
new file mode 100644
index 0000000..3846fcc
--- /dev/null
+++ b/backend/Entities/AgentStatus.cs
@@ -0,0 +1,13 @@
+namespace ControlCenter.Api.Entities;
+
+///
+/// Agent operational status enum.
+/// Maps to the agent_status enum type in PostgreSQL.
+///
+public enum AgentStatus
+{
+ Active = 0,
+ Idle = 1,
+ Thinking = 2,
+ Error = 3
+}
\ 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/Migrations/20260426101703_CreateAgentsTable.Designer.cs b/backend/Migrations/20260426101703_CreateAgentsTable.Designer.cs
new file mode 100644
index 0000000..8b99837
--- /dev/null
+++ b/backend/Migrations/20260426101703_CreateAgentsTable.Designer.cs
@@ -0,0 +1,94 @@
+//
+using System;
+using ControlCenter.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ControlCenter.Api.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260426101703_CreateAgentsTable")]
+ partial class CreateAgentsTable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "agent_status", new[] { "active", "idle", "thinking", "error" });
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ControlCenter.Api.Entities.Agent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("channel");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamptz")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("LastActivity")
+ .HasColumnType("timestamptz")
+ .HasColumnName("last_activity");
+
+ b.Property("Progress")
+ .HasColumnType("integer")
+ .HasColumnName("progress");
+
+ b.Property("SessionKey")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("session_key");
+
+ b.Property("Status")
+ .HasColumnType("agent_status")
+ .HasColumnName("status");
+
+ b.Property("Task")
+ .HasColumnType("text")
+ .HasColumnName("task");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamptz")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Channel")
+ .HasDatabaseName("ix_agents_channel");
+
+ b.HasIndex("SessionKey")
+ .IsUnique()
+ .HasDatabaseName("ix_agents_session_key");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("ix_agents_status");
+
+ b.ToTable("agents", null, t =>
+ {
+ t.HasCheckConstraint("ck_agents_progress_range", "progress IS NULL OR (progress >= 0 AND progress <= 100)");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/Migrations/20260426101703_CreateAgentsTable.cs b/backend/Migrations/20260426101703_CreateAgentsTable.cs
new file mode 100644
index 0000000..4b38049
--- /dev/null
+++ b/backend/Migrations/20260426101703_CreateAgentsTable.cs
@@ -0,0 +1,61 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ControlCenter.Api.Migrations
+{
+ ///
+ public partial class CreateAgentsTable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("Npgsql:Enum:agent_status", "active,idle,thinking,error");
+
+ migrationBuilder.CreateTable(
+ name: "agents",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ status = table.Column(type: "agent_status", nullable: false),
+ task = table.Column(type: "text", nullable: true),
+ progress = table.Column(type: "integer", nullable: true),
+ session_key = table.Column(type: "text", nullable: false),
+ channel = table.Column(type: "text", nullable: false),
+ last_activity = table.Column(type: "timestamptz", nullable: false),
+ created_at = table.Column(type: "timestamptz", nullable: false, defaultValueSql: "now()"),
+ updated_at = table.Column(type: "timestamptz", nullable: false, defaultValueSql: "now()")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_agents", x => x.id);
+ table.CheckConstraint("ck_agents_progress_range", "progress IS NULL OR (progress >= 0 AND progress <= 100)");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_agents_channel",
+ table: "agents",
+ column: "channel");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_agents_session_key",
+ table: "agents",
+ column: "session_key",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "ix_agents_status",
+ table: "agents",
+ column: "status");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "agents");
+ }
+ }
+}
diff --git a/backend/Migrations/AppDbContextModelSnapshot.cs b/backend/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 0000000..c32b60a
--- /dev/null
+++ b/backend/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,91 @@
+//
+using System;
+using ControlCenter.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ControlCenter.Api.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "agent_status", new[] { "active", "idle", "thinking", "error" });
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ControlCenter.Api.Entities.Agent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("channel");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamptz")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("now()");
+
+ b.Property("LastActivity")
+ .HasColumnType("timestamptz")
+ .HasColumnName("last_activity");
+
+ b.Property("Progress")
+ .HasColumnType("integer")
+ .HasColumnName("progress");
+
+ b.Property("SessionKey")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("session_key");
+
+ b.Property("Status")
+ .HasColumnType("agent_status")
+ .HasColumnName("status");
+
+ b.Property("Task")
+ .HasColumnType("text")
+ .HasColumnName("task");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamptz")
+ .HasColumnName("updated_at")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Channel")
+ .HasDatabaseName("ix_agents_channel");
+
+ b.HasIndex("SessionKey")
+ .IsUnique()
+ .HasDatabaseName("ix_agents_session_key");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("ix_agents_status");
+
+ b.ToTable("agents", null, t =>
+ {
+ t.HasCheckConstraint("ck_agents_progress_range", "progress IS NULL OR (progress >= 0 AND progress <= 100)");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/Program.cs b/backend/Program.cs
new file mode 100644
index 0000000..c45c355
--- /dev/null
+++ b/backend/Program.cs
@@ -0,0 +1,38 @@
+using ControlCenter.Api.Data;
+using ControlCenter.Api.Hubs;
+using Microsoft.EntityFrameworkCore;
+
+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 =>
+{
+ var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? "Host=localhost;Database=control_center;Username=postgres;Password=postgres";
+
+ options.UseNpgsql(connectionString, npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
+ });
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+app.UseHttpsRedirection();
+
+// Map SignalR hubs
+app.MapHub("/hubs/agent-status");
+
+app.Run();
\ No newline at end of file
diff --git a/backend/Properties/launchSettings.json b/backend/Properties/launchSettings.json
new file mode 100644
index 0000000..5a40284
--- /dev/null
+++ b/backend/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5178",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7041;http://localhost:5178",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/backend/appsettings.Development.json b/backend/appsettings.Development.json
new file mode 100644
index 0000000..ff66ba6
--- /dev/null
+++ b/backend/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/backend/appsettings.json b/backend/appsettings.json
new file mode 100644
index 0000000..0181f7d
--- /dev/null
+++ b/backend/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Database=control_center;Username=postgres;Password=postgres"
+ }
+}